How to Resolve Git Rebase and Merge Conflicts with Vim
Step-by-step guide to resolving git rebase and merge conflicts using vim/gvimdiff with vim-mergetool: understand LOCAL vs REMOTE, use zdiff3, and tame the layout for a faster, easier workflow.

Rebasing in git
is sold as the tidy way to keep history clean, and merging is the everyday workhorse. That is until a conflict drops you into gvimdiff
with four panes that all look “nearly right”.
If you’re comfortable with git
and vim
but find conflicts disorientating, it's not just you. The meanings of BASE, LOCAL and REMOTE aren't always clear; earlier commits may already have changed the file you’re looking at; and the default gvimdiff
layout rarely explains itself.
In this post we’ll look at what git
actually does in both a merge and a rebase, show how to interpret what gvimdiff
is showing you, and share a few small tweaks that turn conflict resolution into a steady, repeatable and, above all, understandable workflow.
Configuration
There are some git and vim configurations that will make life easier.
Git
Configure git's merge tool and conflict style by running:
git config --global merge.tool gvimdiff
git config --global merge.conflictstyle zdiff3
Recommended git configuration
Omit --global
to make changes to your local configuration (stored in .git/config
). Alternatively, edit ~/.gitconfig
and add:
[merge]
tool = gvimdiff
conflictStyle = zdiff3
Vim
This isn't required, but it will make managing conflicts easier. Add to your ~/.vimrc
:
syntax enable
" Highligt diffs
highlight DiffAdd guibg=#E6FFE6
highlight DiffChange guibg=#FFFFE6
highlight DiffDelete guibg=#FFE6E6
highlight DiffText guibg=#FFCC00
" Highlight conflight zones
function! ConflictsHighlight() abort
syn region conflictStart start=/^<<<<<<< .*$/ end=/^\ze\(=======$\||||||||\)/
syn region conflictMiddle start=/^||||||| .*$/ end=/^\ze=======$/
syn region conflictEnd start=/^\(=======$\||||||| |\)/ end=/^>>>>>>> .*$/
highlight conflictStart guibg=#E6FFE6
highlight conflictMiddle guibg=#F49BAB
highlight conflictEnd guibg=#FEFFC4
endfunction
augroup MyColors
autocmd!
autocmd BufEnter * call ConflictsHighlight()
augroup END
Recommended vim configuration
I also use git-prompt.sh, which gives status information about the current git repo within the shell prompt.
Sample git repository
If you want to follow through the rest of this post, create a new directory and initialise it as a git repo:
git init
Create a file, f.txt
, with one number per line as follows:
1
2
3
4
5
6
7
8
9
10
Add it and commit it:
git add f.txt
git commit -m "Initial version"
Fork a feature
branch but remain on master
:
git branch feature
Edit f.txt
on the master branch to delete line 2, add a line after 3, and change line 7:
1
3
Added master
4
5
6
7 conflict on master
8
9
10
Commit the change
git add f.txt
git commit -m "Edits on master"
Switch to the feature branch (git switch feature
) and edit f.txt
to add a line at the start, edit line 7 and remove line 9:
Add feature
1
2
3
4
5
6
7 feature conflict
8
10
Commit the change:
git add f.txt
git commit -m "Edits on feature"
Both branches have now diverged from BASE, but most changes don't conflict. Line 7, though, has been changed in both branches.
Merging
The sample above is very simple with one commit per branch after the fork. We could illustrate it like this:
A---B (master)
\
C (feature)
More typically there would be more commits on both branches, and possibly other branches involved too. In reality your git repo may look more like this:
A---B---D---F (master)
\
C---E (feature)
The principle is the same: two branches that have diverged over time from a common starting point.
Before we look at what git does during a merge, some terminology:
- The HEAD of each branch is a pointer to the tip of that branch (F and E in the above diagram). You can list the SHA-1 checksum for each branch's HEAD with:
git show-ref --heads
- The merge base is the commit where two separate branches diverged from one another (B in the above diagram). You can list the merge base for two (or more) branches with:
git merge-base master feature
What happens during a git merge?
You want to ensure that changes made to the master
branch are incorporated into your feature
branch. From the feature
branch you run:
$ git merge master
This will creates a “merge commit” on the feature
branch that includes the history of each branch. It looks like this:
A---B---D---F (master)
\ \
C---E----G (feature)
For each file that changed after commit B, a change made in only one branch will propagate to the merge commit, regardless of which branch it was made in.
If a different change is made to the same line(s) in both branches we'll get a merge conflict.
Illustrating a merge conflict
Staying on our feature branch, let's try to merge in the changes from master:
$ git merge master
Auto-merging f.txt
CONFLICT (content): Merge conflict in f.txt
Automatic merge failed; fix conflicts and then commit the result.
git merge resulting in a merge conflict
Before we look at the conflicts, some more terminology. Git refers to the merge base as BASE; to the branch we are merging onto (feature
in this case) as LOCAL; and the branch we are merging from (master
in this case) as REMOTE.
Git has now inserted conflict markers into f.txt.
If we open it in gvim
, the syntax highlighting we specified earlier highlights the conflicts:

