My workflows of using vim-fugitive in Neovim
Intro
Recently, while using Git with Neovim, I noticed that my workflow isn’t as smooth as I’d like it to be. I tend to exit Neovim, and then type git commands in the terminal. Before committing code changes, I usually check the diff information using delta. To cut down the number of keystrokes, I’ve also enabled the Oh My Zsh’s git
plugin, so I can use a bunch of shortcuts like ga = git add
and gcmsg = git commit -m
The main reason for the lack of smoothness is I often find myself switching between Neovim and the terminal, like this.
flowchart i(Edit code) --->|Exit Neovim| j(Use git diff to see diff information) ---> k(The code need to re-edit) k --->|Open Neovim| i
That’s when I recalled vim-fugitive
, which had been sitting in my plugin list. I recall its GitHub page saying it’s “so awesome, it should be illegal”, but I never really looked into it. So after diving into it today, here’s a summary of the workflow I founded.
Workflows
Preliminary
Most of the git commands you used in the terminal can be replaced with :Git ...
. For example, the git blame
’s corresponding command in vim-fugitive is :Git blame
. You can shorten :Git
to just :G
.
Git status
To show the status of files in a git repo is quite simple, you just need to use
:G
The output should be like
Head: main
Merge: origin/main
Help: g?
Untracked (1)
? foo.py
Staged (1)
A bar.py
Unpushed to origin/main (1)
7c08152 add .gitignore
There is a status code before each file.
''
unmodifiedM
modifiedT
file type changedA
addedD
deletedR
renamedc
copiedU
updated but unmerged?
untracked!
ignored
Git add & git commit
Let’s use the previous example again. If we want to add foo.py
to the staging area, place the cursor over foo.py
and press 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
The change will be
Head: main
Merge: origin/main
Help: g?
Staged (2)
A bar.py
A foo.py
Unpushed to origin/main (1)
7c08152 add .gitignore
To undo the staging process is quite easy and intuitive, just press u
Finally, after modifying the file, move the cursor over the file and press cc
(Hint: cc = git commit
). You will see a buffer pop up and you can edit the commit message. After saving the code changes will be committed. In addition, here are some other commonly used commands
ca
,ca = git commit --amend
ce
,ca = git commit --amend --no-edit
cw
, reword the last commit message
Place the cursor over the untracked file, and press I
. You will find that the status code of this file becomes A
. Essentially, this helps us execute git add --intent-to-add
. The benefit of git add --intent-to-add
is that now we can see the diff for the untracked files1, which is something regular git add
can’t do. The former corresponds to Changes not staged for commit in git status
, while the latter corresponds to Changes to be committed. You can verify this by running git status
See Git diff
To see the git diff in the following buffer
Head: main
Merge: origin/main
Help: g?
Staged (2)
A bar.py
A foo.py
Unpushed to origin/main (1)
7c08152 add .gitignore
If the code change is small, you may just place the cursor over the target file and press =
. And the output will be like
=
again to hide the diff information.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
However, usually, your code change will be huge. Then a side-by-side view may be more appropriate. To do this, you just need to replace =
with dv
dv
, you can also edit the file content as you withGit log
To see the git log, just type
:G log
The output should be like
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
Place your cursor over the target commit and press o
, and the details will be shown in another buffer.
]]
and [[
to quickly move among commitsGit blame
This may be my favorite feature because it’s just so convenient and intuitive. The output of :G blame
is
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")
You can see clearly who should be blamed if a line goes wrong for some reason. What’s more, we can interact with this buffer. For example, we can press o
to see the details of a specific commit.
Split code change into multiple commits
Many times, after you’ve made multiple code changes, completing several features or bug fixes, you forget to commit along the way. So how do you split different code changes into separate commits? (After all, it’s a good practice to have one commit to do one thing at a time)
Let’s assume the code of foo.py
is
def main():
print("Please enter a integer")
And we add the following code changes. All we want to do is split the if, elif, else
three cases into three commits.
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]")
Place the cursor over foo.py
and press =
to see the diff information.
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
Now use V
to select the following 2 lines.
+ if i == 0:
+ print("i == 0")
And then, press s
to stage the partial code change
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
Finally, use cc
to 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
The above output indicates that we have successfully added a new commit c46474f
]c
and [c
to quickly move among different hunkWe just need to do this multiple times :)
Git rebase -i
git rebase -i
is another feature I commonly use. Here, we can try to combine the three commits we just split.
68081e9 add else branch
f9dfc93 add i == 1
c46474f add i == 0
First, use :G log
to show the 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
And then, place the cursor over the corresponding commit of add i == 0
, and press ri
(Hint:ri
= rebase -i
). Then modify the code as I do
r c46474f add i == 0
s f9dfc93 add i == 1
s 68081e9 add else branch
After rebasing, the commit history should be like
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
Finally, let’s learn how to resolve the merge conflicts when we merge two branches using vim-fugitive. Suppose on another branch called feature
, the foo.py
file looks like this.
def main():
i = input("Please enter a integer")
if i == 0:
i = i + 1
else:
i = i + 2
We can type the following command in main
branch.
:G merge feature
Now the output of :G
should be like
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
What we need to focus on are the files in Unstaged, which need to be modified to resolve merge conflicts. To make resolving conflicts easier, move the cursor over the file and press dv
, and you’ll see the three buffers open.
- The leftmost buffer contains the code on the
main
branch - The middle buffer contains the conflicting code
- The rightmost buffer contains the code on the
feature
branch
After resolving the conflicts in the middle buffer, use :Gwrite
to save the changes and add the file to the staging area
However, sometimes you may just want to use the code from one specific branch. In this case, modifying the conflict in the middle can be cumbersome and pointless. Instead, you can directly move to the left or right buffer (depending on which branch’s code you want) and use :Gwrite!
to save and stage it.
In either case, you’ll ultimately need to use cc
to commit, and the branch will be successfully merged.
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
Wrap-up
This is the workflow I’ve discovered with vim-fugitive, and I’ve been using it for a while. For me, I’ve found that the previous feeling of discomfort has disappeared. I feel that using Git in Neovim has become much more efficient because I no longer need to frequently switch contexts between Neovim and the Terminal. And frequent context switching will reduce productivity2