return {
"lewis6991/gitsigns.nvim",
keys = {
{
"<leader>hb",
"<cmd>Gitsigns blame_line<cr>",
desc = "Blame Line"
},
{
"<leader>hs",
"<cmd>Gitsigns stage_hunk<cr>",
desc = "Stage Hunk"
},
{
"<leader>hS",
"<cmd>Gitsigns stage_buffer<cr>",
desc = "Stage Buffer"
},
{
"<leader>hr",
"<cmd>Gitsigns reset_hunk<cr>",
desc = "Reset Hunk"
},
{
"<leader>hR",
"<cmd>Gitsigns reset_buffer<cr>",
desc = "Reset Buffer"
},
{
"<leader>hu",
"<cmd>Gitsigns undo_stage_hunk<cr>",
desc = "Undo Stage Hunk"
},
},
}
Chapter 15. Source Control
LazyVim ships with several features to manage your source control history, and there are some excellent third-party plugins you can use as well. Some of these plugins work with multiple version control systems, while others are git-centric. This book will assume you use git because, well, you probably do, even if you use other systems as well.
15.1. The Integrated Terminal (A Rant)
For reasons I cannot explain, Neovim ships with a terminal emulator. It is bizarre to me that an editor that runs in a terminal ships a terminal. It is literally possible to open a terminal, open Neovim, open a terminal in Neovim, and open Neovim in a terminal in Neovim.
Add some nested ssh sessions if you really want to make a mess.
I don’t need a terminal in my editor. I have a terminal already, an excellent one. I just use Kitty splits, tabs, and windows when I need a new terminal window. The smart-splits plugin allows me to switch between editor and terminal seamlessly and Kitty even manages installing itself over ssh for me.
Or I press Control-z
, which is the traditional way Vim users used to access a
terminal. It is a shortcut that I really wish hadn’t gone out of style.
Pressing Control-z
“suspends” Neovim. If you’re not in the know, you’ll think it
closed your editor without saving, because the window disappears and returns
you to your terminal.
But fear not! It is merely suspended, as indicated by the 'nvim' has stopped
message in the output:
As this screenshot also shows, you can see the list of stopped (or running)
background jobs using the jobs
command in any shell. The fg
(short for
foreground) command starts the suspended Neovim process back up. If you have
multiple suspended jobs, the fg %#
command can be used to choose a specific
job id (e.g. fg %1
will run the job with id 1
in the first column of the
jobs
output).
This is not a Neovim-specific feature. The Control-z
trick works with (almost)
any long-running shell command. You can even set a suspended task to keep
running in the background by using the bg
command instead of fg
(though if
the background job prints to stdout you’ll quickly become confused).
Between terminal splits and Control-z
, there’s just no need for the editor to
have its own terminal embedded with it. Still, Neovim ships with an integrated
terminal, so I should probably explain how to use it.
15.2. The Integrated Terminal (For Real This Time)
You can pop up a terminal at any time in Lazyvim using the keybinding Control-/
.
It will appear in front of all your other editor windows (unless you have
the Edgy extra enabled, in which case it will show up in the bottom half of the
editor) and can be dismissed with Control-/
again.
Neovim’s terminal window is a super weird hybrid terminal and Vim window. Once the terminal is open, you can use Normal mode commands to navigate around it.
However, unlike Insert mode, the Escape
key WILL NOT put you in Normal mode,
even though your fingers are, by now, conditioned to hit Escape
reflexively.
This actually makes sense because Escape
is a common key to need to type in
various terminal programs, so it would be rude for Neovim to steal it. LazyVim
has set up the keybinding <Escape><Escape>
(press Escape
twice quickly) to
switch to normal mode from terminal mode, or you can use the hard-to-type
default incantation <Control-\><Control-n>
.
Once in Normal mode, you can navigate anywhere in the Terminal window using any of the navigation keys including Seek and Search modes. This can occasionally be helpful if you need to yank some outputted text to the clipboard.
Pressing a key such as a
or i
will send you back to “Terminal mode” which
effectively just sends every keystroke to the program currently running in the
terminal (probably your shell).
Annoyingly, this means you can’t use Normal mode to reposition your cursor on the command line; it will go back to wherever it was when you last entered normal mode.
If you want to use Vim Normal modes to edit your command line (in any
terminal; not just inside Neovim) configure your shell to use “Vi mode.” All
modern shells support some version of this, and it usually allows you to use
Escape to put the shell in a pseudo-normal mode. It gives you commands like
w and b for navigation and basic line-editing commands like d and c
to edit the command line. |
There are third-party plugins that try to make the terminal experience more
consistent and enjoyable, but in my opinion, they are not worth the trouble. I
can just press cmd-enter
to get a new Kitty terminal pane and have a
perfectly normal terminal experience.
15.3. Checking Your Git Status
Lazyvim is preconfigured with a handful of carefully configured plugins that make your version control life much better.
The simplest of these uses your configured file picker (Telescope or Fzf.lua, as discussed in Chapter 4) to list files that have changed since the last commit. This will behave similarly to other file picker operations, except it only lists files that have modifications in git.
You can open it with <Space>gs
. I use it a lot for switching between files
related to whatever feature I am currently working on, and actually prefer
it to the buffer picker (which only shows opened files) we discussed in
Chapter 9.
The popup behaves slightly differently depending on whether you use Telescope or Fzf.lua. I’ll explain with Telescope first, and mention how Fzf.lua is different afterwards. (Please, can we just collectively settle on one best picker and use it?)
15.3.1. With Telescope
This Telescope screenshot shows that I have modified two files since my last commit:
The preview pane shows the diff of lines I have added and removed. On the left,
you can see that I have page.svx
focused, and a preview of some of the changes
in this file on the right.
The confusing bit to pay attention to is the first two columns. They indicate your git status, and their meaning can be devilishly hard to remember. The symbols themselves are straightforward:
~
means the file on that line contains modifications since the last commit-
means it has been deleted?
means it is an untracked file (has been added to the working directory but not staged or committed)+
means it is a new file that has been staged in git
If the sign shows up in the first column, it means the file has been staged
and will be included in the next commit. If it is in the second column, then it
means the file is not yet staged. If a ~
is in both columns, some parts of it
have been staged and some parts have not.
I had to use this picker quite a few times before I could remember which of column 1 or column 2 means “staged”. Worse, if all or none of the files are staged it can be hard to tell which column is empty.
In addition to allowing you to effectively view your git status, this picker
also allows you to stage entire files. To do so, focus a file and hit the
<Tab>
key. If it is staged it will become unstaged and vice versa, moving
the symbol between the first and second columns.
15.3.2. With Fzf.lua
Fzf.lua behaves similarly, but not identically to Telescope. If you’ve
installed the Fzf.lua extra, the same keybinding (<Space>gs
) pops up an Fzf.lua
window instead :
Ensure the delta-pager CLI tool is installed to get the pretty diffs. |
The main difference is that you use the left and right arrow keys to stage or
unstage a file instead of Tab
, and you can additionally use the Control-x
keybinding to reset an entire file to the last commit state. Unlike Telescope,
these keybindings are helpfully written across the top of the picker so you
don’t have to memorize them.
The two columns are labelled +
and -
. I’m not sure why those symbols were
chosen, as they don’t reflect whether files or lines are added or deleted. The
+
column holds files that have been staged to go in the next commit, while
the -
column holds a status for files that have changed but have not yet been
staged. This is the same as Telescope, but it’s a little bit clearer which is
which with the heading symbols on there.
15.3.3. Other Pickers
Telescope and Fzf.lua both come with pickers to view and search commit history
(<Space>gc
), kind of like a log browser, and to check out a branch. The
latter doesn’t have a keybinding for some reason, but you can bind one to
:Telescope git_branches
or :FzfLua git_branches
if you like the picker UI
for this task.
There are a variety of less commonly-used git-related pickers you can find by
typing either :FzfLua
or :Telescope
and then Enter
. This shows a list
of all installed pickers. Type git
to filter down to the git specific ones.
15.4. Git Files in Neo-tree
Neo-tree also has a Git status viewer. You can open it with <Space>ge
, where
e
means “explore”. It has the advantage of displaying any changed files
inside a folder hierarchy. Here are the same two files from the previous
example as rendered in Neo-tree:
To stage and unstage a file with Neo-tree, use ga
(git add) and gu
(git
unstage) while your cursor is over that line. The A
keybinding will
stage all unstaged files.
You can also use gc
to commit the current state. This pops up a crappy little
text entry window that is absolutely not suitable for typing a proper-length
commit message, so I suggest avoiding it.
Use gp
to push the current branch to the remote repository. I recommend using
the lazygit
integration discussed later instead, but these commands are
available if you spend a lot of time in Neo-tree.
15.5. Status of the Currently Focused File
Every buffer has a couple subtle indications of the changes in that file. Consider this screenshot:
Notice the left sidebar, to the right of the line numbers. It contains a green bar, a small red triangle, and a short orange bar. These indicators show that lines have been added, removed, and modified, respectively.
Additionally, in the status bar, just to the left of the file progress indicator we see these icons, which summarize the same information:
15.6. Staging From the Editor
You can add files to git’s index (so they are ready to commit) right from the
editor. The <Space>gh
menu (mnemonic is “git hunks”) has a bunch of
interesting subcommands:
You can use <Space>ghS
to stage an entire file, which would move it to the
left column in the git status pickers we discussed above. If you want to stage
a patch containing a subset of your changes, navigate to the hunk you want to stage
([h
and ]h
are super handy for this) and hit <Space>ghs
.
Most people have an unfortunate habit of just committing everything instead of
properly curating their history, but if you are one of the rare folks who uses
git properly (please be that person), you’ll use the <Space>ghs command a lot. |
You can also reset a hunk (effectively making it the same as it was at the time
the last commit was made) using <Space>ghr
. If you want to reset the entire
file, use the “but bigger” <Space>ghR
. Resetting is a destructive operation, so
be careful (though u
for undo can usually get you back to where you were).
If you accidentally stage a hunk, use <Space>ghu
to unstage it. Unlike reset,
this won’t change the file; the changes will still be there; they just
won’t be staged anymore.
15.7. Git Information Keybindings
The blame line (<Space>ghb
) command shows the commit that last changed the line the
cursor is currently on, useful for answering the all-important question “Why
on Earth did I do that?”
Preview hunk (<Space>ghp
) temporarily renders the original and changed
version of a hunk (one above the other) so you can see exactly what changed.
The Diff this
(<Space>ghd
and <Space>ghD
)commands do the same except in a
side-by-side view that we will discuss later in this chapter.
Personally, I use many of these commands too often for the number of keystrokes
required to pop them up. So I’ve created an extend-gitsigns.lua
file in my
plugins directory that copies them from <Space>gh
to <Space>h
:
I got these by copying them from the git-signs config on the LazyVim website
and converting from map
calls to the keys =
format.
15.8. Lazygit
Lazygit (which, despite sharing the Lazy
namespace with LazyVim and
Lazy.nvim, is by an entirely different developer) is a terminal UI tool for
interacting with git. It is a separate program that you will need to install
with your operating system’s package manager (e.g. brew install lazygit
) if
you want to use it.
LazyVim is preconfigured to show lazygit in a terminal window using the
keybinding <Space>gg
. I won’t go into all the details of how to use this
third-party program. It can do almost anything git can do in a much more
user-friendly interface.
Lazygit takes a bit of study to get used to, but it has helpful menus and mnemonics for its keybindings so the learning curve is relatively gentle.
Ironically, I used lazygit (in its standalone format from the command line) a
lot more before I started using LazyVim. I used to stage changes using lazygit,
but now I use the <Space>h
menu we just covered instead.
I also now do most of my git work with the exceptional Graphite tool, which simplifies many of the flows I used to use lazygit for (especially rebasing). I still use lazygit every day; I just don’t have it open 100% of the time like I used to.
15.9. Diff Mode
Neovim comes with a powerful, but slightly hard-to-learn diff viewing mode. It shows “before” and “after” files side by side and can even be configured to show the “parent” and changed state if you want a fancy merge tool.
There are several different ways to get yourself into Diff mode. The basic way is to specify it when you open two files on the command line:
nvim -d file1 file2
This opens the indicated files side by side in a linked diff view. Most often,
you won’t have two separate files, though. Instead, you’ll want to see the
difference between the current file and the staging index, which you can do
with the shortcut <Space>ghd
. Or use <Space>ghD
to show the differences
between the current file and the last commit, regardless of what has been
staged.
Once you are done operating in Diff mode, it can be tricky to
get back to the normal file. The issue is that when a file is in diff mode,
it stays that way, even if other windows are opened or closed. The secret is
to use the :diffoff command, which will disable “diff view” for the current
buffer. This doesn’t close the two side-by-side windows, though; you’ll need
to use normal window and buffer management tooling such as <Space>bd and
<Control-w>q to do that. |
Note that by default, the diff view will collapse any code that is identical
between the two files into a single fold. Use the code unfolding command zo
to expand a section.
15.9.1. Editing Diffs
If you use the <Space>ghd
command to show your file in diff view against the
index mode, you can keep editing the file to make additional changes. If you do
this, only edit the file on the right. This is the “working” file. The file
on the left is the “index” file; it shows the staged changes. If you want to
“edit” the file on the left, use the <Space>ghs
, <Space>ghr
, and
<Space>ghu
to stage, reset, and unstage hunks from the right side. It is not
forbidden to edit the index file directly, but it will confuse the Diff mode
machinery, so stick to editing, staging, and unstaging from the right side.
When working with diff view like this, I find that the stage, reset, and
unstage keybindings best match the mental model I am used to. However, there
are two kind of weird commands built into Neovim that you may sometimes need to
reach for: :diffget
and :diffput
. These are more commonly typed as :diffg
and :diffp
to save a couple keystrokes.
These commands are most often used in Visual mode (or with a range), and they essentially mean that (within that range) we should either “make this file the same as the other file” or “make the other file the same as this file”, respectively.
Consider these two files that are slightly different:
The file on the left represents the state of my index, while the file on the right is my working copy. The indexed version was missing the word “Two”, so I have added that on the right. It also had an extra “Four Point Five” line that I have removed on the right. And I modified the spelling of the word “Six”.
Let’s explore a couple ways to make these files identical with :diffg
and
:diffp
. You can use these commands on either file, but it usually makes sense
to operate on only one of them. For this example, assume I’m working on the
right-hand file.
I can use any navigation commands to jump to the second line of the file. If
you are editing a real git indexed file, the [h
and ]h
keybinding are
probably useful for jumping between hunks. However, when you are in “diff” mode
you can also use the [c
and ]c
, which mean “jump between changes,” but
only when you are in “diff” mode. (In a non-diff window, LazyVim has bound
those keys to jump between classes or types.) I usually just use [h
and ]h
,
but in those instances where I am using a diff view that is not attached to
git history, [c
and ]c
should not be forgotten.
So with my cursor on the first line of the file, [c
or [h
will jump to the
second line, which contains the word Two
in my file, but not the index.
I want to stage this change, so I type :diffp
, which means “make the other file
the same as this one.”
The next line is Four Point Five
in the left file, but was deleted in the
right file. For the sake of argument, let’s say I want to “unstage” this
change, which is to say “make the right file the same as the left file”. To do
this from the right window, I can use Shift-V
to enter Visual Line Mode, and
select the lines containing Four
and Five
as well as the blank red space
between those two lines representing the deleted line. Now I can type :diffg
or :diffget
which means “get the contents of the other window and make my
window match it.” Since :diffget
and :diffput
accept ranges, it passes
the visual selection with the usual '<
and '>
marks.
If you find you like the above diff interface, but figuring out which
files have differences is frustrating, you may want to configure the
diffview.nvim plugin. I personally
just use the git status telescope picker, but the diffview.nvim plugin has a
nice interface and some handy commands. |
15.10. Configuring Vim Diff as Merge Tool
Everyone seems to hate resolving merge conflicts. Armed with Diff mode and
rebasing, I actually find the process kind of enjoyable. The trick is to have a
slightly complicated ~/.gitconfig
(and a very large monitor).
I can’t help you with the monitor, but the .gitconfig
needs to look like this:
[diff]
tool = vimdiff
[merge]
tool = vimdiff
conflictstyle = zdiff3
[mergetool "vimdiff"]
cmd = nvim -d $LOCAL $BASE $REMOTE $MERGED \
-c '$wincmd w' -c 'wincmd J'
The zdiff3
conflict style makes diffs a bit easier to read by automatically
resolving identical lines. The two tool =
lines say to use the vimdiff
merge
tool that is configured on the last line.
That last line is a command to open Neovim with a whopping FOUR windows open and focuses the appropriate one.
To demonstrate this, I made a new git repo with two branches with conflicting changes. When I went to rebase (I always use rebase rather than merge commits because it allows me to deal with conflicts in the isolation of one change. This is why it’s important to me that every commit have only one change!), one branch onto the other, of course, I end up with this error:
✦ ❯ git rebase main
Auto-merging file
CONFLICT (content): Merge conflict in file
error: could not apply f611b6f... Uppercase
Could not apply f611b6f... Uppercase
To resolve this conflict, I run git mergetool
. Because of the git configuration
above, it will open Neovim with these four different diff windows:
There are three windows across the top and one in a big pane (also pain) in the bottom.
- Upper-left window
Shows the “local” changes. The meaning of “local” depends on exactly what commands you used to get into the conflict situation. In typical rebase flows, it returns to “the current state of the main branch”. So when there is a conflict, it would contain “the other person’s changes”, so “local” doesn’t seem applicable.
- Middle window
Contains the “common ancestor” or “base” of the changes. Which is to say, this is the state of the file before either you or “the other person” made any changes. This window is not commonly included in merge-tool tutorials, but I find it can be quite helpful when trying to figure out what changed between the base and each of the two side windows.
- Upper-right window
Contains the “Remote” changes, which, like local, can be a misnomer. In rebase flows, it usually means, “the changes I made on the branch I am rebasing onto main.”
- Bottom window
Contains “the current state of the file”, which at the time the rebase failed includes messy conflict markers. This is the only file you should make edits to.
All four files will feature code folding if there are long sections of common code. Also, if you scroll or move the cursor in the lower file, the upper files will also scroll so everything stays in sync, and an underline in the top three windows will indicate which line the diff tool thinks is the “current” one with respect to the cursor position in the lower window.
Most rebase flows start with using vag
and :diffg
from the lower window to
make it identical to one of the upper windows. Then you would use diffget
to
get hunks from the left or right window, depending on context. You’ll also
usually have to do some manual editing.
The problem is, :diffg
doesn’t know which window to get things from because
there are multiple windows open:
Instead, we need to use the command :%diffg 2
. The 2
is a buffer number.
When you run merge-tool directly from the command line, the buffers are
numbered in the order they are open. So 1
is the left-hand buffer, 2
is the
middle one, 3
is the right-hand one, and 4
is the lower window. If you
aren’t sure, you can use the <Space><comma>
keybinding to show the buffer
list:
In this list, the first column holds the buffer number. This number generally
increases monotonically from the most recent time Neovim opened, so it can get
pretty high if you’ve been editing for a while. But when you use git
mergetool
, it typically opens a brand new Neovim instance and 1-4
are
expected.
After running vag
and the :%diffg 2
command, the bottom window looks the
same as the middle window, which is the state everything was before either
branch was created. If I used vag
and then :%diffg 1
it would look the
same as main
, and vag
followed by :%diffg 3
would make it look the
same as my branch. Then I could selectively look at differences between
buffers and use :diffg #
to get changes from the left or right one
respectively.
Merge conflicts can always be somewhat stressful, but I find the four window view often makes it easier to understand what changed and why. That said, I only reach for it when I’m in a particularly knotty merge situation. Normally, I use the git-conflict.nvim plugin.
15.11. Git-conflict.nvim
While merge-tool is very helpful when working with particularly complicated merges, for simple conflicts, I usually find it quicker to just edit the file with the conflict markers in it directly. A plugin called git-conflict.nvim provides syntax highlighting and some keybindings to help navigate conflicts.
Set it up with a config something like this:
return {
"akinsho/git-conflict.nvim",
lazy = false,
opts = {
default_mappings = {
ours = "<leader>ho",
theirs = "<leader>ht",
none = "<leader>h0",
both = "<leader>hb",
next = "]x",
prev = "[x",
},
},
keys = {
{
"<leader>gx",
"<cmd>GitConflictListQf<cr>",
desc = "List Conflicts"
},
{
"<leader>gr",
"<cmd>GitConflictRefresh<cr>",
desc = "Refresh Conflicts"
},
},
}
I use the <Space>h
prefix that I set up previously for staging hunks and add
a few new commands to it. After enabling this extension, if you open a file
with conflicts, it highlights the conflict markers in a different colour.
On my plaintext sample file it looks like this:
The conflict markers include the “current” (whatever is on main) code above, and the “new” (whatever is being rebased) code below, with the original or base code (before either change) in the middle.
I can use the ]x
keybinding to quickly jump to the next conflict (in this
case there is only one). Then I can use one of the following keybindings
to resolve the conflict:
<Space>ho
Choose the top version<Space>ht
Choose the bottom version<Space>hb
Choose both<Space>h0
Go back to whatever is in the middle
The o
and t
keybindings are hard to remember. Technically they mean “ours”
and “theirs”, but depending on which order you did a merge or rebase, it
doesn’t always semantically map to your own or somebody else’s commit. I just
remember that o
is before t
in the alphabet, so it means the upper change.
You could also map them to more mnemonic keybindings if you want.
In all cases, but especially in the latter two, you will likely need to do some manual editing to make the code look correct. This is normal. None of the conflict management extensions uses AI to semantically understand what the changes intended to do, so you still need to do that part yourself!
About ninety percent of the time, this plugin is all I need to resolve a conflict. I only use the mergetool when things are particularly hairy or complicated.
15.12. Summary
This chapter introduced a lot of different ways of interacting with git and version control from inside LazyVim. You probably won’t use all of it, but I wanted to present multiple options so you can decide which ones work best for you.
Perhaps you want to use Lazygit, or maybe you want to stay in the editor and use the functionality that git-signs and native Vim diff mode provide. Maybe you want to install some extra plugins such as git-conflict.nvim or diffview.nvim to streamline your experience (others you might want to look at include Neogit and mini.git).
Or maybe you don’t want to manage this stuff from your editor at all and just
want to drop to Terminal mode and use git
or a wrapper like graphite
. Whatever
works for you, LazyVim provides the integrations you need.
In the next chapter we’ll admit that it’s not 2020 anymore and talk about artificial intelligence.