彷徨えるフジワラ

年がら年中さまよってます

"hg log" にファイル名を指定するなら "-k" で

忙しい人は以下のことだけ覚えれば大丈夫(複数指定に関する条件を追加@2010/05/03)。

特定ファイルに関する全履歴を見たい場合は、"hg log filename" ではなく "hg log -k filename" を使うこと。但し、ファイル/ディレクトリの複数指定が必要な場合は、1.4 版以降が必須。

これ以降は技術的な話。

仕事のリポジトリで "hg log filename" の出力するログを見ていたら、コミットした筈のリビジョンが表示されていないことに気付く。

客先への提供後だったので、すわコミットし忘れか?と一瞬血の気が引いたけど、コミットログに入れておいたバグ識別子を使って "hg log -k bugid" を実行すると、先のファイル名指定形式では表示されなかったリビジョンが、何の問題も無く表示される。

"-v" 付きならファイル名一覧に対象ファイルが列挙されているし、"-p" 付きなら期待通りの差分が表示される。つまりは、更新履歴そのものは壊れていない模様。
mercurial/command.py の log コマンド定義によると、リビジョンを順次走査する処理の肝になる部分は mercurial/cmdutil.py で定義されている walkchangerevs() が行っているようなので、あちこちに「デバッグ print」を埋め込んで調査すること暫し....

ファイル名指定があった場合には、当該ファイルの更新情報を保持している filelog を順次走査しているらしい。この filelog の走査は、全データの一括読み込みによる性能劣化/メモリ消費等を避けるため(多分)の分割読み込み等を行っているので、思いのほか複雑な実装になっている。

問題を簡単にするために、対象ファイルの filelog 内容をダンプする簡易プログラム(dump_filelog.py)をでっち上げる。

import sys
from mercurial import ui, hg

u = ui.ui()
repo = hg.repository(u, ".")

filelog = repo.file(sys.argv[1])
for r in filelog:
    print '%d %d' % (r, filelog.linkrev(r))

このプログラムは、実行時第1引数(argv[1])で指定されたファイル名に対応する filelog が保持する各エントリに対して、「filelog 内インデックス」と「対応する repo 内リビジョン番号」をダンプするもの。

で、実際に問題のあるファイルに対してこれを実行してみると:

$ python dump_filelog.py filename
    :
    :
    :
209 2734
210 2746
211 2759
$

という結果。これだけ見ても意味は無いので補足すると、このファイルは 2759 以後のリビジョンでも変更を加えている(そしてそのリビジョンが表示されない)ので、filelog からは当該リビジョンが見えないのは当然の挙動ということに。

でも、リビジョンを指定した "hg log" なら、"-v" や "-p" も期待通りの出力をする、ってことは、履歴情報側からの参照は辻褄が合ってる、ってことだよね?それってどういうこと?

そこで今度は、リビジョン+ファイル名指定から filelog 内インデックスを引く簡易プログラム(lookup_filerev.py)を作ってみた。

import sys
from mercurial import ui, hg

u = ui.ui()
repo = hg.repository(u, ".")

ctx = repo[sys.argv[1]]
fctx = ctx[sys.argv[2]]
print fctx.filerev()

ファイル名指定付きの "hg log" では表示されないリビジョン(2769 および 2767)に対して、上記のプログラムを実行してみると:

$ python lookup_filerev.py 2769 filename
211
$ python lookup_filerev.py 2767 filename
211
$

先の filelog ダンプにより、リビジョン 2759 に対応する filelog 内インデックスが 211 であることがわかっているので、表示されないリビジョンは、全て同一の filelog 内インデックスを指していることに....。

あ、そうか!表示されない2つのリビジョンを含む、このファイルの直近の修正は全て、別々の名前付きブランチ上で同じ修正を適用したものだ!

っつーことは、(1) 当該状況で filelog への追記処理がバグっているか、(2) 同一変更は統合されるのが filelog の仕様、のどちらかってことか。

でも (1) なら修正が必要だろうし、(2) でも "hg log" にユーザが期待する挙動としては問題じゃねぇ?ということで、本家の開発者 ML に投げてみた

微妙に話が面倒なのは、仕事で使っている環境は未だに Mercurial 0.9.8 あたりを使っているため、この問題が単に古さに起因する可能性があったから(確認したら、0.9.5 だった .... orz @2010/05/03)。

最新版を使った bundle + unbundle での復旧の試みも効果が無かったので、多分関係無いとは思ってたけど、そこはそれ、実装屋としては気になるわけで(笑)。

で、ML でのやり取りで得られた結論としては、(2) の「同一変更は統合されるのが filelog の仕様」ということらしい。

"hg log" の挙動としてはどーよ?という件に関しては、冒頭で書いた様に「"hg log -k filename" を使え」というのが回避策。頑張れば対応できなくは無いけど、実行性能とか考えると無理っぽい、というのは、まぁ、実装内情を知っていると納得できるけどね。

どこかの版から "hg log" での "-k" 実装部分が:

        if opts.get('keyword'):
            for k in [kw.lower() for kw in opts['keyword']]:
                if (k in ctx.user().lower() or
                    k in ctx.description().lower() or
                    k in " ".join(ctx.files()).lower()):
                    break
            else:
                return

という具合にファイル名に関する合致判定を含むようになったのを見て、ファイル名指定をすれば良い筈なのに、何の意味があるんだ?と思っていたけど、やっとバラバラのピースがハマッた感触が。

※ 2010/05/03 追記 ここから

と思って、変更履歴を確認したら、指定キーワードのファイル名合致判定は 1.0 版前から含まれていた。但し、「合致」判定の強度が 1.4 で変更されていた。

1.4 版以前でのキーワード判定処理は:

        if opts.get('keyword'):
            changes = get(rev)
            miss = 0
            for k in [kw.lower() for kw in opts['keyword']]:
                if not (k in changes[1].lower() or
                        k in changes[4].lower() or
                        k in " ".join(changes[3]).lower()):
                    miss = 1
                    break
            if miss:
                continue

以上の様な感じになっていて、複数キーワードが指定された場合、全てのキーワードが合致しないリビジョンは表示対象から外れる按配。なので、複数キーワード=複数ファイル/ディレクトリを指定する必要がある場合は、1.4 版以降が必須。

※ 2010/05/03 追記 ここまで

ちなみに、ML に投げる前に、別リビジョンにおける同一修正で filelog エントリが統合されるか試してみたのだけど、その時は統合される様子が無かったので「filelog エントリの統合は無い」と判断。そうなると「同一変更は統合されるのが filelog の仕様」ってのは辻褄合わなくねぇ?と思っていたのだけど、このエントリを書いているうちに気が付いた。

definitive 本で言うところのキーフレームに相当するリビジョン("4.2.3 Fast retrieval" 参照)を跨ぐようなケースでは、filelog の統合が行われないのではないかな?

今回統合されていた filelog エントリは、連続してないとは言え、リビジョンが極めて近接している(2759, 2767, 2769)のに対して、エントリ統合の実験を実施したのは、リビジョンが 2790 を超えてからだったので、この間にキーフレームが挟まっているのではなかろうか?