blogこの広告は、90日以上更新していないブログに表示しています。2013-12-08Mercurialのextdiffツールを改良する先日、自作のextdiffツールcudiffを公開しました。2013これにはひとつ心残りがありました。行内差分を比較するペアの決定方法が単純で、関係ない行を比較してしまうことです。行の追加と行の変更が続くと、以下のように残念なことになります。None,'bbbb']となっています。None,'xxxx'],['bbb','bbbb']となってほしいところです。difflibのドキュメントをぼんやり眺めていたら、ndiffがまさに期待する通りに動作していました。+さっそくdifflibのソースコードLib/difflib.pyを見ると、以下のようになっていました。b)SequenceMatcherは文字列を引数にとるものだと思っていたので、配列を入れていることに面食らったのですが、考えてみれば文字列とは「文字の配列」でもあるわけで、文字列の比較ができるなら「文字列の配列」の比較もできるわけですね。Differクラスを読んだところ、Differ.compare()で行レベルの比較をして、変更のある行についてはDiffer._fancy_replace()で文字レベルの比較をしている、ということがわかりました。_fancy_replaceには、どの行とどの行を比較するかペアを決めるアルゴリズムがあり、この機能を組み込むため移植することにしました。行レベルの差分はdiffコマンドに丸投げしているので、compareの処理は特に必要ありません。ちなみに、difflibにはもともとside-by-side形式のHTMLを出力する機能があるのですが、ソースコードを見たところ嫌な予感がしたので再利用するのは避けました。処理の流れは軽くしか追ってないのですが、HtmlDiffはndiffの出力結果をわざわざパースしています。diff_lines_iteratorを解析する長い処理...ndiffはDifferを使い、DifferはSequenceMatcherの解析データをもとに出力文字列を生成しています。SequenceMatcherの解析データを直接利用すれば、Differの出力する文字列を解析する手間はかからないわけです。diffをわざわざパースするプログラム書いてる私が言うのもなんですが。移植にあたり、_fancy_replaceのcutoffの値を変えました。この値が高すぎると、行内差分が見たいのにまるごと置き換わったことになってしまいますし、低すぎると、内容はまったく関係の無い行なのに句読点が一致しているといった意味のない行内差分を見せられることになります。0.75なのですが、高すぎると感じたので0.59にしてみました。値が0.6を超えると、シーケンスがよく似ていることを示します」とあります。移植は成功し、めでたく気の利いたdiffが見れるようになりました。変更したソースコードはBitbucketで公開しています。・cudiff.pydifflibのソースコードと翻訳ドキュメントを公開してくれている方々に感謝。2013の4日目です。概要Mac環境のdiffツールで満足な行内差分を得ることができなかったので、extdiff用のツールを自作しました。個人的な要件として、日本語のプレーンテキストの差分を見たいのですが、これを満たすツールがMacだと意外とありません。マルチバイト文字と行の折り返しで躓くことが多いです。25.0.1ソースコードBitbucketで公開しています。・cudiff.py解説まずは.hgrcにプログラムを登録します。cudiffと打ち込むとcudiff.pyが実行されます。プログラムには引数が二つ渡されます。比較対象となるファイル(またはディレクトリ)のパスです。Mercurial側で比較対象のファイルを一時ディレクトリに生成してくれるので、ツール側はそのパスを引数として受け取り、普通にファイルの内容を比較すればいいようになっています。引数は絶対パスだったり相対パスだったりします。カレントディレクトリが一時ディレクトリに移動しているので、そこからの相対パスとして処理できなければなりません。複数ファイルを比較する場合、必要なファイルだけが入った一時ディレクトリが2つ生成されて、そのディレクトリのパスが引数となります。cudiffのオプションはMercurial側で解釈されます。9999とした場合、リビジョン9999とそのparentを比較するための一時ディレクトリが生成され、プログラムにはそのパスが渡されます。cudiff.pyの説明に移りましょう。比較処理はdiffコマンドにブン投げました。sys.argv[2]])-rオプションで、ディレクトリを再帰的に比較します。diff形式で出力します。サブプロセスの標準出力を受け取り、一行一行解析してside-by-side形式で表示するHTMLを組み立てていきます。このあたりは自前で実装しました。行内差分の抽出は以下のようにします。'equal':Python標準ライブラリのdifflibのDiffer.compare()メソッドを基にしています。Python本体ソースコードのLib/difflib.pyにあります。SequenceMatcherで文字列a,bの行内差分の位置を取得して、<span>タグで囲ってCSSで色を付けます。今回、プログラムを作成する言語はどれを選んでもよかったのですが、このdifflib目当てでPythonに決めました。生成したHTMLをテンポラリファイルに書き出して、Webブラウザで開きます。temp_file[1])os.remove(temp_file[1])裏でファイルを削除してコマンドは終了します。ユーザーの操作はコマンド入力とブラウザのタブを閉じることだけです。表示はこのようになります。試してみたdiffツール既存のツールもあれこれ試しましたが、しっくりくるものはありませんでした。SourceTree・http://www.sourcetreeapp.com/行内差分は見られません。同じAtlassianのBitbucketならWeb上で行内差分が見られるのですが、この扱いの差は一体。opendiffopendiffコマンドで、MacのXcodeに標準で付属しているFileMerge.appを起動させることができます。いいのですが、マルチバイト文字の行内差分表示がズレることがあるのが難です。ファイルに非ascii文字があると確認ダイアログが出ます。これは抑制することができないようで、非常に鬱陶しいです。IDEのPyCharmは、コマンドラインからdiffツールとして呼び出すことが可能です。Tool行内差分も出るのですが、スペース区切りで判断しているようで、日本語の文章が一単語と見なされるのが難です。カレントディレクトリを考慮しないらしく、extdiffからの引数が相対パスだと、ファイルが見つからずエラーになります。PyCharmを起動した状態で実行しないと、コマンドライン上にエラーが出ます。一応動きますが注意が必要です。vimdiffMacだとはじめから入っているvimですが、diffツールとして使えるとは知りませんでした。起動時にnowrapになるため、~/.vimrcに以下を記述します。endif行内差分の表示はかなりいいのですが、折り返して物理行数に差がでると、左右のペインがズレていくのが難です。--display=multiRubyで書かれたdiffツールです。・http://hisashim.org/2011/02/23/docdiff-040.html差分抽出はいいのですが、結果の表示が好みでないので避けていました。Rubyを書けばいろいろできるはずですが、先にcudiffができてしまいました。「」\n"diffコマンドの親戚のようなものです。Homebrewで入れて試しました。日本語の文章が一単語とみなされるのが難です。dwdiffで、-Pしてみたり、-dで改行文字などを追加すると、少しはよくなりますが。/Applications/kdiff3.app/Contents/MacOS/kdiff3何が不満だったか確かめようと思い実行したら、エラーで動きませんでした。/Applications/p4merge.app/Contents/MacOS/p4mergeマルチバイト文字の扱いに難があります。/your/directory/diffcPythonで書かれたdiffツールです。・http://code.google.com/p/diffc/文字化けしてしまいました。/r会社はWindows環境なのでWinMergeを使っています。Mac版もあればいいんですどねえ。Kaleidoscope・http://www.kaleidoscopeapp.com/有償のツールは試してないのですが、これは良さそうですね。バージョン管理システムとの連携機能が組み込まれているようです。終わりに普段使いのツールを自分で書くというのは久しぶりで、とても楽しかったです。MercurialとPythonに感謝。2013/12/08(日)追記ちょっと改良しました。スクリプトバックアップ代わりに。"だった場合に誤判定する。SubversionからDVCSへの乗り換えを勧める言説は数多くありますが、以前の私はどれを読んでもピンと来ませんでした。利点はいろいろあるのだろうけど、そこまでクリティカルなのかな、と。自分でMercurialを触るようになると、「確かにこれはSubversionには戻れない」と感じるようになりました。この感覚はどこから来るのか。思うに、それはバージョン管理のモデルの違いです。私がSubversionを使う際のメンタルモデルは「ディレクトリツリー」です。Mercurialを使う際のメンタルモデルは「リビジョングラフ」です。本来、この二つのモデルに優劣はないでしょうが、ことバージョン管理においてはリビジョングラフの方が都合がよいのです。Subversionはリポジトリの中にディレクトリツリーがあります。いったディレクトリのことです。これはシステムによって規定されるわけではなく、標準的な構成として広く知られている、いわば「お約束」です。ブランチやタグの作成とはすなわちディレクトリのコピーです。これもシステムに認識されるわけではなく、「/trunkを/branches/下にコピーして開発を進めるのがブランチである」、「/tags/下にコピーして、以後編集しないものがタグである」といったお約束があるだけです。ここで言うディレクトリツリーとは、バージョン管理対象となっているファイル・ディレクトリのツリーとは異なる概念です。(ただし、シームレスにつながっています。ブランチやタグを作るのと同じコマンドで、任意のサブディレクトリ(たとえば/trunk/foo/bar)をコピーすることも可能です。)Subversionでも、TortoiseSVNなどのツールを使えばリビジョングラフを見ることができます。ディレクトリツリーをもとにグラフを作っているために、Mercurialを知った今にして思えば物足りないところがあります。リビジョングラフの分岐はありますが、合流がありません。そのままではマージしたブランチの履歴をたどることができないため、ファイル・ディレクトリに付属する属性情報にmergeinfoを書き込んでいます。ひとつ、ブランチを作成した時点でリビジョングラフが分岐してしまいます。このため、ファイル単位でリビジョングラフを見ると、ブランチ内で一度も更新されてないのに分岐していて不自然です。Subversionのディレクトリツリーが、ブランチを扱うのに向いてないことの査証といえるでしょう。リポジトリのクローンが複数あり、それぞれに独立したコミットが追加されたとして、それを統合することを考えてみましょう。Mercurialでは、マルチプルヘッドが増えることを厭わなければ機械的に統合が可能です。Subversionは機械的な統合ができません。リポジトリ全体で一意なリビジョン番号を持っており、別のクローンから来たコミットを置く場所がありません。事前にマージしてどちらかを棄却しなければ統合は不可能です。単一のコミットならばどうにかなるかもしれませんが、コミット群だったりブランチができていたりマージされていたりすると、絶望的に面倒でしょう。Subversionが集中型なのはもともとそれを意図して作られたからというのもあると思いますが、ディレクトリツリーでは分散型に対応できないからである、という面もあると思います。全てはつながっています。ディレクトリツリーであること、マージがやりにくいこと、集中型であること。リビジョングラフであること、マージがやりやすいこと、分散型であること。リビジョングラフの方がバージョン管理にふさわしいメンタルモデルであり、ディレクトリツリーによる制約や回り道から解放されると感じます。(ただ、ディレクトリツリーの利点として、普段使っているファイルシステムのメンタルモデルを流用できるので始めるコストが低い、というのはあるかもしれません。Mercurialは、リビジョングラフという、ファイルシステムとは関係のないメンタルモデルを一から構築する必要があるため、学習コストが比較的高い気がします。)私はバージョン管理システムをそれほどハードに使っているわけではなく、Subversionでもマージ地獄にハマったりした経験はありません。それでもMercurialの方がバージョン管理をキメてる爽快感があって好きです。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。