Mercurialのextdiffツールを自作する

このエントリーはMercurial Advent Calendar 2013の4日目です。

概要

Mac環境のdiffツールで満足な行内差分を得ることができなかったので、extdiff用のツールを自作しました。
個人的な要件として、日本語のプレーンテキストの差分を見たいのですが、これを満たすツールがMacだと意外とありません。マルチバイト文字と行の折り返しで躓くことが多いです。

環境

ソースコード

Bitbucketで公開しています。
cudiff.py

解説

まずは.hgrcにプログラムを登録します。

[extdiff]
cmd.cudiff = /your/directory/cudiff.py

コマンドラインでhg cudiffと打ち込むとcudiff.pyが実行されます。

プログラムには引数が二つ渡されます。比較対象となるファイル(またはディレクトリ)のパスです。
Mercurial側で比較対象のファイルを一時ディレクトリに生成してくれるので、ツール側はそのパスを引数として受け取り、普通にファイルの内容を比較すればいいようになっています。
引数は絶対パスだったり相対パスだったりします。カレントディレクトリが一時ディレクトリに移動しているので、そこからの相対パスとして処理できなければなりません。
複数ファイルを比較する場合、必要なファイルだけが入った一時ディレクトリが2つ生成されて、そのディレクトリのパスが引数となります。
hg cudiffのオプションはMercurial側で解釈されます。たとえばhg cudiff -c 9999とした場合、リビジョン9999とそのparentを比較するための一時ディレクトリが生成され、プログラムにはそのパスが渡されます。

cudiff.pyの説明に移りましょう。

比較処理はdiffコマンドにブン投げました。

proc = subprocess.Popen(['diff', '-r', '-u', sys.argv[1], sys.argv[2]])

-rオプションで、ディレクトリを再帰的に比較します。
-uオプションでunified diff形式で出力します。

サブプロセスの標準出力を受け取り、一行一行解析してside-by-side形式で表示するHTMLを組み立てていきます。このあたりは自前で実装しました。

行内差分の抽出は以下のようにします。

class CUDiffer:
    def compare(self, a, b):
        sm = difflib.SequenceMatcher(None, a, b)
        for tag, a1,a2, b1,b2 in sm.get_opcodes():
            if   tag == 'delete':
            elif tag == 'insert':
            elif tag == 'replace':
            elif tag == 'equal':

Python標準ライブラリのdifflibDiffer.compare()メソッドを基にしています。Python本体ソースコードLib/difflib.pyにあります。
SequenceMatcherで文字列a,bの行内差分の位置を取得して、<span>タグで囲ってCSSで色を付けます。
今回、プログラムを作成する言語はどれを選んでもよかったのですが、このdifflib目当てでPythonに決めました。

生成したHTMLをテンポラリファイルに書き出して、Webブラウザで開きます。

temp_file = tempfile.mkstemp()
webbrowser.open('file://' + temp_file[1])
os.remove(temp_file[1])

裏でファイルを削除してコマンドは終了します。
ユーザーの操作はコマンド入力とブラウザのタブを閉じることだけです。

表示はこのようになります。

f:id:toruot:20131204225025p:plain

試してみたdiffツール

既存のツールもあれこれ試しましたが、しっくりくるものはありませんでした。

SourceTree

http://www.sourcetreeapp.com/
行内差分は見られません。同じAtlassianのBitbucketならWeb上で行内差分が見られるのですが、この扱いの差は一体。

FileMerge.app

[extdiff]
cmd.odiff = opendiff

opendiffコマンドで、MacのXcodeに標準で付属しているFileMerge.appを起動させることができます。
なかなかいいのですが、マルチバイト文字の行内差分表示がズレることがあるのが難です。
また、ファイルに非ascii文字があると確認ダイアログが出ます。これは抑制することができないようで、非常に鬱陶しいです。

PyCharm

[extdiff]
cmd.pdiff = /Applications/PyCharm CE.app/Contents/MacOS/pycharm
opts.pdiff = diff

Python IDEのPyCharmは、コマンドラインからdiffツールとして呼び出すことが可能です。
Running PyCharm as a Diff or Merge Command Line Tool
行内差分も出るのですが、スペース区切りで判断しているようで、日本語の文章が一単語と見なされるのが難です。
また、カレントディレクトリを考慮しないらしく、extdiffからの引数が相対パスだと、ファイルが見つからずエラーになります。
PyCharmを起動した状態で実行しないと、コマンドライン上にエラーが出ます。一応動きますが注意が必要です。

vimdiff

[extdiff]
cmd.vimdiff = vimdiff

Macだとはじめから入っているvimですが、diffツールとして使えるとは知りませんでした。
起動時にnowrapになるため、~/.vimrcに以下を記述します。

au FilterWritePre * if &diff | set wrap | endif

行内差分の表示はかなりいいのですが、折り返して物理行数に差がでると、左右のペインがズレていくのが難です。

docdiff

[extdiff]
cmd.docdiff = docdiff
opts.docdiff = --utf8 --lf --digest --format=tty --display=multi

Rubyで書かれたdiffツールです。
http://hisashim.org/2011/02/23/docdiff-040.html
差分抽出はいいのですが、結果の表示が好みでないので避けていました。Rubyを書けばいろいろできるはずですが、先にcudiffができてしまいました。

wdiff, dwdiff

[extdiff]
cmd.wdiff = wdiff
cmd.dwdiff = dwdiff
opts.dwdiff = -c -C4 -d "、。「」\n"

diffコマンドの親戚のようなものです。Homebrewで入れて試しました。
日本語の文章が一単語とみなされるのが難です。dwdiffで、-Pしてみたり、-dで改行文字などを追加すると、少しはよくなりますが。

kdiff3

[extdiff]
cmd.kdiff = /Applications/kdiff3.app/Contents/MacOS/kdiff3

何が不満だったか確かめようと思い実行したら、エラーで動きませんでした。

p4merge

[extdiff]
cmd.fdiff = /Applications/p4merge.app/Contents/MacOS/p4merge

マルチバイト文字の扱いに難があります。

diffc

[extdiff]
cmd.cdiff = /your/directory/diffc

Pythonで書かれたdiffツールです。
http://code.google.com/p/diffc/
文字化けしてしまいました。

その他のdiffツール

WinMerge

[extdiff]
cmd.wdiff = C:\Program Files\WinMerge\WinMergeU.exe
opts.wdiff = /r

会社はWindows環境なのでWinMergeを使っています。Mac版もあればいいんですどねえ。

Kaleidoscope

http://www.kaleidoscopeapp.com/
有償のツールは試してないのですが、これは良さそうですね。バージョン管理システムとの連携機能が組み込まれているようです。

終わりに

普段使いのツールを自分で書くというのは久しぶりで、とても楽しかったです。MercurialPythonに感謝。

明日の担当は flyingfoozy さんです。

2013/12/08(日)追記

ちょっと改良しました。
Mercurialのextdiffツールを改良する