彷徨えるフジワラ

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

(利用状況次第では) 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 loghg 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() が空を返してるんだ!

今回の例で言うと:

  1. "orset(subset=[0, 1, 2, 3, 4], x='4', y='4^1')" の評価を開始
  2. 第1項 "4" を評価
    1. "stringset(subset=[0, 1, 2, 3, 4], '4')" 実行
    2. 評価結果は "[4]"
  3. 第1項の評価結果を subset から除外 ⇒ subset=[0, 1, 2, 3]
  4. 第2項 "4^1" を評価
    1. "parentspec(subset=[0, 1, 2, 3  ], '4^1')" 実行
      1. "stringset(subset=[0, 1, 2, 3  ], '4')" 実行
      2. subset に含まれないので "4" は該当無し扱い
    2. "4^1" の評価結果は "[]"
  5. 第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 を使う案も提案しなければ。