git mergetoolに使えるツールとして、デフォルトで"emerge"というのが用意されており、Emacs使いはこれを使えばEmergeでマージが行えるわけだが、難点もある。ひとつは、新たなEmacsインスタンスを起動してしまうということだ。起動に無駄な時間が掛かるし、マージにあたって既存のセッションで開いているファイルをその場で参照できないのは不便だろう。もっとも、これはemacsclientを使うようにして、Emergeの呼び出し方を少し直せば済む。もうひとつは、EmergeではなくよりモダンなEdiffを使いたいということだが、これは思ったほど簡単ではないのでわざわざこうして記事を書くことになった。

というのも、Emergeにはemerge-files-with-ancestor-commandという便利なものがあり、「マージが終わったらマージ結果を保存して即終了」ということが一発で出来てしまうのだが、Ediffの方にはそういうものがない。こいつはマージを終了してもろくに片付けもせず、全部ほったらかしという行儀の悪さだ。
そういうわけで、まずはEdiffのediff-merge-{files,buffers}が終わったら起動前のウィンドウ設定を復元するようにしてみる。これは比較的簡単。[2013-01-23改訂:フックにappendフラグを指定]

(eval-after-load "ediff"
  '(progn
     ; save and restore window configuration
     (defvar my-ediff-saved-window-configuration nil "Saved window configuration for ediff")
     (defun my-ediff-save-window-configuration ()
       (setq my-ediff-saved-window-configuration (current-window-configuration)))
     (add-hook 'ediff-before-setup-hook 'my-ediff-save-window-configuration)
     (defun my-ediff-restore-window-configuration ()
       (set-window-configuration my-ediff-saved-window-configuration))
     (add-hook 'ediff-suspend-hook 'my-ediff-restore-window-configuration t)
     (add-hook 'ediff-quit-hook 'my-ediff-restore-window-configuration t)))

次に、Ediffが新たに開いたファイルのバッファをマージ終了時に自動的に閉じる、および、マージ終了時に自動的にセーブしつつEmacsフレームを閉じる「バッチ版ediff-merge-files-with-ancestor」を加える。[2013-01-23改訂:ediff-filesに影響を与えるなどおかしかったので大幅修正; 続きはGitHubで]

(eval-after-load "ediff"
  '(progn
     ; batch mode (for use from git mergetool etc.)
     (ediff-defvar-local my-ediff-batch-mode-p nil "True if in batch mode")
     (ediff-defvar-local my-ediff-close-on-quit nil "True if the buffer should be closed on quit.")
 
     (defun my-ediff-batch-mode (&optional mode)
       (ediff-with-current-buffer ediff-buffer-A
                                  (case mode
                                    (set
                                     (setq my-ediff-batch-mode-p t))
                                    (unset
                                     (prog1 my-ediff-batch-mode-p
                                       (setq my-ediff-batch-mode-p nil)))
                                    (t
                                     my-ediff-batch-mode-p))))
 
     (defadvice ediff-find-file (around
                                 mark-newly-opened-buffers
                                 (file-var buffer-name &optional last-dir hooks-var)
                                 activate)
       (let* ((file (symbol-value file-var))
              (existing-p (and find-file-existing-other-name
                               (find-buffer-visiting file))))
         ad-do-it
         (or existing-p
             (ediff-with-current-buffer (symbol-value buffer-name)
                                        (setq my-ediff-close-on-quit t)))))
 
     (defun my-ediff-save-merge ()
       (if (my-ediff-batch-mode)
           (let ((file ediff-merge-store-file))
             (if file
                 (ediff-with-current-buffer ediff-buffer-C
                   (set-visited-file-name file t)
                   (save-buffer))))
         (ediff-maybe-save-and-delete-merge)))
 
     (remove-hook 'ediff-quit-merge-hook 'ediff-maybe-save-and-delete-merge)
     (add-hook 'ediff-quit-merge-hook 'my-ediff-save-merge)
 
     (defadvice ediff-cleanup-mess (around
                                    support-batch-mode
                                    ()
                                    activate)
       (let ((batch-p (my-ediff-batch-mode 'unset))
             (buffers (list ediff-buffer-A ediff-buffer-B ediff-ancestor-buffer))
             (buffer-C ediff-buffer-C))
         ad-do-it
         (dolist (buffer buffers)
           (ediff-with-current-buffer buffer
             (and my-ediff-close-on-quit (kill-buffer))))
         (when batch-p
           (ediff-kill-buffer-carefully buffer-C)
           (delete-frame))))
 
     (defun ediff-merge-files-with-ancestor-in-batch-mode
       (file-A file-B file-ancestor &optional startup-hooks merge-buffer-file)
       (ediff-merge-files-with-ancestor
        file-A file-B file-ancestor
        (cons (function (lambda () (my-ediff-batch-mode 'set))) startup-hooks)
        merge-buffer-file))))
 
(autoload 'ediff-merge-files-with-ancestor-in-batch-mode "ediff")

これで、~/.gitconfigにこんな風に書けばgit mergetoolで瞬時にEdiffが起動し、マージが済んだらq, yで即コマンドラインに帰ってくる。既存Emacsセッションにゴミバッファも残らない。

[merge]
	tool = ediff
[mergetool "ediff"]
	cmd = emacsclient -a \"\" -t -e \"(ediff-merge-files-with-ancestor-in-batch-mode \\\"$LOCAL\\\" \\\"$REMOTE\\\" \\\"$BASE\\\" nil \\\"$MERGED\\\")\"

あとは、たとえばラッパースクリプトを用意して、「<<<<<<<」などの行が残っていたらマージ失敗とするとか、好きに改良すると良い。

参考記事

Tags : ,
Categories : Tech