Skip to content

Latest commit

 

History

History
160 lines (118 loc) · 12.2 KB

README.md

File metadata and controls

160 lines (118 loc) · 12.2 KB

Using Emacs Ediff as Git Merge/Difftool

Table of contents

  1. Overview
  2. Objective
  3. Implementation
    1. Ediff Functions and Arguments
    2. Ediff Script
      1. Script Modifications
    3. Git Configurations
    4. Caveats

Overview

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.

Objective

The objective is to integrate Ediff smoothly into the Git workflow. The following should be achieved:

  1. Launch Ediff whenever git mergetool is invoked.
  2. Launch Ediff whenever git difftool is invoked.
  3. Prefer emacsclient over emacs.
  4. Create a new Emacs frame when on X.
  5. When merging, include ancestor (BASE) if available.
  6. When merging, check resulting merged file for conflict markers.
  7. When merging, use exit code to decide if merge was successful.

Implementation

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.

Ediff Functions and Arguments

There are basically three different cases that the script must handle by executing a specific Ediff function with the proper arguments:

  1. Git diff
    A simple diff, triggered by git difftool should execute ediff with the diff pre- and post-image as arguments.

  2. 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 case ediff-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.

  3. Git merge with ancestor
    Often the conflicting versions of a file have a common earlier version, their ancestor or base. When an ancestor is available ediff-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.

Ediff Script

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

Script Modifications

To use the script successfully you may have to adapt it.

  1. Check if the required tools are available and adapt their path if necessary (line 11-15).
  2. Adapt the poor man's check for console (vs. graphical display) to your needs (line 34-39).

Git Configuration

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.

Caveats

  1. 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 to false.

  2. 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).

Footnotes

1: http://www.gnu.org/software/emacs/manual/html_node/ediff/

Adapted from: `http://ulf.zeitform.de/en/documents/git-ediff.html'

author: Ulf Stegemann (ulf@zeitform.de)