Using ediff as git mergetool
Asked Answered
K

12

56

I would like to be able to use ediff with "git mergetool".

I found some patches that alter the source code, which I don't want to do. Instead, I'd like to add ediff support with my .gitconfig.

I know git has builtin support for emerge, but I prefer ediff.

I attempted to add these lines to my .gitconfig:

[mergetool "ediff"]
    cmd = emacs --eval "(ediff-merge-files-with-ancestor \"$LOCAL\" \"$REMOTE\" \"$BASE\" nil \"$MERGED\")"

But when I try to run this with "git mergetool --tool=ediff", I get this:

eval: 1: Syntax error: "(" unexpected

What am I doing wrong?

Kinna answered 30/11, 2009 at 0:53 Comment(4)
The use of "bzr ..." looks wrong since you are talking about git. At first glance I'm guessing that you mean "git mergetool ..." Might I be correct?Misshapen
Thanks, corrected. I am just now switching from bzr to git. Old habit.Kinna
It's been almost 2 years and this question still gets views/upvotes :)Kinna
If you prefer using the command line, I suggest you take a look at whatworks4me.wordpress.com/2011/04/13/…Hellcat
A
31

I use a a more complicated command. As far as I remember I got it from this thread http://kerneltrap.org/mailarchive/git/2007/6/28/250230 (probably the same as what you are referring to).

[mergetool.ediff]
    cmd = emacs --eval \"\
(progn\
  (defun ediff-write-merge-buffer ()\
    (let ((file ediff-merge-store-file))\
      (set-buffer ediff-buffer-C)\
      (write-region (point-min) (point-max) file)\
      (message \\\"Merge buffer saved in: %s\\\" file)\
      (set-buffer-modified-p nil)\
      (sit-for 1)))\
  (setq ediff-quit-hook 'kill-emacs\
        ediff-quit-merge-hook 'ediff-write-merge-buffer)\
  (ediff-merge-files-with-ancestor \\\"$LOCAL\\\" \\\"$REMOTE\\\"\
                                   \\\"$BASE\\\" nil \\\"$MERGED\\\"))\"

Note that I have split this across several lines to increase readability and escaped the newline with \ so git config considers it as a single line.

I usually use emacsclient to edit e.g. commit messages. The above mergetool configuration unfortunately does not use emacsclient, and when I tried to get it to work with emacsclient I ran in to various problems including the fact that emacsclient returned right away.

But you just reminded me of that issue, so I might work on fixing that problem soon. However if someone else already found a solution that would be great of course ;-)

Algetic answered 30/11, 2009 at 9:54 Comment(6)
If you ever figure out getting this working with emacsclient, I'm eager to know.Umpire
@Umpire I have lost interest in the approach as I am using magit now, so I am afraid that won't happen. You should give magit a try too. It does support ediff, and makes using git much more fun.Algetic
Well I maintain Magit now :-) But I am afraid I have to say its Ediff support isn't all that great. I will work on that after the next release.Algetic
An alternative to this which is slightly nicer IMO is to put the script into, for instance, ~/.emacs.d/mergetool.el and replace $LOCAL etc with (getenv "LOCAL") then use the following as the mergetool command in .gitconfig: /usr/bin/env LOCAL=$LOCAL REMOTE=$REMOTE BASE=$BASE MERGED=$MERGED \ emacs --load ~/.emacs.d/mergetool.el @tarsius: Really looking forward to that! Magit is super awesome.Ancestral
FYI, this also works on Windows when using MSysGit, Emacs for Windows and diffutils for Windows.Brent
link is broken.Duotone
C
13

I use the following script as mergetool which works quite well.

#!/bin/bash