Interpreting conflict markers
The conflict markers show three variants of the conflict.
The top section, from the left angle brackets to the vertical bars and highlighted in green, is labelled "HEAD" and shows the version of the file for the current HEAD, which is the branch we are on (feature
) and is also the LOCAL version.
The middle section, from the vertical bars to the equals signs and highlighted in pink, is labelled "0063e66" and shows the version of the file in BASE (0063e66 is the shortened SHA-1 of the merge-base commit).
The bottom section, from the equals signs to the right angle brackets and highlighted in yellow, is labelled "master" and shows the version of the file for the REMOTE file, which is the version we are merging in (master
).
Conflict resolution
For a simple conflict like this, we could just edit f.txt
, remove the conflict markers (lines of <, |, = and >) and leave whatever we want for line 7 behind (or remove it altogether).
However, it's helpful to look at this simple conflict using gvimdiff
which we set up as git's merge tool.
Using gvimdiff
If we run git mergetool
, gvimdiff
will open and look as follows:

There's a lot going on here. Helpfully - to some extent - each window is labelled with the version of the our conflicted file, f.txt
, that it contains.
In fact, if we look in our repo directory, we will see that git
has created some temporary files:
$ ls
f_BACKUP_14319.txt f_BASE_14319.txt f_LOCAL_14319.txt f_REMOTE_14319.txt f.txt
The top left window shows the LOCAL file, the version on our feature
branch before the merge.
The top middle window shows the BASE file, from when the fork occurred.
The top right window shows the REMOTE file, the version on our master
branch before the merge.
Finally, the bottom window shows the our conflicted file, f.txt
as it currently appears, complete with conflict markers. This is the version we should edit and is often referred to as the MERGED version.
But why are there so many pink and green lines when only one line is in conflict?
How gvimdiff works
Buffers within vim
may be marked to show differences (for example, with :diffthis
). You can see which buffers are marked to show differences with
:echo filter(map(tabpagebuflist(), 'bufname(v:val)'), 'bufwinnr(v:val) != -1 && getbufvar(v:val, "&diff")')
Here's the result of running that command on the current gvimdiff
:

Within one tab, each buffer so marked is compared with all other buffers so marked. We've set up syntax highlighting to show lines that have been added (green), are missing (red) or are changed (yellow).
One (non-conflicting) line was added at the start of the file on the feature
(LOCAL) branch. That means the LOCAL branch differs from both BASE and REMOTE, and the differences are highlighted with the added top line of LOCAL shown in green (the top left and bottom windows), and the "missing" line in red in the other two windows.
This process is repeated for all differences between any of LOCAL, BASE and REMOTE.
In summary: gvimdiff
will show all changes, not just conflicts. In a big merge that can mean a lot of noise.
A cleaner gvimdiff
Alexey Samoshkin has created a vim
plugin, vim-mergetool, which greatly improves gvimdiff
when it is used to resolve git
merge conflicts.
Install the plugin however you prefer to install vim
plugins.
Update your ~/.gitconfig
file to specify that the merge tool is now vim_mergetool
as follows:
[merge]
# tool = gvimdiff
tool = vim_mergetool
conflictstyle = zdiff3
[mergetool "vim_mergetool"]
cmd = gvim -f -c "MergetoolStart" "$MERGED" "$BASE" "$LOCAL" "$REMOTE"
trustExitCode = true
For the moment we'll configure vim_mergetool
to display the same windows as the native gvimdiff
did. Add this to your ~/.vimrc
:
set hidden
let g:mergetool_layout = 'lbr,m'
If you've been following along, close the gvimdiff
window, abort the current merge, run it again and re-invoke mergetool:
git merge --abort
git merge master
git mergetool
Here's the new gvimdiff
window:

This is much cleaner. The top three windows show the local, base and remote files with the non-conflicting changes merged in, but only the conflict is highlighted. The merged window is at the bottom and it has defaulted to taking the "local" version of the conflict.
How vim-mergetool manages files with conflicts
You may have noticed that the top three windows are now named in lowercase rather than uppercase as before ("local" vs "LOCAL"). vim-mergetool
has created local
, remote
, base
buffers from conflict markers in the MERGED
file and then removed the conflict markers from the final version. All resolved (non-conflicting) merges are shown without any special highlighting.
We forced vim-mergetool
to display the four windows shown above. The default configuration of vim-mergetool
shows only the merged file and the remote. To see how that appears, enter:
:MergetoolToggleLayout mr

