Merging Manually Using Your Favorite Editor

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 diff(3) 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 [:MergeProgram:merger program] and generate a copy of the local file with [:Conflict:conflict] markers. Typically, the output of diff3(1) looks like this:

A
<<<<<<< local
B - my local changes
||||||| base
B
=======
B - changes made by others
>>>>>>> other
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 a file 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

Don't forget to add an entry in your hgrc file (either ~/.hgrc or the local working copy .hg/hgrc) to point ["Mercurial"] at your merge command (let's call it hg-merge):

   [ui]
   merge = hg-merge

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 work with diff3 from GnuWin32-0.6.19. It may be wrong if diff3 do not understand windows style path (for exemple on MSYS-1.0.10 and may be same on Cygwin).

@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%
    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