# test args
if [ ! ${#} -ge 3 ]; then
    echo 1>&2 "Usage: ${0} LOCAL REMOTE MERGED BASE"
    echo 1>&2 "       (LOCAL, REMOTE, MERGED, BASE can be provided by \`git mergetool'.)"
    exit 1
fi

# tools
_EMACSCLIENT=/usr/local/bin/emacsclient
_BASENAME=/bin/basename
_CP=/bin/cp
_EGREP=/bin/egrep
_MKTEMP=/bin/mktemp

# args
_LOCAL=${1}
_REMOTE=${2}
_MERGED=${3}
if [ -r ${4} ] ; then
    _BASE=${4}
    _EDIFF=ediff-merge-files-with-ancestor
    _EVAL="${_EDIFF} \"${_LOCAL}\" \"${_REMOTE}\" \"${_BASE}\" nil \"${_MERGED}\""
else
    _EDIFF=ediff-merge-files
    _EVAL="${_EDIFF} \"${_LOCAL}\" \"${_REMOTE}\" nil \"${_MERGED}\""
fi

# console vs. X
if [ "${TERM}" = "linux" ]; then
    unset DISPLAY
    _EMACSCLIENTOPTS="-t"
else
    _EMACSCLIENTOPTS="-c"
fi

# run emacsclient
${_EMACSCLIENT} ${_EMACSCLIENTOPTS} -a "" -e "(${_EVAL})" 2>&1

# check modified file
if [ ! $(egrep -c '^(<<<<<<<|=======|>>>>>>>|####### Ancestor)' ${_MERGED}) = 0 ]; then
    _MERGEDSAVE=$(${_MKTEMP} --tmpdir `${_BASENAME} ${_MERGED}`.XXXXXXXXXX)
    ${_CP} ${_MERGED} ${_MERGEDSAVE}
    echo 1>&2 "Oops! Conflict markers detected in $_MERGED."
    echo 1>&2 "Saved your changes to ${_MERGEDSAVE}"
    echo 1>&2 "Exiting with code 1."
    exit 1
fi

exit 0

To use it with `git mergetool' put the following in your git config:

[merge]
        tool = ediff

[mergetool "ediff"]
        cmd = /path/to/ediff-merge-script $LOCAL $REMOTE $MERGED $BASE
        trustExitCode = true

Additionally, you should check (in the script) the paths of the tools used and if the poor man's console detection works for you.

The script itself starts an emacs client (or emacs followed by an emacs client, -a "") and evals either ediff-merge-files-with-ancestor or ediff-merge-files if there's no base version (e.g. when merging two branches where the same path/file has been created independently).

After the emacs client has finished the merged file is checked for conflict markers. Should those be found, your work will be saved away to a temporary file, the script will exit with code 1 and git will restore the pre-mergetool contents of the merged file.

When there are no conflict markers present, the script exits with code 0 and git will regard the merge as successful.

Important: Setting the mergetool option trustExitCode to true as well as the post-edit check for conflict markers will not work if you start emacsclient with the --no-wait option.

Cavit answered 23/11, 2011 at 14:59 Comment(2)
$LOCAL $REMOTE $MERGED $BASE -> \"$LOCAL\" \"$REMOTE\" \"$MERGED\" \"$BASE\"Breaststroke
Also probably if [ ! $(git --no-pager diff --check ${_MERGED}) = 0 ]; then will make the check more robust.Bauble
C
8

Here's my setup, which works fairly well, using Emacs 23.3 at least. The trick I used was using (recursive-edit) in a hook such that emacsclient does not exit until an advised ediff-quit hook calls (exit-recursive-edit).

I used an advisted ediff-quit to ensure the exit-recursive-edit is the very last thing done.

There are also hooks to save the current frame and window state and restore it afterwards, and the hook makes the current frame fill the screen. You may wish to modify that, but I find merging full screen is the best way.

I've not solved the issue of aborting the ediff and making emacsclient return a non-zero exit.

Put in your gitconfig:

[mergetool "ediff"]
       cmd = emacsclient --eval \"(git-mergetool-emacsclient-ediff \\\"$LOCAL\\\" \\\"$REMOTE\\\" \\\"$BASE\\\" \\\"$MERGED\\\")\"
       trustExitCode = true
[mergetool]
    prompt = false
[merge]
    tool = ediff

Put in your .emacs or equivalent:

;;
;; Setup for ediff.
;;
(require 'ediff)

(defvar ediff-after-quit-hooks nil
  "* Hooks to run after ediff or emerge is quit.")

(defadvice ediff-quit (after edit-after-quit-hooks activate)
  (run-hooks 'ediff-after-quit-hooks))

(setq git-mergetool-emacsclient-ediff-active nil)

(defun local-ediff-frame-maximize ()
  (let* ((bounds (display-usable-bounds))
     (x (nth 0 bounds))
     (y (nth 1 bounds))
     (width (/ (nth 2 bounds) (frame-char-width)))
     (height (/ (nth 3 bounds) (frame-char-height))))
    (set-frame-width (selected-frame) width)
    (set-frame-height (selected-frame) height)
    (set-frame-position (selected-frame) x y)))

(setq ediff-window-setup-function 'ediff-setup-windows-plain)
(setq ediff-split-window-function 'split-window-horizontally)

(defun local-ediff-before-setup-hook ()
  (setq local-ediff-saved-frame-configuration (current-frame-configuration))
  (setq local-ediff-saved-window-configuration (current-window-configuration))
  (local-ediff-frame-maximize)
  (if git-mergetool-emacsclient-ediff-active
      (raise-frame)))

(defun local-ediff-quit-hook ()
  (set-frame-configuration local-ediff-saved-frame-configuration)
  (set-window-configuration local-ediff-saved-window-configuration))

(defun local-ediff-suspend-hook ()
  (set-frame-configuration local-ediff-saved-frame-configuration)
  (set-window-configuration local-ediff-saved-window-configuration))

(add-hook 'ediff-before-setup-hook 'local-ediff-before-setup-hook)
(add-hook 'ediff-quit-hook 'local-ediff-quit-hook 'append)
(add-hook 'ediff-suspend-hook 'local-ediff-suspend-hook 'append)

;; Useful for ediff merge from emacsclient.
(defun git-mergetool-emacsclient-ediff (local remote base merged)
  (setq git-mergetool-emacsclient-ediff-active t)
  (if (file-readable-p base)
      (ediff-merge-files-with-ancestor local remote base nil merged)
    (ediff-merge-files local remote nil merged))
  (recursive-edit))

(defun git-mergetool-emacsclient-ediff-after-quit-hook ()
  (exit-recursive-edit))

(add-hook 'ediff-after-quit-hooks 'git-mergetool-emacsclient-ediff-after-quit-hook 'append)
Co answered 22/12, 2010 at 19:4 Comment(3)
To make emacsclient return a non-zero exit code, you can adopt something like my answer here: superuser.com/questions/295156/… In my experience it has not been sufficient to kill the clients associated with the buffers; instead I generalized my loop to send error exit commands to all of the attached clients. (This is an easy generalization: just replace server-buffer-clients with server-clients.)Gebhart
Try package with-editor. It is designed for the usage of emacs or emacsclient as an $EDITOR, and allows you to use C-c C-c for zero exit code or C-c C-k for non-zero exit code.Durarte
*ERROR*: Symbol’s function definition is void: display-usable-boundsPauiie
M
6

Aside from the git vs bzr issue I identified in my comment above, I was able to confirm that you need to escape the parens as in

 cmd = emacs --eval "\\(ediff-merge-files-with-ancestor \"$LOCAL\" \"$REMOTE\" \"$BASE\" nil \"$MERGED\"\\)"

Note the double backslash characters. I kind of understand that they are needed (rather than a single one) to get through both the sh/bash quoting AND the emacs startup quoting mechanisms. I'll leave it to someone with a better grasp of Emacs and shell quoting to explain the gory details.

-pmr

Misshapen answered 30/11, 2009 at 3:52 Comment(0)
K
4

The elisp code in Viper3369's code (Using ediff as git mergetool) uses a function "display-usable-bounds" which doesn't exist. Since the hooks do a lot more than is strictly necessary, simply deleting all references to "display-usable-bounds" is sufficient to make it work for me. Good work! ;)

(Edit: I think I should post the modified emacs-lisp code:

;;
;; Setup for ediff.
;;
(require 'ediff)

(defvar ediff-after-quit-hooks nil
  "* Hooks to run after ediff or emerge is quit.")

(defadvice ediff-quit (after edit-after-quit-hooks activate)
  (run-hooks 'ediff-after-quit-hooks))

(setq git-mergetool-emacsclient-ediff-active nil)


(setq ediff-window-setup-function 'ediff-setup-windows-plain)
(setq ediff-split-window-function 'split-window-horizontally)

(defun local-ediff-before-setup-hook ()
  (setq local-ediff-saved-frame-configuration (current-frame-configuration))
  (setq local-ediff-saved-window-configuration (current-window-configuration))
  ;; (local-ediff-frame-maximize)
  (if git-mergetool-emacsclient-ediff-active
      (raise-frame)))

(defun local-ediff-quit-hook ()
  (set-frame-configuration local-ediff-saved-frame-configuration)
  (set-window-configuration local-ediff-saved-window-configuration))

(defun local-ediff-suspend-hook ()
  (set-frame-configuration local-ediff-saved-frame-configuration)
  (set-window-configuration local-ediff-saved-window-configuration))

(add-hook 'ediff-before-setup-hook 'local-ediff-before-setup-hook)
(add-hook 'ediff-quit-hook 'local-ediff-quit-hook 'append)
(add-hook 'ediff-suspend-hook 'local-ediff-suspend-hook 'append)

;; Useful for ediff merge from emacsclient.
(defun git-mergetool-emacsclient-ediff (local remote base merged)
  (setq git-mergetool-emacsclient-ediff-active t)
  (if (file-readable-p base)
      (ediff-merge-files-with-ancestor local remote base nil merged)
    (ediff-merge-files local remote nil merged))
  (recursive-edit))

(defun git-mergetool-emacsclient-ediff-after-quit-hook ()
  (exit-recursive-edit))

(add-hook 'ediff-after-quit-hooks 'git-mergetool-emacsclient-ediff-after-quit-hook 'append)
Koerner answered 6/1, 2011 at 9:27 Comment(0)
C
4

Thanks, it also works in xemacs, however the quoting as in the reply by pmr doesn't seem to work whereas I think the quoting in all the other replies is fine:

[mergetool "ediff"]
    cmd = xemacs -eval \"(ediff-merge-files-with-ancestor \\\"$PWD/$LOCAL\\\" \\\"$PWD/$REMOTE\\\" \\\"$PWD/$BASE\\\" nil \\\"$PWD/$MERGED\\\")\"
[merge]
    tool = ediff

I put this above code in ~/.gitconfig.

Callery answered 1/3, 2011 at 22:42 Comment(0)
E
3

Here's a variant of tarsius's setup. It handles when the ancestor file $BASE doesn't exist, and it allows you to abort the merge without trashing git's state about the conflict (by not automatically saving on exit). It also has newlines backslashed so that you can keep the formatting.

[mergetool.ediff]
    cmd = emacs --eval \" \
(progn \
  (setq ediff-quit-hook 'kill-emacs) \
  (if (file-readable-p \\\"$BASE\\\") \
      (ediff-merge-files-with-ancestor \\\"$LOCAL\\\" \\\"$REMOTE\\\" \
                                       \\\"$BASE\\\" nil \\\"$MERGED\\\") \
      (ediff-merge-files \\\"$LOCAL\\\" \\\"$REMOTE\\\" nil \\\"$MERGED\\\")))\"
Edva answered 25/10, 2010 at 21:24 Comment(0)
T
2

There is a way to use the ediff-merge-files-with-ancestor function with emacsclient.

The simplest one (for the GNU/Linux user) is to do a shell read from a pipe after the emacsclient call. An hook added in append to ediff-quit-hook (it must be run after ediff-cleanup-mess otherwise ediff session is not terminated properly) will shot a character in the pipe through shell-command.

A more refined one will use a semaphore.

And here arrives the Unix power user.

Then arrives the Emacs Guru (Stefan Monnier) and tells you that you can call

emacsclient --eval '(progn (ediff-merge-files-wit.......) (recursive edit))'

after adding

(throw 'exit )

somewhere at the end of ediff-quit-hook. No named pipe, no semaphores, just Emacs LISP. Simple, elegant and does not require weird tests to avoid using pipes or semaphores when they are not used.

Thank you Stefan!

Trilly answered 30/11, 2009 at 0:54 Comment(0)
A
2

For using Subversion's interactive merge tool instead of git see this post for some instructions to set this up.

Accord answered 29/4, 2011 at 19:21 Comment(2)
Merging with subversion? Sounds like a recipe for disaster :DKinna
My point wasn't to merge the git stuff using subversion, it was "Here's how to set this up for subversion; you might be able to apply this info to you're own situation." Also, searches for setting up an interactive ediff merge tool land here, so other people who ARE using subversion might find this useful.Accord
B
1

This was a valuable find for me. I have a small addition, since I use emacs desktop-save-mode:

[mergetool "ediff"]
cmd = emacs --no-desktop -eval \"(ediff-merge-files-with-ancestor \\\"$PWD/$LOCAL\\\" \\\"$PWD/$REMOTE\\\" \\\"$PWD/$BASE\\\" nil \\\"$PWD/$MERGED\\\")\"

and added the "(when" clause below, because I prefer a multi-frame ediff normally:

;;
;; Setup for ediff.
;;
(require 'ediff)

(when (or (not desktop-save-mode) (member "--no-desktop" command-line-args))
      (defvar ediff-after-quit-hooks nil
       ... (rest of TauPan's code here) ...
)
Batangas answered 6/4, 2011 at 23:52 Comment(1)
Glad that my question could be of help; Thanks for the improvements/changes.Kinna
A
1

This is a nice discussion about doing this usuing mercurial. It looks as though they have a wrapper script which alleviates the emacsclient issue: https://www.mercurial-scm.org/wiki/MergingWithEmacs

Accord answered 27/4, 2011 at 21:18 Comment(0)
B
1

Combining my favorite ideas from above. This configuration uses emacsclient and require therefore that an emacs is already running.

This also works for git difftool - it will invoke ediff-files. (When git difftool calls then the ancestor will be equal to the merged.)

In .gitconfig:

[mergetool "ec-merge"]
        prompt = false
        cmd = ec-merge "$LOCAL" "$REMOTE" "$BASE" "$MERGED"
        trustExitCode = true
[merge]
        tool = ec-merge
[difftool]
        prompt = false

In ~/bin/ec-merge (make sure ~/bin is in your PATH):

#!/bin/bash

set -e

LOCAL=$(readlink -f "$1")
REMOTE=$(readlink -f "$2")
BASE=$(readlink -f "$3")
MERGED=$(readlink -f "$4")

emacsclient --eval "(jcl-git-merge \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\")"

! egrep -q '^(<<<<<<<|=======|>>>>>>>|####### Ancestor)' "$MERGED"

In .emacs:

(server-start)

(defvar jcl-save-and-kill-buffers-before-merge nil
  "Normally if emacs already visits any of the concerned files (local,
remote, base or merged) ediff will ask it shall save and kill the
buffer.  If you always want to answer yes to this then set this 
to non-nil.")

(defun jcl-git-merge (local remote ancestor merged)
  (when jcl-save-and-kill-buffers-before-merge
    (dolist (file (list local remote ancestor merged))
      (setq file (file-truename file))
      (let ((old-buffer (and file (find-buffer-visiting file))))
        (when old-buffer
          (with-current-buffer old-buffer
            (save-buffer))
          (kill-buffer old-buffer)))))
  (prog1
      (if (string-equal ancestor merged)
          (progn
            (ediff-files local remote (list 'jcl-exit-recursive-edit-at-quit))
            (format "ediff compared %s and %s" local remote))
        (if ancestor
            (ediff-merge-files-with-ancestor local remote ancestor
                                             (list 'jcl-exit-recursive-edit-at-quit)
                                             merged)
          (ediff-merge-files local remote (list 'jcl-exit-recursive-edit-at-quit merged)))
        (format "ediff merged %s" merged))
    (recursive-edit)))

(defun jcl-exit-recursive-edit-at-quit ()
  (add-hook 'ediff-quit-hook (lambda () (throw 'exit nil)) t t))

Normally if emacs already visits any of the concerned files (local, remote, base or merged) ediff will ask it shall save and kill the buffer. If you like me always want to answer yes to this then add also this to your .emacs:

(setq jcl-save-and-kill-buffers-before-merge t)
Baksheesh answered 1/3, 2015 at 19:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.