テンプレート関数 revset() の利用
hg log で特定のファイルの履歴を取る時に そのファイルの変更リビジョンから次の変更の直前までにあるすべてのタグを同時に取得する方法は何かないですかねえ?
#mercurialjp
— Kaz Nishimura (@kazssym) 2014, 8月 8
上記ツイートを契機としたやり取りの過程で、Mercurial 3.0 からテンプレート定義中で revset()
が使えるようになったのを、すっかり失念していたことに気付きました……orz
このまま放置すると、また忘れてしまいそうなので、何か実際的な処理で使って、しっかり記憶しておきたいところです。
何か良い題材はないかと考えたところ、先日公開したrevsets に関するエントリで例示している以下の処理が、まさに revset()
のユースケースにピッタリなことを思い出しました。
BRANCH=foo hg log -r "roots(branch($BRANCH))" --template "{rev}\n" | while read root do hg log -r "heads(descendantsin($root, branch($BRANCH)))" \ --template "$root {rev}\n" done | while read start end do hg diff -r "p1($start)" -r "$end" done
なお、while ループ内での revsets 問い合わせが非常に複雑なので、簡略化のため以下の様なエイリアス descendantsin
の定義を仮定しています。
[revsetalias] descendantsin($1, $2) = (($1)::) and ($2) and not (((($1)::) and not ($2))::)
但し、エントリ執筆時点の最新版である Mercurial 3.1 では、revsetalias によるエイリアス定義は revset()
の中で使用できません…… orz なので、例としてはエイリアス定義を使っていますが、実際の利用ではエイリアス定義を使わないようにしてください。
(※ 2014-08-28 追記) 修正が取り込まれましたので、2014-11 にリリースされる 3.2 からは、revset()
中でもエイリアスが使えるようになります。
元の処理では、hg log
を二段重ねにしていますが、テンプレート関数 revset
を使うことで、これらを単一の hg log
に集約することができます。
hg log -r "roots(branch($BRANCH))" \ --template "{revset('heads(descendantsin(%s, branch($BRANCH)))', rev) % '{rev} {revision}\n'}" | while read start end do hg diff -r "p1($start)" -r "$end" done
書き換え後のテンプレート定義における肝は、revset()
関数の利用と、その結果のリビジョン一覧を使ったマップ処理 (詳細はこちらのエントリを参照) になります。
まずはじめに、-r
オプションに対する revsets 記述により、テンプレート適用の対象となるリビジョンが抽出されます。
抽出されたテンプレート適用対象リビジョン毎に、テンプレート定義に従って、以下の処理が実施されます:
revset()
が revsets 問い合わせ文を構築revset()
が revsets 問い合わせを実施- 問い合わせ結果のリビジョン群に対して、マップ処理を実施
テンプレート関数 revset()
における問い合わせ文の構築では、第1引数のフォーマット文中の %s 等が、それ以降の引数で置換されます。
revset('heads(descendantsin(%s, branch($BRANCH)))', rev)
上記記述での rev
は、通常のテンプレートによる表示の際と同様に、「テンプレート適用対象リビジョン」 (= {rev}
) を指します。テンプレート関数に対する引数として、テンプレートキーワードを指定する場合は、波括弧 ("{}
") が付かない点に注意してください。
この revsets 問い合わせの実施結果は、元スクリプトでの最初のループにおける revsets 問い合わせに相当することになります。revset()
関数に対する rev
指定が、元スクリプトでの $root
に該当します。
hg log -r "roots(branch($BRANCH))" --template "{rev}\n" | while read root do hg log -r "heads(descendantsin($root, branch($BRANCH)))" 〜〜
テンプレート関数 revset()
の結果は、リビジョンのリストになりますので、マップ処理で各要素毎に出力を整形します。
revset(〜〜〜) % '{rev} {revision}\n'
上記マップ処理のフォーマット部分の構成要素は、それぞれ以下の意味を持ちます:
{rev}
は、テンプレート適用対処リビジョン (=-r
による問い合わせ結果の要素){revision}
は、revset()
による問い合わせ結果の要素
以上の結果として、(1)「-r
オプションでの revsets 記述による問い合わせ」というループと、(2)「テンプレート関数 revset()
による問い合わせ」というループの、2つのループによる処理を、1つの hg log
実行で実施できるわけです。
書き換え前後でどちらが分かり易いか?という点に関しては、好みが分かれるところだと思いますが(笑):
- 対象リビジョン数 (上記例の場合は「ブランチルートの数」) が多く、かつ
- プロセスの fork/exec が遅い環境での実行
上記の条件が成立するようなケースでは、対象リビジョン毎の hg log
の繰り返し実施がコスト高になりますので、記述内容が多少複雑でも、単一の hg log
実行に集約できるメリットは大きいと言えるでしょう。
なお、標準のテンプレート機能の範囲では、テンプレート適用対象リビジョンのメタデータは取得できますが、間接的に取得したリビジョンのメタデータは取得できません。例えば、テンプレート適用対処リビジョンに対して:
- ブランチ名 ⇒
{branch}
- 第1親リビジョンのリビジョン番号 ⇒
{p1rev}
- 第1親リビジョンのブランチ名 ⇒ 取得不能
テンプレート適用対象リビジョン以外のリビジョンのメタデータを取得したい場合は、間接的にメタデータを取得するテンプレートフィルタを併用してください。