(利用状況次第では) 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
を使う案も提案しなければ。