読者です 読者をやめる 読者になる 読者になる

Mercurialのチェンジセットを指定するrevsetsについて

この記事は、Mercurial Advent Calendar 2011の2日目の記事です。

revsetsとは?

築いてきた歴史から特定のチェンジセットを効率よく検索するための、Mercurialのサブセット言語だ。

Mercurialでは、

$ hg log -r 1000

のlogコマンドように'-r'(--revision)オプションを付与できるコマンド(logコマンドは特に-rを多用する)がいくつかあるが、単にチェンジセットIDやリビジョン番号だけでなく、
revsetsと呼ばれる関数言語(helpでは問い合わせ言語と訳されている)を使うことで複雑なリビジョンを指定できる。

しかし、残念ながらこのrevsetsについて周知している人は少ないのではないだろうか?
このrevsetsを使う時はhg logコマンドの場合が最も多いと考えているが、'hg log'のhelpにはrevsetsへのポインタが示されておらず、唯一hg helpとしたときにrevsetsという項目を見つけるしかなく不親切だ。

肝心のrevsetsのhelpは、

$ hg help revsets

で確認できる。

revsetsの文法

functional languageとhelpに大仰に定義されているが、単項演算子と中値演算子、述語を組み合わせてチェンジセットを特定する言語である。 演算子や述語は値を受け取り値を返すことができ、またその結果に対しても適応できる、つまり関数として扱えるのでfunctional languageと定義されていると考えている。

single prefix operator(単項前置演算子)

演算子名 意味
not x、!x 特定のチェンジセットを除いたり、中値演算子や述語が返すチェンジセットを除外する

infix operator(中値演算子)

これらは、特定のチェンジセット同士を計算に用いる。
'^'と'~'はgitでは普通に使うものだけど、Mercurialにも1.9から実装され使えるようになった。

演算子 意味
x::y 、x..y チェンジセットの区間を表現できる
x and y、x & y あるチェンジセット同士を比較して共通に含まれているものを返す
x or y、x + y チェンジセットx、yどちらかに含まれているものを返す
x - y チェンジセットxに含まれていてかつyに含まれていないものを返す
x^ チェンジセットxの最初の親を返す。x^1と同義
x^n チェンジセットxのn番目の親を返す
x~n チェンジセットxのn番目の祖先を返す。

predicate(述語)

述語の引数に指定するものは文字列で次のような分類に分けられる。

  • set, pattern, single, regex(詳しくはhg help patternを参照)
  • interval(詳しくはhg help dateを参照)

何も指定しなければシェルの拡張ワイルドパターンとして展開される。

述語 意味
adds hg addしたファイルのパターンをpatternに記述する
all 0:tipと同じ。すべてのチェンジセットを返す
ancestor(single, single) 指定した二つの単一のチェンジセットの最大共通祖先を返す
ancestors(set) 指定したチェンジセットの祖先を返す
author(string)、user(string) 指定したユーザがコミットしたチェンジセットの集合を返す
bookmark([name]) すべてのbookmarkか名前付けされたbookmarkを返す
branch(set or pattern) 指定したブランチのチェンジセットを返す
children(set) チェンジセットの子供を返す
closed() クローズした(hg ci --close-branchした)チェンジセットを返す
contains(pattern) 指定したファイルパターンに含まれているファイルが変更されたチェンジセットを返す
date(interval) hg help datesを見ること。日付の区間の表現を確認できる
desc(string) コミットメッセージからマッチしたチェンジセットを返す
descendants(set) チェンジセットの子孫を返す
file(pattern) ファイルパターンを指定してマッチするファイルが変更されたチェンジセットを返す
grep(regex) keyword(string)と似たようなものだが、引数は正規表現を与える。特殊文字を使うならgrep(r'...')と記述する
head() チェンジセットすべてのheadを返す
heads(set) 指定したチェンジセット集合のheadを返す
keyword(string) コミットメッセージ、ユーザ名、変更のあったファイル名からマッチしたチェンジセットを返す。大文字小文字を無視する
last(set, [n]) 指定したチェンジセット集合の末尾からn番目を返す。デフォルトではn=1
limit(set, [n])、first(set, [n]) 指定したチェンジセット集合の先頭からn番目を返す。デフォルトではn=1
max(set) チェンジセットの中で一番大きなリビジョン番号のものを返す
merge() マージしたチェンジセット集合を返す
min(set) チェンジセット集合の中で一番小さなリビジョン番号のものを返す
modifies(pattern) パターンで指定した変更があるチェンジセット集合を返す
mq() MQ管理下にあるチェンジセット集合を返す
parents([set]) 現在、もしくは指定したチェンジセットの親を返す
presents(set) 指定したチェンジセットのすべての親を返す
removes(pattern) 指定したパターンのファイルが削除されたチェンジセットを返す
rev(number) 指定したリビジョン番号のチェンジセットを返す
reverse(set) 指定されたチェンジセット集合を逆にして返す
roots(set) 指定されたチェンジセットにおいて親がないものを返す
sort(set[, [-]key...]) 指定したチェンジセットをソートして返す。keyに指定できるものは'rev(リビジョン番号)'、'branch(ブランチ名)'、'desc(コミットメッセージ)'、'user(ユーザ名。authorでもよい)'、'date(コミット日時)'。keyに'-'を指定することで降順にする
tag(set) タグが打たれたチェンジセットの集合を返す
transplanted([set]) transplantしたチェンジセットを返す

