Fugitive.vim - resolving merge conflicts with vimdiff
When git branches are merged, there is always the chance of a conflict arising if a file was modified in both the target and merge branches. You can resolve merge conflicts using a combination of fugitive’s
:Gdiff command, and Vim’s built in
diffput. In this episode, we’ll find out how.
This is the third in a five part series on fugitive.vim.
I’ll be running my Core Vim Class online on Thursday, December 5th. Tickets cost $255, but you can get the earlybird discount of $230 if you buy yours before November 29th. The price includes an exclusive screencast that summarises the material from the class.
:Gdiff on a conflicted file opens 3-way diff
When you run
:Gdiff on a conflicted file, fugitive opens 3 split windows. They always appear in this order:
- the left window contains the version from the target branch
- the middle window contains the working copy of the file, complete with conflict markers
- the right window contains the version from the merge branch
I discuss target and merge branches a lot in the screencast, so lets just make sure that we’re on the same page. The ‘target’ branch is the one that is active when you run git merge. Or in other words, it’s the HEAD branch. The ‘merge’ branch is the one that is named in the
git merge command. In this scenario the ‘master’ branch is the target, and the ‘feature’ branch is merged into target, making it the merge branch.
Strategies for reconciling 3-way diffs
There are two basic strategies for reconciling a 3-way diff. You can either keep your cursor in the middle file, and run
:diffget with the bufspec for the file containing the change you want to keep. Or you can position your cursor on the change that you want to keep, and run
:diffput with the bufspec for the working copy file. We’ll take a look at each of these strategies in turn, starting with diffget.
In the context of a 2-way diff, the
:diffput commands are unambiguous. If you ask Vim to get the diff from the other window, there is only one place for it to look. When you do a 3-way merge, things get a little more complex. This time, it would be ambiguous if you were to tell Vim to fetch the changes from the other window. You have to specify which buffer to fetch the changes from by providing a
The buffspec could either be the buffer number, or a partial match for the buffer’s name. Buffer numbers are assigned sequentially, so they will differ from session to session, but you can always be sure that they will uniquely identify their buffer.
Fugitive follows a consistent naming convention when creating buffers for the target and merge versions of a conflicted file. The parent file from the target branch always includes the string
//2, while the parent from the merge branch always contains
//3. These partial matches are sufficient to uniquely identify the target and merge parents when using the
Resolving a 3-way diff with
:diffget command modifies the current buffer by pulling a change over from one of the other buffers. In resolving a merge conflict, we want to treat target and merge parents as reference copies, pulling hunks of changes from those into the conflicted working copy. That means that we want to keep the middle buffer active, and run
diffget with a reference to the buffer containing the change that we want to use.
:diffget //2– fetches the hunk from the target parent (on the left)
:diffget //3– fetches the hunk from the merge parent (on the right)
Note that Vim does not automatically recalculate the diff colors after you run
:diffget. You can tell Vim to do this by running
Resolving a 3-way diff with
:diffput command modifies another buffer by pushing a change from the active buffer into it. In the context of a 3-way merge conflict, we want to push changes from the target and merge versions into the working copy.
The example in the video used a file called
demo.js, which could be referenced using the buffspec ‘demo’. In this case, we could run the exact same command each time:
:diffput demo– pushes the hunk from the active buffer into the conflicted working copy
Although the command is kept constant, we have to activate the correct window before running it. Whereas using
diffget, the window remained constant but we had to pass a different argument each time.
In a 2-way diff, the diffget and diffput commands require no argument. Vim provides a couple of convenient shorthand mappings for these commands:
do performs a
diffput. These mappings don’t normally work in a 3-way diff, because the
diffput commands both require an argument in this context. But in the case of the
diffput command, it’s pretty easy to guess what that argument is going to be.
When you do a 3-way diff between working copy, target and merge parents, fugitive assumes that if you run
dp from either of the parent buffers, you want to put the change into the working copy. So even though the
dp mapping normally only works in a 2-way diff, you can use it in this special case of a 3-way diff.
Keeping one parent version in its entirety
In reality, it’s often the case that one of the parent versions is to be kept wholesale, and the other version is to be discarded. In this scenario, fugitive’s
:Gwrite command comes in handy. This overwrites the working tree and index copies with the contents of the currently active file.
If you run
:Gwrite from the target or merge version of a file, fugitive raises a warning. This is to protect you from accidentally overwriting the working copy and index files when you’ve carefully cherry picked the changes from the parent versions. If you want to stage either of the parent versions in their entirety, use
:Gwrite! to show you really mean it.
This table summarizes some of the commands used in the video:
||jump to previous hunk|
||jump to next hunk|
||shorthand for `:diffput`|
||close all windows apart from the current one|
||write the current file to the index|
dp command normally only works in a two-way diff, as does
do: the shorthand for
To leave vimdiff mode, you just need to close the windows that are being compared. The quickest way to do this is to run
:only from the window that you want to keep open.
When you call
:Gwrite from vimdiff mode, it writes the current file to the index and exits vimdiff mode.