Vim: Git integration

With vim-fugitive you can perform almost any Git command from within Vim with :Git. (realizing full-well that you can shell out to Git with :!git and :read !git) The Fugitive is nicer, and turns commits into hyperlinks (it's more intuitive to browse interactively).

Staging and committing

Navigating hunks

GitGutter is a Vim plugin that provides markers in the sign column that marks modified lines. It also provides hunk text objects ic and ac for operating on hunks.

Use ]c and [c for jumping between hunks, or use :GitGutterQuickFix for loading all hunks into the quickfix list. Hunks can be previewed, staged, or undone with <leader>hp, <leader>hs, or <leader>hu respectively.

You can also use :Git add --patch from Fugitive to stage hunks interactively from within Vim.

Committing

You can use :Git commit from within Vim, which will open a new window. I don't normally do this, because I usually keep one or more terminal panes set aside for Git.

I think everyone should set the commit.verbose config option, which adds the staged diff to the commit editor. This can really slow down vim for large commits (but you shouldn't have large commits in the first place!) If you really need to disable showing this diff in the commit editor, you can commit with git -c commit.verbose=false commit.

I highly recommend making commits that touch submodules do nothing but touch the submodule. In the case of adding a new submodule, it should be

git submodule add $REMOTE_URL
git commit

Updating a submodule to a new hash should also be done in its own commit. I do make the concession, however, that this is a questionable practice if you value independently buildable commits (as in, each commit builds and passes the pipeline). Use your best judgment, based on your team's Git values (hopefully they do have values).

# Update the submodule locally
cd $SUBMODULE_PATH
git switch $GIT_BRANCH
git pull
cd -
# Commit the submodule update by itself
git add $SUBMODULE_PATH
git commit

When committing a submodule update like this (as opposed to rewording a commit that updates a submodule as would be common in an interactive rebase) I believe you should always add the submodule changes to the commit for traceability. This can be done from within Vim by

:read !git diff --submodule --staged $SUBMODULE_PATH

Technically, you can leave off the $SUBMODULE_PATH if you followed my advice that changes to submodule should be committed by themselves.

TODO: I haven't figured out the right invocation to :read !git when squashing two submodule update commits during an interactive rebase.

You can use gq to reflow the text in a commit message. Use visual-line mode to select text to reflow or gqip to reflow the current paragraph.

Git archaeology

Git log

Fugitive provides :Git log (which can accept any of the usual commandline arguments). Pressing <CR> on any of the commit hashes will open that commit in a new buffer (a fancier git-show).

fzf.vim provides :Commits and :BCommits that opens an interactive commit browser that you can search. Again, pressing <CR> on a selected commit (or multiple commits, selected with TAB in their own buffers.

TODO: I want to make this use the same smarts as git-gl because then I can trigger special sauce on keybinds. See https://github.com/Notgnoshi/dotfiles/issues/13.

:BCommits is similar to running :Git log %. Additionally, you can make a visual selection. Then, running :BCommits shows commits that touched that range of lines. This is an easier-to-browse :Git blame.

Git blame

Fugitive provides :Git blame which nicely wraps git-blame in a scroll-synchronized window opened to the side of the current buffer. Pressing <CR> on the selected commit will open that commit in a new buffer. Pressing - will reblame at the selected commit.

When you open a commit with :Git blame or similar, you'll see something like

tree d80fb1c28a89bcdf6798dd26f986e1745e207525
parent 4e4ea6709c08807b1d769e89a582720d4e11ebfb
author Austin Gill <Notgnoshi@gmail.com> Sun Jul 25 13:46:12 2021 -0500
committer Austin Gill <Notgnoshi@gmail.com> Mon Aug 30 14:50:15 2021 -0500

which, notably, doesn't include the hash of the displayed commit. To get the hash, do y<C-G> to yank the hash. Or, possibly more helpful, as long as you have system clipboard support, you can do "+y<C-G>.

Misc

Use :Gedit master:% to open the current file as of the master revision in a new buffer. You can do the same with :Git diff master:% % to the current file against master.

Merge conflict resolution

TODO: This is something I still have to learn. I typically do git status followed by git diff $FILE_WITH_CONFLICTS in one terminal pane, and resolve the conflicts manually with Vim in another pane. This isn't a terrible experience, but I bet it could be better.