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
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.
- Record changes
- Advanced staging
- Review the commit history
- Merge conflicts
- Advanced merge
This article iterates on a FizzBuzz implementation in Python to demonstrate the use of Fugitive for common Git operations.
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 .
Initialize a new Git repository at
Change directory to this repository:
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
The plugin defines the Ex command
:Git [args] (abbreviated as
that works almost like
git in a shell. The only difference is that commands
: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
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.
The summary buffer constitutes Fugitive's main interface, from which you can stage, diff, and commit files to record changes to a Git repository.
Run the command
:G without arguments to access the main summary buffer
(interactive equivalent of
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
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
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
gitignore file to hide these
swap files from Git's untracked files, or configure Vim to save them elsewhere
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
I (this is equivalent to
git add --intent-to-add main.py):
In the unstaged state, the file belongs to the worktree. The attribute
indicates a new file. Consult
git-status(1) for the full list of
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).
After staging changes to the index, you can commit them with
cc (same as
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:
cato amend the last commit.
cwto reword the last commit.
cfto create a fixup commit.
crcto revert the commit under the cursor.
c? to view the full list.
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
:G switch -c fizzbuzz:
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".
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,
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
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
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
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.
To show how to edit the staged version, let's try to add "Buzz" before "Fizz" instead. To that end:
- Select the deletion of
print(i)on line 9 with
V, and stage this change with
- Select lines 11-14 with
V, and stage these changes with
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
To fix the staged version, replace the first
if in the left window:
Then, save your changes with
:w and return to the summary to see the updated
You can now commit the staged changes with
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.
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
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.
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
]], open the commit under the
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
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
If you have practical Vim
keybindings, you should be able to navigate the quickfix list with
[q. The analogous command
:Gllog uses the location list instead.
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
:term git log --oneline --graph --all:
See Fugitive-compatible plugins such as gv.vim for a better integration of the Git log into Vim.
This section shows how to merge the branch
master and resolve
the conflicts using Fugitive.
: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
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.
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
- Center: the current working copy.
- Right: "theirs", the tip of the branch
fizzbuzzyou are trying to merge with
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
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>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
//3 in the file paths. With
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
3X, applied to a hunk, or even to an entire file.
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 :1starts 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 concerns
main.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
- Top-right: "theirs" corresponding to the tip of
- 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.
Instead of merging the previous changes, another strategy is to rebase the
master, and then perform a fast-forward merge to keep
the history linear.
First, reset the
:G reset HEAD^. This
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
--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
stash modified worktree files and excluding untracked or ignored files
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 (
Ensure the working directory is clean by using
:G reset --hard
(discarding any worktree changes). Then, start the interactive rebase of
: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
git rebase --edit-todo, except the commits are listed in reverse order).
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
"theirs" on the right corresponds to the last commit picked from
Resolve the conflict as you did previously and save with
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
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
czp, if any.
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 with
ru: rebase against the upstream branch.
- Modify the commit under the cursor:
rw: reword the message.
rm: modify the commit.
- During a rebase:
re: edit the todo.
Real-world merges typically contain many conflicting files. Fugitive provides
:G mergetool, similar to
git mergetool, to iterate on these
Create a new Git repository and commit a few files
based on the following template:
On a new branch named
feat, commit the following changes:
Then, switch back to
master, and commit these changes:
§With the CLI
git merge feat to merge
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:
nvimdiff1for a simple local/remote diff.
nvimdiff2for a 2-way merge.
nvimdiff3for 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
To take advantage of Fugitive, including the diff keymappings
you can configure a custom merge tool. The following commands register the diff
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.
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
- If you want to delete this file for the merge commit, then stage the deletion
- If you want to keep the version in
master, then unstage the deletion with
You can manually iterate on the remaining conflicts from the summary view, but
: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
]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:
- (If you need a diff view, run
:Gvdiffsplit!or any other variant.)
- Resolve the conflicts, then write and stage the result with
- (Close other windows, if any, with
- Go to the next quickfix entry with
- If there are remaining conflicts, go back to 2.
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 blameto get the latest change for each line on the current file in a separate buffer that defines keymaps of its own (
:GMoveto move a file like
git mv, and other commands of the like (
:h fugitive-commandsfor 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.