Vim Tips Wiki
No edit summary
Line 34: Line 34:
   
 
Mergetools can help make short work of even gnarly merge conflicts. Invoke
 
Mergetools can help make short work of even gnarly merge conflicts. Invoke
Vimdiff as a mergetool with <pre>git mergetool -t gvimdiff</pre>. Recent
+
Vimdiff as a mergetool with <tt>git mergetool -t gvimdiff</tt>. Recent
 
versions of Git invoke Vimdiff with the following window layout:
 
versions of Git invoke Vimdiff with the following window layout:
   

Revision as of 19:24, 5 June 2012

Git ships with support to invoke Vimdiff as a “mergetool” to help resolve merge conflicts. Unfortunately Vim struggles a bit with three-way diffs; both with highlighting the differences and with shuffling individual changes between the three windows.

This article details a simpler way to use Vimdiff as a Git mergetool.

An Introduction

You are likely to encouter a merge conflict in Git eventually by merging one feature branch into another or by merging upstream changes into a local branch.

Git is quite good at automatically resolving conflicts. A simplistic explanation is that Git starts by finding the common ancestor of your local changes and the changes you are merging into yours. Git then replays the history of each lineage, making intelligent choices along the way about how and where code is added or moved or renamed. If there are conflicts that Git cannot resolve itself the file is left in a conflicted state that must be resolved manually -- more on this below as the MERGED version of the file.

The most simple way to resolve a Git merge conflict is to open the conflicted file with your favorite text editor and to manually find and remove the conflict markers, keeping the best version of the code from each side of the conflict, then marking the conflict as resolved in Git.

Plain

Using Vimdiff as a Mergetool

Resolving manually is fine for small conflicts and for obvious conflicts, but this can be frustrating and error-prone for large conflicts and for conflicts where each side of the changes differ only subtly.

Mergetools can help make short work of even gnarly merge conflicts. Invoke Vimdiff as a mergetool with git mergetool -t gvimdiff. Recent versions of Git invoke Vimdiff with the following window layout:

+--------------------------------+ 
| LOCAL  |     BASE     | REMOTE | 
+--------------------------------+ 
|             MERGED             |
+--------------------------------+ 
LOCAL
A temporary file containing the contents of the file on the current branch.
BASE
A temporary file containing the common base for the merge.
REMOTE
A temporary file containing the contents of the file to be merged.
MERGED
The file containing the conflict markers. Git has performed as much automatic conflict resolution as possible and the state of this file is a combination of both LOCAL and REMOTE with conflict markers surrounding anything that Git could not resolve itself. The mergetool should write the result of the resolution to this file.

Vimdiff for Three-Way Merges

The default Vimdiff window layout presents quite a lot of useful information. In many cases, however, it is simply too much noise and hides the relevant visual clues you need to solve a conflict quickly.

For example, here is the default view of the conflict shown above:

Vimdiff-default

Vimdiff is an excellent two-way diff viewer and many of the helper shortcuts are geared for moving changes between only two windows at a time. Displaying differences between three (or more!) versions of a file is a hard problem to solve and syntax highlighting alone just isn't well-suited to the job.

Playing to Vimdiff Strengths

It is useful to be able to quickly see the remote version of the file, or the common ancestor of both files, or even your version of the file before any automatic merging was attempted. However, it is often most useful to view the simple comparison of just the “left” conflict and the “right” conflict.

For example, here is the same conflict as above but only diffing the conflicts themselves:

Vimdiff-two-way

The resolution is much clearer and since this is two-way merge all of the Vimdiff keyboard shortcuts work as intended.

An Alternate Vimdiff Mergetool

In order to achive the layout detailed above we need to step outside of the Vimdiff mergetool that ships with Git, and even outside of Git itself. The “diffconflicts” script in the section below can be used as a mergetool that will then invoke Vimdiff. Save it as “diffconflicts” somewhere on your shell PATH and mark it as executable (chmod +x diffconflicts).

This script invokes Vimdiff with three tabpages. The first tabpage is a two-way diff of each “side” of a conflict. This allows the full use of vanilla Vimdiff.

Resolve conflicts in the left window; this is the file that will eventually be written to disk as the resolved file.

If you wish to abort Vimdiff and signal to Git that the resolution is not complete, exit Vimdiff with :cq. This tells Vim to exit with an error code which then tells Git that the conflict was not resolved.

#       +------+
#       | Tab1 |
#       +--------------------------------+
#       |    LCONFL     |    RCONFL      |
#       +--------------------------------+

The second tabpage is the standard three-way diff of the pre-automerge “yours”, “theirs”, and the merge base.

Because syntax highlighting in a three-way diff is so noisy, I have found mappings to quickly disable diff syntax highlighting in one window at a time to be very useful in this window layout. (Example mappings are included at the bottom.)

