在 Neovim 里面使用 vim-fugitive 的工作流
引言
最近在使用 Git + Neovim 的时候,发现我的工作流还是有一些不那么顺畅的地方。我习惯性退出 Neovim,然后在命令行写 Git 相关的命令,并且在提交代码变更前我习惯用 delta 查看 diff 信息。为了减少要打的字符,我还开启了 Oh My Zsh 的 git
插件,这样我就可以用一堆缩写了,比如 ga = git add
、gcmsg = git commit -m
不顺畅主要的原因是我发现我经常在 Neovim 和命令行之间切换,就像这样
flowchart i(编辑代码) --->|退出 Neovim| j(用 git diff 看下 diff 信息) ---> k(发现有点小问题,需要修改) k --->|打开 Neovim| i
就在这时我想起了躺在我插件列表的 vim-fugitive
,我记得它在 GitHub 主页 说过 it’s “so awesome, it should be illegal”,当时没有深究。于是今天我经过一番钻研之后,下面是我总结的一些使用 vim-fugitive
的工作流
Workflows
Preliminary
大多数命令行的的 Git 命令,你都可以替换为 :Git ...
,比如 git blame
就是 :Git blame
,其中 :Git
可以缩写为 :G
Git status
用 vim-fugitive
查看当前仓库的状态非常简单,直接用
:G
输出类似下面这样
Head: main
Merge: origin/main
Help: g?
Untracked (1)
? foo.py
Staged (1)
A bar.py
Unpushed to origin/main (1)
7c08152 add .gitignore
其中
- Untracked 表示新文件,还没有用 git 管理
- Staged 表示添加到 staging area 的文件
- Unpushed to origin/main 表示本地的 Commit
每个文件前面有一个文件状态码,不同的状态码的含义并不同
''
unmodifiedM
modifiedT
file type changedA
addedD
deletedR
renamedc
copiedU
updated but unmerged?
untracked!
ignored
Git add & git commit
还是用前面的例子,如果我们想要把 foo.py
添加到 staging area 里面,就将光标停在 foo.py
上,然后按下 s
Head: main
Merge: origin/main
Help: g?
Untracked (1)
? foo.py
Staged (1)
A bar.py
Unpushed to origin/main (1)
7c08152 add .gitignore
你应该可以看到变成了下面这样
Head: main
Merge: origin/main
Help: g?
Staged (2)
A bar.py
A foo.py
Unpushed to origin/main (1)
7c08152 add .gitignore
要撤销 staging 也很容易,按 u
就行,这个跟 Vim 的操作是一致的,很好记
最后,修改完文件之后,移动光标到文件上,按 cc
,就会执行 git commit
,可以看到弹出了一个 buffer 让我们填写 commit message,保存退出 commit 就添加成功了。除此之外,还有下面这些常用的命令
ca
,ca = git commit --amend
,将修改合并到 last commit 并且重新填写 commit message,ce
,ca = git commit --amend --no-edit
,将修改合并到 last commit 并且保持 commit message 不变cw
,用来修改 last commit message
将光标停留在 untracked file 上,然后按下 I
,就可以看到文件的状态变成了 A
。本质是帮我们执行了 git add --intent-to-add
。git add --intent-to-add
的好处是本来 untracked file 也可以看 diff 了1,普通的 git add
做不到这点,前者对应 git status
的 Changes not staged for commit,后者对应 Changes to be committed,可以用 git status
验证一下
See Git diff
另外一个常用的功能是,查看文件的 diff 信息看我们具体修改了什么,比如下面的
Head: main
Merge: origin/main
Help: g?
Staged (2)
A bar.py
A foo.py
Unpushed to origin/main (1)
7c08152 add .gitignore
如果该文件的代码变更并不多,那么直接将光标停在文件上按下 =
就可以看到 diff 信息了
=
就可以将 diff 信息关闭Head: main
Merge: origin/main
Help: g?
Staged (2)
A bar.py
A foo.py
@@ -0,0 +1,2 @@
+def main():
+ print("Please enter a integer")
Unpushed to origin/main (1)
7c08152 add .gitignore
但如果该文件的代码变更很大,那么可能 side-by-side 的 diff 信息展示更合理,此时只需要将光标停在文件上按下 dv
就可以,
dv
看 diff 信息,那么除了看之外,你还可以进行修改Git log
直接用下面的命令就可以看到所有的 Commit 的信息
:G log
可以看到新打开的 buffer 里展示了如下的信息
commit 23ffed83d3ac5b3b7de37ecb29b55e5cf35b5f65
Author: MartinLwx <*****************>
Date: Thu Oct 10 22:07:02 2024 +0800
add bar.py
commit 7c081527a2dbadafbaf7907c5b547afc9e725ac9
Author: MartinLwx <*****************>
Date: Thu Oct 10 22:05:54 2024 +0800
add .gitignore
将光标停留在 commit 上 按下 o
键 就可以在新的 buffer 看到这个 commit 的详细信息
]]
和 [[
可以快速在不同的 commit 之间移动Git blame
这里单独挑 git blame 出来是因为 vim-fugitive 的 git blame 展示非常直观,左边清楚展示了是什么 commit 引入的变更,以及是哪个作者引入的。对应的命令是 :G blame
23ffed83 1 (MartinLwx 2024-10-10 22:07:02 +0800 1) def main():
23ffed83 2 (MartinLwx 2024-10-10 22:07:02 +0800 2) print("Please enter a integer")
更厉害的是,这个 :G blame
的输出是可以交互的,比如你想要展示 commit 更详细的信息,只需要将光标停留在 commit 上,然后按下 o
Split code change into multiple commits
很多时候,你修改了代码完成了多个功能的开发或者是 Bug 修复,但是你中途忘记一边改一边提交 commit 了,那么要如何拆分不同的代码变更分别提交 commit?(毕竟一个 commit 解决一个问题是好的实践)
先来看 foo.py
文件,假设它本来是这样子的
def main():
print("Please enter a integer")
然后我们做了如下的代码变更,并且假设每个分支代表一个特性的添加或者是功能的修复
def main():
i = input("Please enter a integer")
if i == 0:
print("i == 0")
elif i == 1:
print("i == 1")
else:
print("i not in [0, 1]")
将光标停留在文件上,按 =
就可以看到 diff 信息
Head: main
Push: origin/main
Help: g?
Unstaged (1)
M foo.py
@@ -1,2 +1,8 @@
def main():
i = input("Please enter a integer")
+ if i == 0:
+ print("i == 0")
+ elif i == 1:
+ print("i == 1")
+ else:
+ print("i not in [0, 1]")
Unpushed to * (3)
d2d951f add foo.py
23ffed8 add bar.py
7c08152 add .gitignore
接下来用 V
选中如下 2 行
+ if i == 0:
+ print("i == 0")
然后按下 s
单独 stage 部分代码变更,此时下方就会出现一个 Staged 的文件
Head: main
Push: origin/main
Help: g?
Unstaged (1)
M foo.py
@@ -2,3 +2,7 @@ def main():
i = input("Please enter a integer")
if i == 0:
print("i == 0")
+ elif i == 1:
+ print("i == 1")
+ else:
+ print("i not in [0, 1]")
Staged (1)
M foo.py
Unpushed to * (3)
d2d951f add foo.py
23ffed8 add bar.py
7c08152 add .gitignore
用 cc
提交 commit 即可
Head: main
Push: origin/main
Help: g?
Unstaged (1)
M foo.py
@@ -2,3 +2,7 @@ def main():
i = input("Please enter a integer")
if i == 0:
print("i == 0")
+ elif i == 1:
+ print("i == 1")
+ else:
+ print("i not in [0, 1]")
Unpushed to * (4)
c46474f add i == 0
d2d951f add foo.py
23ffed8 add bar.py
7c08152 add .gitignore
可以看到我们新添加了一个 commit c46474f add i == 0
,这个步骤可以多次执行
]c
和 [c
可以在不同的 diff 信息之间快速移动如法炮制,就完成了代码变更的拆分
Git rebase -i
git rebase -i
是我另外一个常用的功能,这里我们可以尝试将刚才拆分的 3 个 commit 合并
68081e9 add else branch
f9dfc93 add i == 1
c46474f add i == 0
先用 :G log
展开 commit history
commit 68081e928ad8dbbafc8e20ebefaeb7f3dad968bf
Author: MartinLwx <****************>
Date: Thu Oct 10 23:10:14 2024 +0800
add else branch
commit f9dfc93d9846a3cec6220b0f91b8e4a84dcb9367
Author: MartinLwx <****************>
Date: Thu Oct 10 22:57:04 2024 +0800
add i == 1
commit c46474f9cf8c34071fa95f797e94ffc75b1ee3e2
Author: MartinLwx <****************>
Date: Thu Oct 10 22:55:03 2024 +0800
add i == 0
然后将光标停留在 add i == 0
这一个对应的 commit 上,按下 ri
(Hint:ri
= rebase -i
),然后进行如下修改
r c46474f add i == 0
s f9dfc93 add i == 1
s 68081e9 add else branch
合并之后
Head: main
Push: origin/main
Help: g?
Unpushed to * (4)
c814dce merge 3 commits
d2d951f add foo.py
23ffed8 add bar.py
7c08152 add .gitignore
Resolve merge conflicts
最后看一下,分支合并的时候要如何使用 vim-fugitive,假设在另外一个叫作 feature
的分支上,foo.py
代码长这样子
def main():
i = input("Please enter a integer")
if i == 0:
i = i + 1
else:
i = i + 2
我们在 main
分支下用
:G merge feature
此时用 :G
看到的会是这样子
Head: main
Push: origin/main
Help: g?
Unstaged (1)
U foo.py
Staged (1)
U foo.py
Unpushed to * (6)
21055dc add else branch
f9dfc93 add i == 1
c46474f add i == 0
d2d951f add foo.py
23ffed8 add bar.py
7c08152 add .gitignore
其中我们需要关注的是 Unstaged 里面的文件,需要修改这些文件解决合并冲突。为了解决方便冲突,光标在文件上停留然后按下 dv
,就可以看到打开了 3 个 buffer
- 左边的是发起合并的分支的代码,也就是
main
分支 - 中间的是冲突的代码
- 右边的是被合并的分支的代码,也就是
feature
分支
只需要在中间的 buffer 修改好代码冲突,用 :Gwrite
就完成了文件的保存以及将其添加到 staging area 里
但有时候,你可能只是想要采用某一个分支的代码,那么在中间修改冲突比较繁琐而且没啥意义,这个时候直接移动到左边的 buffer 或者右边的 buffer(取决于你想要哪一个分支的代码),然后用 :Gwrite!
进行文件保存以及将其添加到 staging area 里
无论是上面哪一种情况,最后都需要用 cc
提交一下 commit,分支就合并好了
Head: main
Push: origin/main
Help: g?
Unpushed to * (8)
d23d66e Merge branch 'feature'
6ad1330 nonsense update
21055dc add else branch
f9dfc93 add i == 1
c46474f add i == 0
d2d951f add foo.py
23ffed8 add bar.py
7c08152 add .gitignore
总结
以上就是我发现的 vim-fugitive 的工作流,并且使用了有一会。对于我而言,我发现确实之前的不顺畅感消失了,我感觉在 Neovim 里面用 Git 更加高效了,因为我不再需要频繁地在 Neovim、Terminal 之间进行上下文切换。而频繁的上下文切换可是会降低生产力的2