個人的には

  • sort()の文法気持ち悪い
  • desc()ってdescending?って思いきやコミットメッセージを検索するとか

等々少し不満はあるがあまり使わないのであまり気にしてない。

よく使うパターンをalias化する

これだけいろいろ述語があり、さらに演算子で組み合わせられるので、少し難しい検索条件を作るとrevsetsが長くなる。
そこでよく使うパターンはalias化させておくと便利だ。 .hgrcの[revsetalias]セクションに次の用に書いておく。

[revsetalias]
h = heads()
b($1) = reverse(branch($1))
begin($1) = roots(branch($1))

hg logのオプションとrevsetsの対応

hg logのオプションのいくつかはrevsetsの述語と対応している。
hg log オプション 等価なrevsets
-f ::.
-d x date(x)
-k x keyword(x)
-m merge()
-u x user(x)
-b x branch(x)
-P x !::x
-l x limit(x)

問い合わせ例

デフォルトブランチのチェンジセット
# ブランチ名に記号(-や/)が入った場合うまく処理されないので必ず'ブランチ名を囲むようにしている
$ hg log -r "branch('default')"
デフォルトブランチで版が1.5よりあとでmergeコミットを除くチェンジセット
$ hg log -r "branch('default') and 1.5:: and not merge()"
オープンブランチのヘッド
$ hg log -r "heads() and not closed()"
1.3から1.5の間で'bug'という単語が含まれかつhgext/以下のファイルのチェンジセット
$ hg log -r "1.3::1.5 and keyword('bug') and file('hgext/**')"
2011/05/01から2011/09/01までに変更があったチェンジセットをユーザ名でソート
$ hg log -r "sort(date('2011-05-01 to 2011-09-01'), user)"
リリースされていないチェンジセットにおいて'bug'もしくは'issue'が含まれているチェンジセット
$ hg log -r "(keyword('bug') or keyword('issue')) and not ancestors(tagged())"

templateと組み合わせることが多い

revsetsを使ってまで過去を検索するときは、--templateと組み合わせてチェンジセットの情報を絞って画面に出力させることが多い。 僕や、周りを観測してみたところ次のようにtemplateを指定してる人が多い。
このtemplateもtemplate keywordとtemplate filterとで構成されてて、また一エントリくらいかけるので紹介だけにとどめておく。 詳しくはhg help templatingで確認できる。

# Hash値とコミットメッセージのみほしい場合。gitのpretty=onlineと等価。よく使うので僕は.hgrcにaliasでこのテンプレートを定義している
hg log --template "{node} {desc}\n"
# コミットした日時(isodateフィルターつけると)やユーザ名もほしい
hg log --template "{date|isodate}[{author}] {node|short} {desc} \n"

個人的にrevsetsに対して思ったこと

revsetsで指定できる述語はたくさんあるが、頻繁に使うものは限られると思う。
  • reverse: デフォルトではリビジョン番号の昇順に表示されるので、reverseを挟むと最近のチェンジセットから表示してくれるので一番使う
  • branch: ブランチ単位で確認することが多いので
  • merge: !を付けたり'-'でmergeされたコミットを削除したり
  • ..: 特定のチェンジセット間を探すことは多い
  • ancestorsとparentsの使いどころ: ancestorsはここからの過去のもの全部取得したいときとかは便利。parentsは特定のmergeコミットを調査するときとか
  • keyword: ある程度バグの元になってるんじゃないかって単語が分かっている場合のときに指定する。基本的に何か文字列を検索したい場合はkeyword()使うといい思う
  • file: モジュールを絞り込んで検索するときに使う
  • date: この日からこの日までとかもよく使う

まとめ

あとから歴史を細かく検索してきたいときは、プロジェクトを進める上で結構クリティカルなことに対応する場合(不安定になった、バグがいつのまにか混入した、あの時点に戻りたい等)のときが多い。
Mercurialを使って開発している場合はrevsetsやtemplateを一通り覚えておくといざというときに歴史を素早く検索でき、原因究明を早められるだろう。