Merging Manually Using Your Favorite Editor and diff3

Some people prefer editing conflicts within an editor, manually merging the areas that contain conflicts. To do this with Mercurial, you can use a small wrapper-script around the GNU diff3 utility that is described here.

Wrapping GNU diff3(1)

The GNU diff3(1) utility can accept a list of three files, similar to the one Mercurial passed to a merger program and generate a copy of the local file with conflict markers. Typically, the output of diff3(1) looks like this:

A
<<<<<<< /path/file
B - my local changes
||||||| /tmp/file~base.yx63PT
B - the common base
=======
B - changes made by others
>>>>>>> /tmp/file~other.fdfgW2
C

To make diff3(1) generate a copy of the merged file with this sort of conflict markers and fire up an editor on this copy, you can save the following to for example hg-diff3-merge in your PATH and turn on its execute bit:

if test $# -ne 3 ; then
        echo >&2 "usage: `basename $0` MYFILE OLDFILE YOURFILE"
        exit 1
fi

# Keep a local copy of the filenames involved in the merge.
LOCAL="$1"
BASE="$2"
OTHER="$3"

cleanup() {
        if test -n "${TMPDIR}" && test -n "${WC}" ; then
                B=`dirname "${WC}"`
                TMPDIRPATH=`cd "${TMPDIR}"; pwd`
                WCPATH=`cd "${B}"; pwd`
                if test X"${B}" = X"${TMPDIR}" ; then
                        /bin/rm -f "${WC}"
                fi
        fi
}

success() {
        if test -z "${WC}" || test -z "${LOCAL}" ; then
                err 1 "internal merge script error."
        fi
        # The merge was successful.  Copy back the merged file on top of ${LOCAL}
        cp "${WC}" "${LOCAL}" && /bin/rm "${WC}"
        if test $? -ne 0 ; then
                err 1 "Failed to save merged file at ${LOCAL}"
        fi
}

err() {
        errcode=$1
        shift
        echo >&2 "`basename $0`: error: $*"
        cleanup
        exit $errcode
}

# Since this script depends on manual edits being performed to the files being
# merged, make sure that ${EDITOR} is truly set to something, even if this is
# just plain good ol' vi(1).
EDITOR="${EDITOR:-vi}"
export EDITOR

# First make sure $TMPDIR points to a meaningful directory.  We will be using
# this shell variable further down, so it's a good idea to make sure it isn't
# empty later on.
TMPDIR="${TMPDIR:-/var/tmp}"
export TMPDIR

# We will be using a temporary file with the diff3(1) output as the merge
# buffer, until either the merge removes all conflict markers from the working
# copy of the file or we fail somehow to complete the merge.
WC=`mktemp "${TMPDIR}/hgmerge-XXXXXX"`
if test $? -ne 0 ; then
        err 1 "Cannot create temporary file at ${TMPDIR}/hgmerge-XXXXXX"
fi

# We depend on diff3(1) being available to do the first pass of the merge,
# adding conflict markers around the areas that should be edited.
which diff3 >/dev/null 2>&1
if test $? -ne 0 ; then
        err 1 "No diff3(1) utility found in the current PATH."
fi

# First try to add conflict markers around the areas that need special
# attention in the ${LOCAL} file.  The output is not saved directly over the
# file that is currently in-conflict, but is saved in the ${WC} temporary file
# to allow editing of the conflict regions without
diff3 -m "${LOCAL}" "${BASE}" "${OTHER}" > "${WC}"
rc=$?
if test $rc -eq 0 ; then
        # No conflicts found.  Merge done.
        success
        exit 0
elif test $rc -gt 1 ; then
        err 1 "serious diff3 error, while trying to merhge ${LOCAL}"
fi

# In all other cases, diff3(1) has found conflicts, added the proper conflict
# markers to the ${WC} file and we should now edit this file.  Fire up an
# editor with the ${WC} file and let the user manually resolve the conflicts.
# When the editor exits successfully, there should be no conflict markers in
# the ${WC} file, otherwise we consider this merge failed.
${EDITOR} "${WC}"
if test $? -ne 0 ; then
        err 1 "merge error for ${LOCAL}"
