Vim Fugitive by Tim Pope is a Git wrapper for Vim. Its purpose is to integrate Git inside Vim, providing easier access to the most common features, and some additional ones that would be harder to replicate from the command-line interface (CLI).
Beginners might find Fugitive difficult because it provides a new interface and requires both a good knowledge of Git (concepts and commands from the first 3 chapters of the Git Book) and Vim (Ex commands, buffers, windows, diffs).
The reference documentation for Fugitive is accessible with :help fugitive
.
It contains the list of commands, keybindings, and object specifiers. The
purpose of this article is to bridge the gap between a working knowledge of Git
and the use of Fugitive through concrete examples.
§Introduction
This article iterates on a FizzBuzz implementation in Python to demonstrate the use of Fugitive for common Git operations.
§FizzBuzz
The algorithm is simple: for from to :
- If and both divide , print "FizzBuzz".
- Else if divides , print "Fizz".
- Else if divides , print "Buzz".
- Else, print .
§Git
Initialize a new Git repository at ~/fizzbuzz
:
Change directory to this repository:
Then, start vim
(or nvim
).
§Fugitive
To install Fugitive, follow the instructions for your Vim plugin manager on
VimAwesome. To check that it is
properly installed, try to open the documentation with :help fugitive
.
The plugin defines the Ex command :Git [args]
(abbreviated as :G [args]
)
that works almost like git
in a shell. The only difference is that commands
such as :Git diff
or :Git log
are augmented: their output is redirected to
a Fugitive buffer inside Vim that serves as a pager.
In these buffers, Fugitive identifies objects: file names, commit hashes, or diffs. It provides keybindings to act upon them and perform Git operations: staging, diffing, committing, etc. It also provides syntax highlighting for selected output formats.
On the Ex command line, Fugitive extends the revision specifiers defined by Git
in git-rev-parse(1)
. You can use
them with the command :Git
, but also with extra commands such as :Gedit [object]
to edit the specified object in a new buffer.
§Record changes
The summary buffer constitutes Fugitive's main interface, from which you can stage, diff, and commit files to record changes to a Git repository.
§Summary view
Run the command :G
without arguments to access the main summary buffer
(interactive equivalent of git status
):
Note that Fugitive doesn't perform any window management, so you will often end
up with a new split, or it will replace the focused buffer. Such commands in
Fugitive have variations to split vertically, horizontally, or open a new tab.
For the status, you can use Vim's built-in window management commands, like
:only
to hide windows other than the focused one (same as <C-w><C-o>
). You
can chain it after a Git command as :G | only
.
Inside the buffers it manages, Fugitive defines a number of keybindings for
common Git operations. Press g?
to quickly open the documentation at the key
mappings section.
§Track files
Edit a new file with :e main.py
and append the following content:
Save the file with :w
. The summary buffer shows its name under the
"Untracked" section, which means it doesn't belong to the repository yet:
Vim may create an additional main.py.swp
file for recovery purposes. You can
create a gitignore
file to hide these
swap files from Git's untracked files, or configure Vim to save them elsewhere
(see: :h directory
).
Files listed in the summary buffer are an example of Fugitive objects that you
can act upon. Git offers the ability to add untracked files without staging
them. To track the file with Fugitive, position the cursor on main.py
and
press I
(this is equivalent to git add --intent-to-add main.py
):
In the unstaged state, the file belongs to the worktree. The attribute A
indicates a new file. Consult
git-status(1)
for the full list of
attributes.
§Stage files
To stage main.py
, press s
over its file name (equivalent to git add main.py
):
If you stage a file by mistake, you can unstaged it with u
(same as git rm --staged main.py
).
§Commit
After staging changes to the index, you can commit them with cc
(same as git commit
):
While editing a commit message, you can review the diff from the summary view
in the bottom window (or use cvc
, equivalent to git commit --verbose
, to
include the full diff under the commit message for reference). Write and close
the message buffer with :wq
to commit:
There are other useful keybindings:
ca
to amend the last commit.cw
to reword the last commit.cf
to create a fixup commit.crc
to revert the commit under the cursor.
Press c?
to view the full list.
§Advanced staging
Up until this point, all the operations performed by Fugitive were available from the Git CLI. Now you will see how to make arbitrary edits of the index to partially stage complex changes.
§Create a new branch
First, create a feature branch named fizzbuzz
with :G switch -c fizzbuzz
:
Then, update main.py
as follows and save these changes:
The idea for the next two sections is to split this change into a commit that adds the code to print "Fizz", and another one for "Buzz".
§Inline diffs
Place the cursor on main.py
and press >
to display the inline diff:
You can close inline diffs with <
, and toggle them with =
. Fugitive defines
additional keybindings that make reviewing easier. For instance, ]c
and [c
,
that also work in regular diff sessions, jump to the next or previous hunk,
automatically expanding and closing the inline diffs. These mappings also work
on the section titles, but they apply to all the files under them. See :help fugitive-navigation-maps
.
For unstaged files, the inline diffs always show the changes that haven't been recorded to the repository yet, between:
- The worktree and the HEAD if the file isn't in the index (same as
git diff HEAD -- main.py
). - The worktree and the index otherwise (same as
git diff -- main.py
).
Besides files, Fugitive can directly stage or unstage any hunk or line of an
inline diff with s
or u
. These action apply to the hunk under the cursor
(same as selecting a hunk in git add --patch main.py
or git reset --patch main.py
), but you can also target individual lines.
Try to prepare a commit only for the code that prints "Fizz" by selecting the
lines 9 to 11 with V
:
Then, press s
to stage them:
Repeat the operation with lines 13 and 14 to get the following staged changes:
Note that inline diffs for staged files show the changes between the index and
the HEAD (like git diff --staged main.py
). These changes are the ones that
get recorded to the repository after a commit.
§Split changes
To show how to edit the staged version, let's try to add "Buzz" before "Fizz" instead. To that end:
- Unstage
main.py
usingu
. - Select the deletion of
print(i)
on line 9 withV
, and stage this change withs
. - Select lines 11-14 with
V
, and stage these changes withs
.
You should get the following result:
Notice that the conditional inside the loop in the staged version starts with
elif
because the code in our worktree contains the branch to print "Fizz". As
changes accumulate in the worktree, it will become more and more difficult to
commit them independently (you can think of splitting these changes as the
inverse of merging them).
Fortunately, one of Fugitive's killer features is being able to edit the
content of the index directly. When dealing with lines where an inline diff
isn't enough, you can open a full vertical diff between the worktree and the
index by pressing dv
over a file under the "Unstaged" section:
The diff compares the following content:
- On the left, the version in the index.
- On the right, the version in the worktree.
Note that if you used dv
on a file under the "Staged" section, you would get
a diff between the index and the latest commit, same as the associated inline
diff.
To fix the staged version, replace the first elif
by if
in the left window:
Then, save your changes with :w
and return to the summary to see the updated
diffs:
You can now commit the staged changes with cc
:
The previous commit recorded the addition of the conditional branch to print "Buzz", so now you can record the changes to print "Fizz" in a new commit.
First, stage all the remaining changes:
Then, commit them:
§Review the commit history
One of the fundamental Git operations is inspection of the commit history.
§Push to a remote
To demonstrate how Fugitive handles unpushed commits, create a bare repository that will serve as a "remote":
Then, define it as the origin with :G remote add origin ~/fizzbuzz.git
.
Finally, switch back to the master branch with :G switch master
, and push
your changes to this remote with :G push -u origin master
.
§Unpushed commits
In main.py
, add the if
branch that prints "FizzBuzz":
Stage and commit these changes:
Now that the remote tracking branch is set, the summary shows a list of unpushed commits:
These commits are another example of Fugitive objects that you can interact
with. Try to open a commit in a new tab with O
:
This view shows the output of the command git show -p --format=raw 0f71f9e
,
but it is interactive: you can press O
on the tree, on the parent, on the
changed files (to see the previous version on ---
or the current on +++
),
and on the hunks.
The summary only shows local commits that haven't been pushed to a remote. This is convenient because you can safely modify them, without the risk of upsetting your coworkers with a forced push.
§Commit log
If you want to see someone else's work, or view the history for another branch, you need to open the full commit log. For that, Fugitive provides two commands.
The first one is :G log
, equivalent to git log
, except that Fugitive
captures the output into a buffer:
You can jump between commits with [[
and ]]
, open the commit under the
cursor with o
, and use any Vim commands (like /
to search for a word).
Additionally, this command accepts the same arguments as git log
, for
instance, you can use :G log --oneline --decorate --graph --all
for a fancy
output:
Unfortunately, this command outlines a limitation of Fugitive: it doesn't support syntax highlighting for arbitrary Git commands, although you can act on the recognized objects identifiers.
As an alternative, Fugitive has :Gclog[!]
to load the history into the
quickfix list (when !
is given, it doesn't jump to the first entry upon
invocation):
If you have practical Vim
keybindings, you should be able to navigate the quickfix list with ]q
and
[q
. The analogous command :Gllog
uses the location list instead.
§Going further
If getting a colored output is more important than being able to interact with
the buffer, you can use the built-in terminal to run a regular Git command, for
instance, :term git log --oneline --graph --all
:
See Fugitive-compatible plugins such as gv.vim for a better integration of the Git log into Vim.
§Merge conflicts
This section shows how to merge the branch fizzbuzz
with master
and resolve
the conflicts using Fugitive.
§Unmerged files
On branch master
, run :G merge fizzbuzz
. The summary shows a conflict:
main.py
has the status U
under both the "Unstaged" and "Staged" sections
which means "Unmerged, both modified". "Both" alludes to the two ancestor
branches, master
and fizzbuzz
, that both modified the file in a conflicting
way. Indeed, the inline diff shows that it contains conflict markers with two
sections, corresponding to the versions on these two branches.
Note that under "Staged", main.py
also appears without any inline diff, that
would compare the HEAD to the index. But the file isn't actually staged, this
is only a reminder that you cannot commit other staged changes without
resolving this conflict first.
§2-way diff
Pressing dv
on an unstaged file opens a 2-way diff to solve the conflict:
This time, there are 2 ancestors for a total of 3 versions:
- Left: "ours", which corresponds to the HEAD (tip of the branch
master
). - Center: the current working copy.
- Right: "theirs", the tip of the branch
fizzbuzz
you are trying to merge withmaster
.
Conveniently, the conflict resolution markers point to the correct side. Here, you have to merge the two sides by hand:
If you use :w
to save the changes, the conflict is not marked as resolved.
You need to either stage the file with s
from the summary, or use :Gwrite
to save and stage the file with a single command.
Often, one of the two ancestors contains the exact changes you want to merge.
Move to its side with <C-w>h
or <C-w>l
and run :Gwrite!
to overwrite and
stage the working copy (like git checkout --ours -- main.py
or with git checkout --ours -- main.py
).
Finally, note the components //2
and //3
in the file paths. With d2o
and
d3o
, you can obtain a hunk from either sides (like :diffget //2
or
:diffget //3
). You can do the same from the summary view with the commands
2X
and 3X
, applied to a hunk, or even to an entire file.
§3-way diff
More complex merges often call for a 3-way merge to also have the common
ancestor of the two branches in sight. With git checkout --conflict=diff3 -- main.py
, you can update an unmerged file with the content of the ancestor,
also called "base", added between the conflict markers. The interactive
equivalent with Fugitive is to open main.py
and run :Gdiffsplit :1 | Gvdiffsplit!
:
This command chains two subcommands:
:Gdiffsplit :1
starts a diff between the current file (main.py
) and the object:1
. The documentation states that:1:%
corresponds to the current file's common ancestor during a conflict (with%
indicating the current file, which the default when omitted).:Gvdiffsplit!
starts a vertical diff between the current file and all its direct ancestors (Fugitive understands that the diff concernsmain.py
, even though after the previous command, the active buffer is the base version).
The windows are organized as follows:
- Top-left: "ours" corresponding to the HEAD.
- Top-center: "base" corresponding to the common ancestor of
master
andfizzbuzz
. - Top-right: "theirs" corresponding to the tip of
fizzbuzz
. - Bottom: the working copy.
This merging strategy is overkill in many situations, but sometimes you have to reference all these versions while you edit the merged copy.
You can now merge the changes as in the previous section and commit them.
§Rebase
Instead of merging the previous changes, another strategy is to rebase the
branch fizzbuzz
onto master
, and then perform a fast-forward merge to keep
the history linear.
§Stash
First, reset the HEAD
of master
to HEAD^
with :G reset HEAD^
. This
command rewinds master
to the commit before the previous merge, keeping the
working directory as-is with the latest version of main.py
(you could use the
option --hard
, but this is the occasion to handle a dirty working directory).
The stash is useful when you want to perform a rebase with uncommitted changes,
because this operation requires a clean worktree. Fugitive provides a number of
keybindings to push to, and pop from, the stash. The first one is czz
to
stash modified worktree files and excluding untracked or ignored files
(equivalent to git stash
).
To get the changes back, you can use czp
to pop the changes without trying to
restore the index (equivalent to git stash pop
). There are variants that also
restore the index (czP
) or apply the changes without actually deleting the
stash entry (cza
).
§Rebase conflict
Ensure the working directory is clean by using czz
or :G reset --hard
(discarding any worktree changes). Then, start the interactive rebase of
fizzbuzz
onto master
with :G rebase master fizzbuzz
. As with the previous
merge, there is a conflict:
Note that the summary shows the rebase todo, that you can edit with re
(same
as git rebase --edit-todo
, except the commits are listed in reverse order).
Use dv
on main.py
to open a 2-way diff:
"Ours" on the left corresponds to the last commit on the rebased branch that
you are building (initially, this is the root commit on master
), whereas
"theirs" on the right corresponds to the last commit picked from fizzbuzz
.
Resolve the conflict as you did previously and save with :Gwrite
:
With your changes staged, you can continue the rebase with rr
(same as git rebase --continue
), until the next conflict:
Since this conflict is simple enough, there is no need for a diff. Just open
the file in a new tab by pressing O
:
Make the appropriate modifications and save it with :Gwq
(shortcut for
:Gwrite | :quit
):
Finally, complete the rebase with rr
and unstash the changes you made
previously with czp
, if any.
§Keybindings
Fugitive defines helpful keybindings for starting a rebase. Here are some examples:
- Rebase from the ancestor of the commit under the cursor:
ri
: interactive rebase.rf
: autosquash rebase (automatically meld fixup and squash commits created withcf
andcs
).
ru
: rebase against the upstream branch.- Modify the commit under the cursor:
rw
: reword the message.rm
: modify the commit.
- During a rebase:
rr
: continue.ra
: abort.rs
: skip.re
: edit the todo.
§Advanced merge
Real-world merges typically contain many conflicting files. Fugitive provides
the command :G mergetool
, similar to git mergetool
, to iterate on these
conflicts.
§Preparation
Create a new Git repository and commit a few files a.py
, b.py
, and c.py
,
based on the following template:
On a new branch named feat
, commit the following changes:
- In
a.py
, replaceprint('a')
withprint('A')
. - Remove
b.py
. - In
c.py
, replaceprint('c')
withprint('C')
.
Then, switch back to master
, and commit these changes:
- In
a.py
, replaceprint('a')
withprint('aa')
. - In
b.py
, replaceprint('b')
withprint('bb')
. - In
c.py
, replaceprint('c')
withprint('cc')
.
§With the CLI
Run git merge feat
to merge feat
with master
:
The output shows two content conflicts, and one modify/delete conflict that Git
cannot resolve automatically. To iterate on these files, you can run git mergetool
with the option -t
to choose a diff tool:
vimdiff1
ornvimdiff1
for a simple local/remote diff.vimdiff2
ornvimdiff2
for a 2-way merge.vimdiff
,nvimdiff
,vimdiff3
, ornvimdiff3
for a 3-way merge.
Content conflicts open the diff tool, whereas modify/delete conflicts only ask whether to keep the modified version or not.
Note that after normally closing the merge tool, Git marks the conflict for
a.py
as solved by staging the file. Instead, if you want to discard the
changes you've made, use :cquit
to exit Vim and return the status code 1 to
Git.
To take advantage of Fugitive, including the diff keymappings d2o
and d3o
,
you can configure a custom merge tool. The following commands register the diff
tool fugitive3
for 3-way diffs using Fugitive:
The main drawback of git mergetool
is that it requires closing the editor,
because even if you run it in another terminal, Vim will prevent you from
editing files that are already open in the other instance.
§With Fugitive
Let's try to do the same with Fugitive and stay inside Vim. First, run :G reset --hard
to discard any changes in the working directory, and initiate the
merge again with :G merge feat
:
From the summary, you can already solve the conflict for b.py
, modified on
master
, and deleted on feat
:
- If you want to delete this file for the merge commit, then stage the deletion
with
s
. - If you want to keep the version in
master
, then unstage the deletion withu
.
You can manually iterate on the remaining conflicts from the summary view, but
Fugitive provides :G mergetool
to make this task easier. This command loads
the unmerged files into the quickfix list. After merging the conflicting hunks,
you can stage the result with :Gwrite
, and switch to the next item in the
quickfix list with :cnext
(or ]q
if mapped).
For more complicated merges, you will likely want the same 2 or 3-way merge
view as with :Gvdiffsplit
. The main issue with :Git mergetool
is that it
doesn't manage Vim windows.
If you switch to the next quickfix entry, Vim merely replaces the buffer of the
focused window, while the others remain unchanged, eventually with the leftover
content from a previous diff.
Here's a workflow that circumvents this limitation:
- Run
:G mergetool
. - (If you need a diff view, run
:Gvdiffsplit!
or any other variant.) - Resolve the conflicts, then write and stage the result with
:Gwrite
. - (Close other windows, if any, with
<C-w><C-o>
.) - Go to the next quickfix entry with
]q
or:cnext
. - If there are remaining conflicts, go back to 2.
§Conclusion
Fugitive is an invaluable tool to work with Git. It provides easy access to common CVS features, but also partial staging operations that would be quite inefficient with Git's command-line interface. Other features not covered in this article include:
:Git blame
to get the latest change for each line on the current file in a separate buffer that defines keymaps of its own (:h :Git_blame
).:GMove
to move a file likegit mv
, and other commands of the like (:h fugitive-commands
for the full list).
Hopefully, you now know enough to explore these features on your own to work with Git inside Vim efficiently.
Thanks to Keith Edmunds for pointing out imprecisions and mismatches between the terminal screenshots and the surrounding instructions.