彷徨えるフジワラ

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

ファイルを登録除外したリビジョンの検出

本エントリも、備忘録代わりに「お気に入り」に入れてあったツィートの棚卸しシリーズの一部です(笑)

※ 本文中では revsets 述語の removes() に関して説明していますが、adds()modifies() でも似たような現象が発生しますので、可能であれば filelog()file() の使用をお勧めします。

以前、以下のようなツィートを見かけました。

実際には、「追加漏れ」云々は勘違いだったみたいなのですが:

という返信に対して:

という指摘がありました。

と思ったのですが、 @wonderful_panda 氏の想定しているシチュエーションが、一般的なファイルの登録除外等ではなく:

という状況で、確かにこれは想定外でした。
ここで一旦、removes() の挙動を整理してみましょう。

Mercurial の revsets 述語である removes() は、判定対象となる各リビジョンに対して以下のような条件判定を行っています。

  • 第1親の管理対象ファイル一覧と、自身の管理対象ファイル一覧を比較
  • 登録除外されたファイル一覧と指定パターンの比較
  • 合致するものがあれば、条件成立

そのため:

  • 祖先 A でファイル F を追加
  • 子孫 Ym で登録除外
  • X 系子孫と、Y 系子孫を M でマージ
  • M の第1親は X 系

という状況の場合、hg log -r "removes('F')" で表示されるリビジョンは、Ym と M になります。

ここでは、ファイル F を管理対象にしているリビジョンはグレーの背景で、指定条件に合致するリビジョンは二重丸で表しています。

Ym が条件に合致する事に関しては、説明の必要はないでしょう。

M との合致に関しては、「第1親との比較」であることを思い出してください。

M の第1親である Xn 時点までは、ファイル F が管理対象である一方で、Y 系とマージした M 時点からは、ファイル F の登録が除外されています。このことが、先述した条件を満たすため、リビジョン M は removes('F') に合致するとみなされるわけです。

さて、ここでもう一度、先程のツィートを見てみましょう。

これは、以下のような状況を指します。

  • 祖先 A 時点では、ファイル F は未登録
  • 子孫 Ym で、ファイル F を登録
  • X 系子孫と、Y 系子孫を M でマージ
  • M の第1親は X 系
  • マージの際に、ファイル F を不要とみなして、登録除外((実際の登録除外では、-f 付きで hg remove を実行する必要があるので、「追加漏れ」と言うのはちょっと苦しいですけどね))

上記条件の場合、ファイル F の登録除外を実施しているリビジョンは M なのですが、第1親側である X 系では、元々ファイル F を管理対象とはしていません。そのため、管理対象一覧を比較しても、ファイル F が登録除外されたことを検出できないのです。

removes() に対して修正提案しようかとも思ったのですが:

他の述語との統一性もありますし、そもそも指摘を受けたようなケースに関しては、祖先にさかのぼって判定する必要があるので、「性能的に許容できない」旨で却下されるのが目に浮かびます。

そこで、既存の機能を組み合わせることで、このような状況における「ファイル F を登録除外した(最初の)リビジョン」として、 マージ実施リビジョン M を認識できるような revsets 記述を考えて見ましょう。

「ファイル F を管理対象に含む」リビジョンは contains() を使って抽出できます。

$ hg log -r "contains('F')"

抽出されたリビジョン群の中で、「リビジョン群の中に(構造的な)子を持たない」リビジョンは、以下のいずれかに当てはまる筈です。

  • リポジトリの(全ての)リビジョン中に、本当に子リビジョンを持たない、あるいは
  • 子リビジョンは持つが、抽出されたリビジョンには含まれない(= 子リビジョンはファイル F を管理対象にしていない

「子を持たない」リビジョンは、heads() を使って抽出できます。

$ hg log -r "heads(contains('F'))"

上記で抽出した各リビジョンの子リビジョンは:

  • 本当に子リビジョンを持たないリビジョンからは、子リビジョンが導出できない
  • それ以外のリビジョンからは、子リビジョンが導出できて、且つ
    • 自分はファイル F を管理対象にしているが
    • 子リビジョンはファイル F を管理対象にしていない

つまり、後者の子リビジョンは「ファイル F を登録除外」したリビジョンに他なりません。

「指定リビジョン群の子」リビジョンは、children() を使って導出できます。

$ hg log -r "children(heads(contains('F')))"

これで「ファイル F を登録除外した(最初の)リビジョン」が検出できるようになりました。

このような revsets 記述を用いることで、先述したような特殊なケースに関しても、ファイルの登録除外を検出することができます。必要に応じて、エイリアス登録しておくのも良いでしょう。

[revsetalias]
strictlyremoves($1) = children(heads(contains($1)))

removes() での単純な判定では、最初に示した一般的な例では、マージ実施リビジョンも列挙されてしまいます。しかし、strictlyremoves() 相当の方法で判定した場合は、マージリビジョンが列挙されることはありませんから、そういった点でも、厳密な判定が可能です。

なお、上記の revsets 記述は、説明のわかりやすさ重視で組み立てたものです。そのため、リビジョンや管理対象ファイルが万単位で存在するような規模のリポジトリの場合は、応答性が多少劣化するかもしれません。

「大規模リポジトリで頻繁に実行されるバッチ等で使用」するようなケースであれば、もう少し実行性能に注意した revsets 記述を組み立てた方が良いでしょうね。

但し、revsets の実行効率の話は、かなり内部実装的な話に踏み込むので、機会を改めて説明しようと思います(笑)。

ちなみに、本エントリを書く際の動作確認過程において、contains() 述語には:

他の述語におけるパターン指定(但し、パターン形式指定無しの場合)が、「カレントディレクトリ相対」と解釈されるのに対して、contains() は「リポジトリルート相対」と解釈される

という微妙な挙動を見つけました。

早々に修正提案をしようと思いますが、利用の際にはご注意ください。
修正が取り込まれましたので、Mercurial 2.9 以降では現ディレクトリ相対で解釈されるようになります。