The result is an even cleaner layout. By default, vim-mergetool
has selected the LOCAL (feature) branch version of the conflict, but this default may be changed as we'll see later. We can work thought the conflicts using the standard ]c
and [c
vim
commands to jump to the next or previous conflict respectively. Once on a conflict, we can use the standard do
or dp
commands to "obtain" or "put" this version from or to the other window. Note that the remote window is read-only; the only file we can edit is the merged version.
To revert our configuration to this default, remove the let g:mergetool_layout = 'lbr,m'
line from your ~/.vimrc
- but leave it as it is for the moment.
Finishing the merge
The aim here is to make the left-hand window (the MERGED
file) look as we want it to with respect to the conflicts, possibly by simply accepting the automatically-chosen LOCAL resolution as shown by default above. Once we're happy with the left-hand window we can write the change and quit with :xa
.
Our file, f.txt
, will be shown by git
as a change to be committed, which we can then do:
$ git status
On branch feature
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: f.txt
$ git commit -m "Merge changes from master"
Rebasing
A git rebase
is another way of merging changes between branches, but there are some subtle and important differences from a git merge
.
In the diagram below, we start from the same place as the earlier git merge
example: the feature
branch was forked from revision B on the master
branch, and has had commits C and E added; meanwhile, master
has continued with commits D and F.
A---B---D---F (master)
\
C---E (feature)
You want to ensure that changes made to the master
branch are incorporated into your feature
branch. From the feature
branch you run:
$ git rebase master
This is what happens:
git
finds the merge base of the two branches (B)- It saves the diff introduced by each commit of the branch you’re on (
feature
) to temporary files - It resets the current branch to point to the last commit of the branch you are rebasing onto (
master
, commit F in this case) - It applies each of the saved diffs from step 2 in sequence.
This is the end result:
A---B---D---F (master)
\
C2---E2 (feature)
The C2
and E2
commits differ from the original C
and E
commits because they are being reapplied to a different base. The SHA-1 checksums will also differ from the original commits. In other words, we have changed the history for the feature
branch. If we had previously pushed the feature
branch to a remote repository, we'd need to git push --force
now because the remote HEAD no longer exists in our local repo.
Also notice that the merge base has moved. Whereas it was B
before, it's now F
.
Managing merge conflicts from rebase
Merge conflicts from a rebase can be complex, but let's start with a simple case and revisit exactly what we did with the merge above.
First, let's see our commit history. From the feature
branch we run:
$ git log --oneline
176107f (HEAD -> feature) Edits on feature
0063e60 Initial version
Now let's rebase onto master
with git rebase master
. This is what happens:

Git has pointed to the master
branch and tried to reply the diff history. In this case there is only one commit to reply, and the third line of the output shows that commit 176107f
caused a conflict.
If we inspect f.txt
, we see conflict markers as before, but the top section, HEAD, now shows the master
branch and the bottom section the feature
branch. That's the opposite way around to what we saw in the merge. What's going on?

Remember that when we rebase onto master
, git temporarily points to the most recent master
commit before replaying the diffs. That makes the master
branch our LOCAL file and the feature
branch the REMOTE file.
This is confirmed when we run git mergetool
:

The local window, top left, has the master
branch and the remote window, top right, has the feature
branch.
LOCAL vs REMOTE for merge and rebase
When resolving conflicts in a merge:
- LOCAL (ours): your current branch (e.g. feature)
- REMOTE (theirs): the branch you’re merging in (e.g. master)
When resolving conflicts in a rebase:
- LOCAL: the branch you're rebasing onto (e.g. master)
- REMOTE: the feature commit being replayed
Fix vim-mergetool for rebase conflicts
If we enter :MergetoolToggleLayout mr
as we did before, vim-mergetool
shows us the merged file with the conflict from the master
branch selected by default, and compares it with the feature
branch:

This is typically not what we want - we would usually prefer our feature
branch version and want to compare it with the master
version. In other words, for a conflict arising from a git rebase, we want to:
- prefer the changes from the
feature
(REMOTE) version; and - see the merged compared with the
master
(LOCAL) version
We can achieve that with the following two vim commands:
:MergetoolPreferRemote
:MergetoolSetLayout ml
Now our gvimdiff
layout looks like this:

That layout assumes that, for merge conflicts, the feature
branch version is correct.
Finishing the rebase
Once we've resolved all conflicts (or we're just happy to accept the version of the file on the left), we write and close all files. The easiest way to do that is :xa
in vim
.
If we check git status
, we can see that all conflicts are resolved and that we only need to run git rebase --continue
to clean up:
$ git status
interactive rebase in progress; onto e00fdc5
Last command done (1 command done):
pick 176107f Edits on feature
No commands remaining.
You are currently rebasing branch 'feature' on 'e00fdc5'.
(all conflicts fixed: run "git rebase --continue")
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: f.txt
If we now look at the git log, we can see that the SHA-1 for the "Edits on feature" commit has changed, reflecting that this commit was "replayed" on top of the master
branch:
$ git log --oneline
0b3ea56 (HEAD -> feature) Edits on feature
e00fdc5 (master) Edits on master
0063e60 Initial version
More complex rebases
Sometimes rebases can cause head-scratching. Let's set up a simple and rather contrived example. Initialise a new directory as a git
repository as before, then create f.py
as follows:
#!/usr/bin/env python3
def fn(x: int) -> int:
return x + 2
Add and commit it:
git add f.py
git commit -m "Initial version"
Fork a feature
branch but remain on master
:
git branch feature
Edit f.py
on the master branch as follows:
#!/usr/bin/env python3
def fn(x: int) -> int:
return x * 2 - 1
Add and commit:
git add f.py
git commit -m "Fix return value of fn"
Make one more edit on the master
branch:
#!/usr/bin/env python3
def fn(x: int) -> int:
return x * 3
Add and commit:
git add f.py
git commit -m "Properly fix return value of fn"
Switch to the feature
branch (git switch feature
) and make a similar series of edits:
- Change the
return
line toreturn x * 2 + 1
- Commit:
git commit -am "Fixup fn"
- Change the
return
line toreturn x * 3
- Commit:
git commit -am "Properly fix fn"
f.py
has ended up the same on both branches, but it diverged during the development process. Of course, in reality the changes would be more numerous and complex, and would probably be spread over the longer period of time, but this will serve to illustrate a point.
Test merge
If we carried out a git merge
at this point, it would complete without conflict:
$ git merge --no-commit --no-ff master
Automatic merge went well; stopped before committing as requested
Remember, a git merge
will apply the differences between BASE and LOCAL, and between BASE and REMOTE. In this case, those two changes would be the same (ie, not in conflict), so the merge would succeed.
Rebase
From the feature
branch, let's rebase onto master
:

There were conflicts rebasing, and after rebasing f.py
looks like this:

If these edits had actually taken place over a long period of time, the above could be confusing. Both branches have return x * 3
so where did these two return
lines come from?
The conflict details
The git log
on the feature
branch:
$ git log --oneline
6fc5a6e (HEAD -> feature) Properly fix fn
8baa6b9 Fixup fn
be4e89c Initial version
git log from feature
The git log
on the master
branch:
$ git log --oneline
f694e2b (HEAD -> master) Properly fix return value of fn
2139ab6 Fix return value of fn
be4e89c Initial version
git log from master
The current git status
:
$ git status
interactive rebase in progress; onto f694e2b
Last command done (1 command done):
pick 8baa6b9 Fixup fn
Next command to do (1 remaining command):
pick 6fc5a6e Properly fix fn
(use "git rebase --edit-todo" to view and edit)
You are currently rebasing branch 'feature' on 'f694e2b'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: f.py
no changes added to commit (use "git add" and/or "git commit -a")
Let's go through this carefully:
- The merge base is
be4e89c
- Because this is a rebase,
git
then points to the HEAD of the branch we want to rebase onto (master
,f694e2b
) - Git now replays the diffs from BASE to the HEAD of
feature
one by one. - The first diff is in conflict - in that diff on the
feature
branch we changedreturn x + 2
toreturn x * 2 + 1
. However, on themaster
branch that return line has already changed fromreturn x + 2
so we have a conflict, andgit
will insert conflict markers as before. This is thepick 8baa6b9 Fixup fn
in the status report above, which we can see from thegit log
command is the first commit after the fork. So far, so good. git
now takes the next diff ("Next command to do (1 remaining command): pick 6fc5a6e Properly fix fn" in the status report) and tries to apply it. However, the line it needs to change is already in conflict.git
cannot proceed. The last line of thegit rebase master
command saysCould not apply 8baa6b9...
, and that is confirmed by the status report.- Because of the conflict on top of an existing conflict, git stops.
The solution is to resolve the existing conflicts and continue the rebase with git rebase --continue
The longer term solution to try to avoid these problems is to rebase frequently.
When to use merge versus rebase
So which should you use, merge or rebase? Here's a quick summary:
Merge:
- Creates a new merge commit with two parents
- All existing commits remain unchanged.
- History fully visible.
- The merge base does not change
- Possibly one conflict resolution
- Best for shared branches where history preservation is important
Rebase:
Key points:
- Each commit of the
feature
branch is replayed on themaster
branch creating new commits. - There is no merge commit.
- History is changed and made more linear.
- The merge base moves forward.
- Possibly multiple conflict resolutions
- Best for cleaning up a local feature branch
Summary
Resolving git
merge conflicts arising from git merge
can be confusing, and those arising from git rebase
even more so. Having a good understanding of what is actually happening, and by using the vim-mergetool
plugin, conflict resolution becomes much easier.