#              +------+
#              | Tab2 |
#       +--------------------------------+
#       |  LOCAL   |   BASE   |  REMOTE  |
#       +--------------------------------+

Finally, the third tabpage is the actual conflicted file that contains the conflict markers.

This view shouldn't be necessary but I find it is helpful to quickly sanity-check the diff in the first tabpage in case the diffconflicts script did not work correctly.

#                     +------+
#                     | Tab3 |
#       +--------------------------------+
#       |       <<<<<<< HEAD             |
#       |        LCONFL                  |
#       |       =======                  |
#       |        RCONFL                  |
#       |       >>>>>>> someref          |
#       +--------------------------------+

The diffconflicts Script

#!/bin/bash
# Instead of editing a file with  <<<< ==== >>> conflict markers, this opens
# each "side" of the conflict markers in a two-way vimdiff window.
#
# Layout:
#
#   Tab1 is a two-way diff of the conflicts.
#       +--------------------------------+
#       |    LCONFL     |    RCONFL      |
#       +--------------------------------+
#   Tab2 is a three-way diff of the original files and the merge base.
#       +--------------------------------+
#       |  LOCAL   |   BASE   |  REMOTE  |
#       +--------------------------------+
#   Tab3 is the MERGED or 'result' file that contains the conflict markers.
#       +--------------------------------+
#       |       <<<<<<< HEAD             |
#       |        LCONFL                  |
#       |       =======                  |
#       |        RCONFL                  |
#       |       >>>>>>> someref          |
#       +--------------------------------+
#
# Workflow:
#
# 1.    Save your changes to the LCONFL temporary file (the left window on the
#       first tab; also the only file that isn't read-only).
# 2.    The LOCAL, BASE, and REMOTE versions of the file are available in the
#       second tabpage if you want to look at them.
# 3.    When vimdiff exits cleanly, the file containing the conflict markers
#       will be updated with the contents of your LCONFL file edits.
#
# NOTE: Use :cq to abort the merge and exit Vim with an error code.
#
# Add this mergetool to your ~/.gitconfig (you can substitute vim for gvim):
#
# git config --global merge.tool diffconflicts
# git config --global mergetool.diffconflicts.cmd 'diffconflicts gvim $BASE $LOCAL $REMOTE $MERGE'
# git config --global mergetool.diffconflicts.trustExitCode true
# git config --global mergetool.diffconflicts.keepBackup false

if [[ -z $@ || $# != "5" ]] ; then
    echo -e "Usage: $0 \$BASE \$LOCAL \$REMOTE \$MERGED"
    exit 1
fi

cmd=$1
BASE=$2
LOCAL=$3
REMOTE=$4
MERGED=$5
LCONFL=$(dirname $5)/$$.left.tmp
RCONFL=$(dirname $5)/$$.right.tmp

# Remove the conflict markers for each 'side' and put each into a temp file
sed -e '/<<<<<<</,/^=======$/d' -e '/>>>>>>>/d' $MERGED > $LCONFL
sed -e '/^=======$/,/>>>>>>>/d' -e '/<<<<<<</d' $MERGED > $RCONFL

# Fire up vimdiff
$cmd -f -R -d $LCONFL $RCONFL \
    -c ":set noro" \
    -c ":tabe $LOCAL" -c ":vert diffs $BASE" -c ":vert diffs $REMOTE" \
    -c ":winc t" -c ":tabe $MERGED" -c ":tabfir"

EC=$?

# Overwrite $MERGED only if vimdiff exits cleanly.
if [[ $EC == "0" ]] ; then
    cat $LCONFL > $MERGED
fi

# Always delete our temp files; Git will handle it's own temp files
rm $LCONFL $RCONFL

exit $EC

Toggle diff Highlighting in a Single Window

" Disable one diff window during a three-way diff allowing you to cut out the
" noise of a three-way diff and focus on just the changes between two versions
" at a time. Inspired by Steve Losh's Splice
function! DiffToggle(window)
    " Save the cursor position and turn on diff for all windows
    let l:save_cursor = getpos('.')
    windo :diffthis

    " Turn off diff for the specified window (but keep scrollbind) and move
    " the cursor to the left-most diff window
    exe a:window . "wincmd w"
    diffoff
    set scrollbind
    set cursorbind
    exe a:window . "wincmd " . (a:window == 1 ? "l" : "h")

    " Update the diff and restore the cursor position
    diffupdate
    call setpos('.', l:save_cursor)
endfunction
" Toggle diff view on the left, center, or right windows
nmap <silent> <leader>dl :call DiffToggle(1)<cr>
nmap <silent> <leader>dc :call DiffToggle(2)<cr>
nmap <silent> <leader>dr :call DiffToggle(3)<cr>

References

Related plugins

Comments

Feedback welcome!