Vim Tips Wiki
(Created page with "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 ...")
 
(add related plugins)
Line 145: Line 145:
   
 
==References==
 
==References==
https://github.com/git/git/blob/master/mergetools/vim
+
* https://github.com/git/git/blob/master/mergetools/vim
  +
*{{help|toc}}
 
  +
==Related plugins==
  +
* [http://sjl.bitbucket.org/threesome.vim/ Threesome] by Steve Losh
  +
* {{script|id=621|text=Conflict2Diff}}? Not sure if this actually does 3-way diff.
   
 
==Comments==
 
==Comments==

Revision as of 15:33, 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 following script 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

).

#!/bin/bash
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

Now configure Git to use it as a mergetool by running the following commands in your shell:

git config --global merge.tool diffconflicts
git config --global mergetool.diffconflicts.cmd 'diffconflicts vim $BASE $LOCAL $REMOTE $MERGE'
git config --global mergetool.diffconflicts.trustExitCode true
git config --global mergetool.diffconflicts.keepBackup false

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.

References

Related plugins

Comments

Feedback welcome!