Wikia

Vim Tips Wiki

A better Vimdiff Git mergetool

Talk0
1,612pages on
this wiki
Revision as of 17:20, October 8, 2013 by 78.52.147.229 (Talk)

Proposed tip Please edit this page to improve it, or add your comments below (do not use the discussion page).

Please use new tips to discuss whether this page should be a permanent tip, or whether it should be merged to an existing tip.
created June 5, 2012 · complexity basic · author Whiteinge · version 7.0

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.

Overview

You are likely to encounter 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 $MERGED'
# 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

This doesn't seem to work. After adding the script as my mergetool and attempting to run mergetool to resolve a conflict, I get:

git config option merge.tool set to unknown tool: diffconflicts
Resetting to default...
merge tool candidates: opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse
    ecmerge p4merge araxis bc3 emerge vimdiff

Any idea? --August 28, 2012

I do not know anything about this tip, but a quick search for "diffconflicts" in the tip shows you need to create the script, put it in a directory in your PATH, and use chmod (as well as use the git config commands shown). If you get it working, please report here. JohnBeckett (talk) 04:39, August 29, 2012 (UTC)
SOLUTION: you need to follow the hints in the script (configure mergetool.diffconflicts). 10:55, October 27, 2012 (UTC)

Around Wikia's network

Random Wiki