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 ii from 11 to 2020:
- If 33 and 55 both divide ii, print "FizzBuzz".
- Else if 33 divides ii, print "Fizz".
- Else if 55 divides ii, print "Buzz".
- Else, print ii.
§
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 using u.
- Select the deletion of print(i) on line 9 with V, and stage this change with s.
- Select lines 11-14 with V, and stage these changes with s.
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 with master.
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 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 master and fizzbuzz.
- 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 with cf and cs).
- 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, replace print('a') with print('A').
- Remove b.py.
- In c.py, replace print('c') with print('C').
Then, switch back to master, and commit these changes:
- In a.py, replace print('a') with print('aa').
- In b.py, replace print('b') with print('bb').
- In c.py, replace print('c') with print('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 or nvimdiff1 for a simple local/remote diff.
- vimdiff2 or nvimdiff2 for a 2-way merge.
- vimdiff, nvimdiff, vimdiff3, or nvimdiff3 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 with u.
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 like git 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.