vimdiffでより賢いアルゴリズム (patience, histogram) を使う

>>>>>>> 2019.01.05 追記ここから

vim 8.1.0360 以降では vim に diff の機能が内蔵されたため、この記事に書いたことは vim の設定のみで実現可能になりました。(@tsuyoshi_cho @fumiyas 情報ありがとうございました。)


~/.vimrc

set diffopt=internal,filler,algorithm:histogram,indent-heuristic


vim 内臓の diff を使う internal 指定は diffexpr がセットされていると無視されてしまうので注意してください。また、 algorithm:アルゴリズム名 に加えて、差分の位置を最適化する indent-heuristic も指定しておくのがおすすめです。

もし diffchar.vim をお使いの場合、 diffexpr がセットされないように let g:DiffExpr = 0 も書いておく必要があります(diffchar.vim については別途記事を書いています)。

<<<<<<< 2019.01.05 追記ここまで

vimdiff使ってますか?差分を取る際には非常に便利ですよね。git difftoolに設定して使っている人も多いと思います。しかしgit diffは差分計算のアルゴリズムを選択できますが、vimdiffはデフォルトでは差分計算のアルゴリズムを選択できません。

git diffの差分アルゴリズムには標準のもの以外にpatienceやhistogramがあり、より人間に読みやすい差分を表示してくれる「賢い」アルゴリズムになっています。それぞれgit diffコマンドのオプション--patienceや--histogramを付けるか、または~/.gitconfig設定ファイルにアルゴリズムを指定することで利用できます。

具体的なアルゴリズムの詳細は僕も詳しくないですが、patienceアルゴリズムは「ファイル内でユニークかつ比較ファイル同士で一致する行をなるべく"変化していない行"と認識する」よう働きます。また、histogramアルゴリズムは「patienceアルゴリズムの基本的ルールを継承しつつ高速化を図ったアルゴリズム」で、多くのファイルでpatienceアルゴリズムと同一の結果が得られるようです。また、histogramアルゴリズムはEclipseに統合されたGitクライアントEGit(が使うJGit)の標準アルゴリズムです。僕はコマンドラインツールのgit diffにおいてもhistogramアルゴリズムを使ってます。

さて、vimdiffにおいてもhistogramアルゴリズムを使いたいところです。vimの設定ではvimdiffの差分計算に使用するコマンドをdiffexprという設定値で指定できます。そして、git diffコマンドも--no-indexオプションを指定することでdiffコマンドと同じようにスタンドアロンツールとして利用できます。じゃあ、diffexprにgit diff --no-indexを指定すればいいのか・・・というと実はそれだけでは失敗します。

理由は、vimがdiffexprに設定したコマンドから受け付ける差分形式が、diffコマンドのnormal形式かed形式でなければならないからです(vimのヘルプにはed形式とだけ書いてあるのですが、normal形式も受け付けます)。そして、git diffコマンドでは直接diffコマンドのnormal形式やed形式で出力することができずunified形式のみに対応しています。

そこで、unified形式をnormal形式に変換するrubyスクリプトを書いて使うことにしました。以下がその変換スクリプトです。利用にはrubyが必要です。


~/bin/git-diff-normal-format

#!/usr/bin/env ruby

iwhite = ''
if (ARGV[0] == '-b')
iwhite = ARGV.shift.dup << ' '
end

diffout = `git diff --no-index --no-color -U0 #{iwhite}#{ARGV[0]} #{ARGV[1]}`
diffout.sub!(/\A.*?@@/m, '@@')
diffout.gsub!(/^\+/, '> ')
diffout.gsub!(/^-/, '< ')
diffout.gsub!(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@.*/) do
before_start = $1
before_size = $2
after_start = $3
after_size = $4

action = 'c'
if (before_size == '0')
action = 'a'
elsif (after_size == '0')
action = 'd'
end

before_end = ''
if (before_size && before_size != '0')
before_end = ",#{before_start.to_i + before_size.to_i - 1}"
end

after_end = ''
if (after_size && after_size != '0')
after_end = ",#{after_start.to_i + after_size.to_i - 1}"
end

"#{before_start}#{before_end}#{action}#{after_start}#{after_end}"
end
diffout.gsub!(/^(<.*)(\r|\n|\r\n)(>)/, '\1\2---\2\3')
print diffout


上記スクリプトを~/bin/git-diff-normal-formatとして保存し、~/binにPATHを通しておきます。そしたら、.vim設定ファイルに次の記述を追加します。


~/.vimrc

" diffのコマンド

set diffexpr=MyDiff()
function MyDiff()
let opt = ""
if &diffopt =~ "iwhite"
let opt = opt . "-b "
endif
silent execute "!git-diff-normal-format " . opt . v:fname_in . " " . v:fname_new . " > " . v:fname_out
redraw!
endfunction

最後に、git diffで使うアルゴリズムを指定します。下記記述ではgit difftoolで起動するコマンドにvimdiffを指定する設定も一緒に行っています。


~/.gitconfig

[diff]

tool = vimdiff
algorithm = histogram

これでvimdiffやgit difftoolを実行した時にhistogramアルゴリズムが使えるようになります(patienceを使いたい場合は、上記設定のalgorithmの値をpatienceに変えてください)。

では具体的な効果を見てみましょう。サンプルとして用意した2つの比較対象のファイルをvimでウィンドウを分割して並べたものが下の画像です。

スクリーンショット 2014-07-07 19.04.36.png

まずは、今回の設定をする前、デフォルトのアルゴリズムでvimdiffによる差分をとった結果が以下です。

スクリーンショット 2014-07-07 18.58.18.png

そして、今回の設定をした後の、histogramアルゴリズムでvimdiffによる差分をとった結果が以下です。

スクリーンショット 2014-07-07 18.59.32.png

こちらのほうが、より差分内容も分かりやすく、かつ実際に手で行った変更の操作に近いものになっていますね。

※ちなみにこのvimdiffの色設定は過去に書いたこの記事を参照。

自分のブログより転載。(一部をQiita用に修正)