(利用状況次第では) Mercurial 2.5 版以上の利用を推奨
簡単にまとめると:
リビジョンセット記述(特に "
or"/"|"/"+" 結合)を多用する場合は、評価結果が間違っている可能性があるため、Mercurial バージョン2.5以上の利用を推奨
最新の情報に関しては、こちらのエントリを参照してください。
以下、事の顛末に関する詳細なので、Mercurial の内部的な話に興味の無い方は読み飛ばしてください。
"R or R^1" ≠ "R^1 or R" ??
事の発端は、リビジョンセット記述の挙動確認のための、以下の様な "hg log" 実行:
$ hg log log --template "{rev}\n" --rev "4 or 4^1"
4
$
え?????4の第1親("4^1")を or 結合してるんだから、リビジョンは2つ表示されなくちゃ駄目でしょ?
試しに、要素単独で問い合わせを実行してみると:
$ hg log log --template "{rev}\n" --rev "4^1"
3
$
これは期待通りの結果を返す。ふと思い付いて、式の順序を入れ替えてみると:
$ hg log log --template "{rev}\n" --rev "4^1 or 4"
3
4
$
うわぁー、動いちゃったか。これは明らかにバグだなぁ。ということで、デバッグモードに突入することに。
リビジョンセット評価の処理を掘り下げる
以前調べた『40桁ハッシュ値絡みでリビジョンセットが機能しない』件では、hg logとhg debugrevspecとで、リビジョンセットの解釈が異なっていた経験があったので、とりあえずはhg debugrevspecで2つのリビジョンセット記述の解釈状況を確認してみることに。
$ hg debugrevspec -v "4 or 4^1"
(or
('symbol', '4')
(parent
('symbol', '4')
('symbol', '1')))
4
$ hg debugrevspec -v "4^1 or 4"
(or
(parent
('symbol', '4')
('symbol', '1'))
('symbol', '4'))
3
4
$
うーむ、解釈後の構文ツリーを見る限りでは、どちらの式も妥当な感じに解釈されているっぽいなぁ。そうすると、構文ツリー構築後の要素評価の問題か?
"or" 結合を行っている以下の処理に:
def orset(repo, subset, x, y): xl = getset(repo, subset, x) s = set(xl) yl = getset(repo, [r for r in subset if r not in s], y) return xl + yl
各要素のデバッグプリントを追加してみることに。
def orset(repo, subset, x, y): xl = getset(repo, subset, x) print '[DBG] subsetx=%s, x=%s, xl=%s' % (str(subset), str(x), str(xl)) s = set(xl) yl = getset(repo, [r for r in subset if r not in s], y) subsety = [r for r in subset if r not in s] print '[DBG] subsety=%s, y=%s, yl=%s' % (str(subsety), str(y), str(yl)) return xl + yl
元の処理で、第2項を評価する際の subset として、起動時に指定された subset ではなく、"[r for r in subset if r not in s]" を指定しているのは、既に第1項で引き当てられたリビジョン(= r in s なリビジョン)は、第2項の評価結果から除外して構わないため。
さて、駄目な結果を返す "4 or 4^1" に対して実行してみると:
[DBG] subsetx=[0, 1, 2, 3, 4], x=('symbol', '4'), xl=[4]
[DBG] subsety=[0, 1, 2, 3], y=('parent', ('symbol', '4'), ('symbol', '1')), yl=[]
あれ? "4^1" が空(=該当リビジョン無し)で評価されてるぞ?
"4^1" の評価処理を遡って行くと、parentspec() ⇒ symbolset() ⇒ stringset() に行き着く。stringset() では、引き当てたリビジョンが指定された subset に含まれない場合は、該当リビジョン無しとしている。
def stringset(repo, subset, x): x = repo[x].rev() if x == -1 and len(subset) == len(repo): return [-1] if len(subset) == len(repo) or x in subset: return [x] return []
で、parentspec() の処理を確認すると:
ps = set() cl = repo.changelog for r in getset(repo, subset, x): ....
これだ! 第1項(= "4^1" における "4")の評価のための getset(repo, subset, x) 呼び出しの際に、自分の呼び出しで指定された subset をそのまま渡しているから、結果として第1項を評価する stringset() が空を返してるんだ!
今回の例で言うと:
- "
orset(subset=[0, 1, 2, 3, 4], x='4', y='4^1')" の評価を開始 - 第1項 "
4" を評価- "
stringset(subset=[0, 1, 2, 3, 4], '4')" 実行 - 評価結果は "
[4]"
- "
- 第1項の評価結果を
subsetから除外 ⇒subset=[0, 1, 2, 3] - 第2項 "
4^1" を評価- "
parentspec(subset=[0, 1, 2, 3 ], '4^1')" 実行- "
stringset(subset=[0, 1, 2, 3 ], '4')" 実行 subsetに含まれないので "4" は該当無し扱い
- "
- "
4^1" の評価結果は "[]"
- "
- 第1項と第2項の評価結果の和である "
[4]" を返却
といった感じかな?
本来 "parentspec()" は、"stringset(subset=全リビジョン, '4')" の結果を、subset=[0, 1, 2, 3 ]" でフィルタリングしなければいけないのに、"stringset()" 呼び出し(=副要素評価)に subset を指定しているのが駄目なんだな。
類似処理の確認
とりあえず原因はわかったけれど、でもこれって:
指定されたリビジョンに対する直接的な条件判定(例: コミットログの内容など)ではなく、間接的な導出(例: 親/子や、移植元/先など)を行う
といった全ての処理において発生し得る問題じゃねぇ?と思って一通りソースを調べてみたら、案の定、"ancestorspec()" ("~"相当) や rangeset() (":" 相当) でも subset の扱いが不適切である事を発見。
とりあえずパッチは取り込んでもらえたけれど:
将来的に、入力と出力の関係を上手いこと関連付けられれば、反復処理とかが組み立てやすいよね?(超意訳)
との Matt のお言葉なので、折を見て上手いリファクタリング案を考えてみた方が良いのかな?
今回の問題の再発防止は、現状だと目視で確認しなければならないから、annotation 等を使うなどして(ある程度)自動でガードできるようにしたいのは確かなんだよなぁ。
それから、subset として『全リビジョン』を指定する際に、list(repo) を使用している箇所がいくつかあったけど、list(repo) 評価時点で(ひょっとしたら大規模な)配列を作ってしまい、資源効率的にはよろしくない感じなので、代わりに repo.changelog を使う案も提案しなければ。