fi
if grep '^<<<<<<<' "${WC}" >/dev/null 2>&1 ||
   grep '^|||||||' "${WC}" >/dev/null 2>&1 ||
   grep '^=======' "${WC}" >/dev/null 2>&1 ||
   grep '^>>>>>>>' "${WC}" >/dev/null 2>&1 ; then
        err 1 "conflict markers still found in the working-copy.  Merge aborted for ${LOCAL}"
fi

success
exit 0

How the script works

This script tries first to automatically merge the files using the GNU diff3(1) utility program. If the automatic merge fails because there are conflicts, then an editor is launched on the output of diff3(1) to let you manually resolve the conflicts. When the editor is done modifying the resolved file, the script checks again to make sure that all conflict markers have been removed now. If there are still some conflict markers, the merge fails. Re-running the merge should be ok.

Enabling the script usage

The script must be enabled in your hgrc file (either ~/.hgrc or the local working copy .hg/hgrc) to point Mercurial at your hg-diff3-merge:

   [ui]
   merge = hg-diff3-merge

Or integrate it in your merge-tools configuration with proper priority:

    [merge-tools]
    hg-diff3-merge.priority = 100

A Simpler Version

# hgmerge.sh - invoke diff3 to merge files, save conflicts in mine.hgmerge
#
# author: Noel Burton-Krahn
# created: Feb 28, 2007

# exit on error or undefined variables
set -eu

# command-line args
mine="$1"
orig="$2"
theirs="$3"

# where to save merged file
merged="$mine".hgmerge

rm -f "$merged"
if diff3 -L mine -L original -L theirs -E -m "$mine" "$orig" "$theirs" > "$merged"
then
    mv "$merged" "$mine"
    echo Merged "$mine"
else
    echo Conflict saved in "'""$merged""'".  Rename to $(basename "$mine") when fixed.
    exit 1
fi

Version run under Windows 2000/XP

This script like pervious. It tested with diff3 from GnuWin32, MinGW, Cygwin and with space on path to merged files.

@echo off

IF "%3"==""     GOTO err_arg_cnt
IF NOT "%4"=="" GOTO err_arg_cnt

IF NOT EXIST %1 GOTO err_file_not_exist
IF NOT EXIST %2 GOTO err_file_not_exist
IF NOT EXIST %3 GOTO err_file_not_exist

SET my=%1
SET orig=%2
SET your=%3
SET merged=%my%.hgmerge

IF EXIST %merged% (del /q %merged%)
diff3 -L my -L orig -L your -E -m %my% %orig% %your% > %merged%

IF ERRORLEVEL 1 (
    echo C %my%
    move /y %merged% %my%
    EXIT 1
) ELSE (
    echo M %my%
    move /y %merged% %my%
)
EXIT 0

:err_arg_cnt
  echo Wrong arg count!
  echo You run:
  echo ^> %0 %*
  echo Must:
  echo ^> %0 my orig your
EXIT 1

:err_file_not_exist
  echo One of '%*' files not exist.
EXIT 1

Python version

Finally, if the batch file doesn't work for you (for some reason it doesn't on my Vista 64 system with Mercurial 1.3), here is a Python version. It writes all conflicts and merge failures to a log file, which I find useful.

import sys
import subprocess
import os.path

if len(sys.argv) != 4:
    print "Usage: hgdiff3 <my> <orig> <your>"
    sys.exit(1)

my = sys.argv[1]
orig = sys.argv[2]
your = sys.argv[3]

merged = my + '.hgmerge'

if not os.path.exists(my) or not os.path.exists(orig) or \
    not os.path.exists(your):
    print "One or more of the merge files do not exist"
    sys.exit(1)

if os.path.exists(merged):
    os.remove(merged)

output = open(merged, 'w')
p = subprocess.Popen(['diff3', '-m',
                    '-L', 'local', '-L', 'base', '-L', 'other',
                    my, orig, your], stdout=output)
result = p.wait()
output.close()

if result == 0 or result == 1:
    os.remove(my)
    os.rename(merged, my)

if result == 0:
    print "diff3 merged automatically"
elif result == 1:
    print "  Conflicts: " + my
    log = open('hgmerge.log', 'a')
    log.write( "Conflicts: " + my + "\n" )
    log.close()
else:
    print "  Merge failed (binary file?): " + my
    log = open('hgmerge.log', 'a')
    log.write( "Failed: " + my + "\n" )
    log.close()

sys.exit(result)

MergingManuallyInEditor (last edited 2009-07-28 09:31:32 by ColinCaughie)