Ediff is «a comprehensive visual interface to Unix diff and patch utilities»1 that comes with your Emacs. This document shows how to use Ediff with Git for resolving merge conflicts and inspecting differences between file versions.
The solution described here uses a wrapper-shell-script that is called by git mergetool and git difftool. Arguments for the script are provided by Git via the appropriate Git configuration.
The impatient may just download the wrapper-script for Ediff, possibly adapt it (see also: Ediff Script) and add the appropriate Git configuration values.
The objective is to integrate Ediff smoothly into the Git workflow. The following should be achieved:
- Launch Ediff whenever git mergetool is invoked.
- Launch Ediff whenever git difftool is invoked.
- Prefer emacsclient over emacs.
- Create a new Emacs frame when on X.
- When merging, include ancestor (BASE) if available.
- When merging, check resulting merged file for conflict markers.
- When merging, use exit code to decide if merge was successful.
As the Git mergetool and difftool help describes, it is possible to define custom merge and diff tool commands. Although it would be feasible to define an emacsclient command directly in the Git configuration, I prefer to use a shell script. This makes it easier to decide which Ediff function to execute and which exit code to return. The script is called with the appropriate arguments from git mergetool or git difftool as defined in the Git configuration.
There are basically three different cases that the script must handle by executing a specific Ediff function with the proper arguments:
-
Git diff
A simple diff, triggered by git difftool should executeediff
with the diff pre- and post-image as arguments. -
Git merge without ancestor
A merge without ancestors occurs for instance if you merge two branches and those two branches have created the same path/file independently, i.e. without a common base version. In this caseediff-merge-files
should be executed with three arguments: the version of the file in the current branch, the version of the file in the branch to be merged from and the target file to write the merged version to. -
Git merge with ancestor
Often the conflicting versions of a file have a common earlier version, their ancestor or base. When an ancestor is availableediff-merge-files-with-ancestor
should be executed with four arguments: the version of the file in the current branch, the version of the file in the branch to be merged from, the target file to write the merged version to and the base version of the file.
Following below is the wrapper-script for Ediff which should be
executable. The Git configuration below assumes that the script is named
ediff.sh
.
1: #!/bin/bash 2: 3: # test args 4: if [ ! ${#} -ge 3 ]; then 5: echo 1>&2 "Usage: ${0} LOCAL REMOTE MERGED BASE" 6: echo 1>&2 " (LOCAL, REMOTE, MERGED, BASE can be provided by \`git mergetool'.)" 7: exit 1 8: fi 9: 10: # tools 11: _EMACSCLIENT=/usr/bin/emacsclient 12: _BASENAME=/bin/basename 13: _CP=/bin/cp 14: _EGREP=/bin/egrep 15: _MKTEMP=/bin/mktemp 16: 17: # args 18: _LOCAL=${1} 19: _REMOTE=${2} 20: _MERGED=${3} 21: if [ ${4} -a -r ${4} ] ; then 22: _BASE=${4} 23: _EDIFF=ediff-merge-files-with-ancestor 24: _EVAL="${_EDIFF} \"${_LOCAL}\" \"${_REMOTE}\" \"${_BASE}\" nil \"${_MERGED}\"" 25: elif [ ${_REMOTE} = ${_MERGED} ] ; then 26: _EDIFF=ediff 27: _EVAL="${_EDIFF} \"${_LOCAL}\" \"${_REMOTE}\"" 28: else 29: _EDIFF=ediff-merge-files 30: _EVAL="${_EDIFF} \"${_LOCAL}\" \"${_REMOTE}\" nil \"${_MERGED}\"" 31: fi 32: 33: # console vs. X 34: if [ "${TERM}" = "linux" ]; then 35: unset DISPLAY 36: _EMACSCLIENTOPTS="-t" 37: else 38: _EMACSCLIENTOPTS="-c" 39: fi 40: 41: # run emacsclient 42: ${_EMACSCLIENT} ${_EMACSCLIENTOPTS} -a "" -e "(${_EVAL})" 2>&1 43: 44: # check modified file 45: if [ ! $(egrep -c '^(<<<<<<<|=======|>>>>>>>|####### Ancestor)' ${_MERGED}) = 0 ]; then 46: _MERGEDSAVE=$(${_MKTEMP} --tmpdir `${_BASENAME} ${_MERGED}`.XXXXXXXXXX) 47: ${_CP} ${_MERGED} ${_MERGEDSAVE} 48: echo 1>&2 "Oops! Conflict markers detected in $_MERGED." 49: echo 1>&2 "Saved your changes to ${_MERGEDSAVE}" 50: echo 1>&2 "Exiting with code 1." 51: exit 1 52: fi 53: 54: exit 0
To use the script successfully you may have to adapt it.
- Check if the required tools are available and adapt their path if necessary (line 11-15).
- Adapt the poor man's check for console (vs. graphical display) to your needs (line 34-39).
To add the Ediff as default diff and merge tool, you need the following in one of your Git configurations (preferably in ~/.gitconfig
):
[diff]
tool = ediff
guitool = ediff
[difftool "ediff"]
cmd = /PATH/TO/ediff.sh $LOCAL $REMOTE $MERGED
[merge]
tool = ediff
[mergetool "ediff"]
cmd = /PATH/TO/ediff.sh $LOCAL $REMOTE $MERGED $BASE
trustExitCode = true
Just replace the /PATH/TO/
parts in the above snippet and leave the arguments ($LOCAL etc.) as they are. These three or four variables are provided by git difftool and git mergetool respectively.
-
To perform the check for conflict markers (line 45ff) the script needs to wait for emacsclient to finish. This means that you cannot use emacsclient with the option
–no-wait
.
If you desperately need that option and want to add it anyway (in line 38) then you should remove the check for conflict markers (line 44-52) as it would always be successful. Additionally, you must set mergetool.ediff.trustExitCode tofalse
. -
If conflict markers are found after emacsclient has returned, the script exits with code 1. With mergetool.ediff.trustExitCode set to
true
Git will then restore the original (conflict) version of the file, thus throwing away everything you've done in Emacs. That's okay if you wanted to start over anyway. However, losing your edits may not be desired in some cases (e.g. you just forgot to remove a conflict marker). Therefore, the script saves your edited version away before exiting so that you can always retrieve what you've done (see line 46f).
1: https://www.gnu.org/software/emacs/manual/html_node/ediff/
Adapted from: `https://ulf.zeitform.de/en/documents/git-ediff.html'
author: Ulf Stegemann ([email protected])