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を一通り覚えておくといざというときに歴史を素早く検索でき、原因究明を早められるだろう。

ClojureをReal Worldで使うためにMavenと連携する

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

Clojureを導入するには

Clojureをプロジェクトで使いたい、という現場の要望は結構多いかと思う。
安心してほしい。
フロントエンドにいきなりClojureを使うのはさすがに通りにくいと思うが、 バッチ等をJava(もしくはJVMで動く何か)で動かしているならClojureでそのJavaのバッチを置き換えられる。
置き換えるのはとても簡単だし、周囲の理解や上司の理解も得られるのではないだろうか。
何と言ったってコンパイルしてしまえばプロダクトコードは関係なくなる。
動いていてほしいのはバッチで、コードを気にするのはあなたで、あなたが快適に仕事ができるなら今すぐJavaをすててClojureでコードを書こう。

開発はleiningenで

Clojureで何か作るときは基本はleiningenを使うのが一番いいと思う。
leiningenについての記事はいろいろあるのでここでは割愛する。
しかし、このleiningenの環境を実行環境に整えるは結構面倒くさいと思っている。
Mavenならバイナリをダウンロードしてすぐに使えるのが強力な点だ。
それに、もしあなたプロジェクトでJavaを使っているならMavenはすでに使っていることだろう。

Clojure Maven Pluginの導入

Clojureは基本的にJavaだ。 JVMの心を理解しないといけないのはJavaと一緒である。
MavenJVM上で動作するツールとして非常に親和性が高いと思っている。 Clojureで何か作業するにしてもJavaで作られたライブラリを使いたいことは多々ある(今後Clojureが進化してClojureだけでいろいろできたらいいとは思う)。
Mavenを使っていればMaven上にあるJavaのモジュールをすぐに取り込めたりもできる。
そこで、ClojureMavenで動かすためにClojure Mavenプラグインを導入しよう。

まずは、lein pomでproject.cljからpom.xmlを生成する。

$ lein pom

そして、生成されたpom.xmlにClojure Mavenプラグインの依存関係を追加する。

<build>
  <plugins>
    <plugin>
      <groupId>com.theoryinpractise</groupId>
      <artifactId>clojure-maven-plugin</artifactId>
      <version>1.3.8</version>
      <configuration>
        <sourceDirectories>
          <sourceDirectory>src</sourceDirectory>
        </sourceDirectories>
        <mainClass>example.run</mainClass>
      </configuration>
      <executions>
        <execution>
          <id>compile-clojure</id>
          <phase>compile</phase>
          <goals>
            <goal>compile</goal>
          </goals>
        </execution>
        <execution>
          <id>test-clojure</id>
          <phase>test</phase>
          <goals>
            <goal>test</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

プロジェクトの設定

Clojure Mavenプラグインでは、プロジェクトの構成に柔軟に対応するためconfiguration要素で設定できる。

ソースコードの場所指定

Maven Clojureプラグインではデフォルトで、src/main/clojure/*.clj、src/test/clojure/*.cljをコンパイルしようとする。 leiningenのlein newで生成したプロジェクトは、デフォルトでsrc/{proj}/core.clj、src/{proj}/test/core.cljを作成し、core.cljでnamespaceを、

(ns {project}.core)
(ns {project}.test.core)

のように設定している。
ソースコードの場所を指定するディレクトリはパッケージのディレクトリを指定することになるので、leiningenのデフォルトに対応しようとすると、

<configuration>
  <sourceDirectories>
    <sourceDirectory>src</sourceDirectory>
  </sourceDirectories>
  <testSourceDirectories>
    <testSourceDirectory>test</testSourceDirectory>
  </testSourceDirectories>
</configuration>

と設定する。

もちろん自由にプロジェクトの構成は変えていい。
個人的にClojureプロジェクトでおすすめなディレクトリ構成は次のようにする。

<configuration>
  <sourceDirectories>
    <sourceDirectory>src/main/clj</sourceDirectory>
  </sourceDirectories>
</configuration>

clj/以下は*.cljのコードをおいて、さらにJava等のモジュールがあればsrc/main/jvm以下に置く。

Goalに対する設定

Maven Clojureプラグインで実行できるGoalはいくつかあるが、一番重要なものがclojure:runだろう。 特にcompileしたものを動作させるのか、それともClojureのコード自体を直接実行するのかどうかで設定する項目も変わってくる。 Clojureのコードを実行させるには、

<configuration>
  <script>src/example/run.clj</script>
</configuration>

また、main関数を定義してそこを起点にして実行させたい場合は、コンパイルが必要になる。 main関数を定義したクラスを指定することで実行できるようになる。

<configuration>
  <mainClass>example.run</mainClass>
</configuration>

デプロイ後はMavenで実行

さて、ここからあとはデプロイして実行するだけだ。
Javaで今までバッチを動かしていたならjdkやMavenは導入済みだろう。
Mavenがないって?
問題ない。バイナリを今すぐダウンロードしてパスを通す
そして、今までバッチをキックしていたshellスクリプト等でJavaを動かしていたものを

mvn clojure:compile
mvn clojure:run

に置き換える。たったこれだけだ。

これまで通りバッチは仕事をし続けてくれるし、JVMで動作するので問題があったとしてもこれまで通りのデバッグ方法(jstackによるスレッドダンプやVisual VM等のツール)が通用する。
あなたは今までの1/10の時間でClojureでコードを書いて余った時間はさらに自己鍛錬に打ち込める。
で、あるとき、誰かが*.cljコードを見つけたときはバックエンドはすべてClojureに置き換わっていて、十分に信頼性を得られたことになる。
すべてのコードをClojureに置き換える日も近いだろう。
yay!