2007-01-18

最近みた TechTalks: Mercurial Project

Mercurial という分散 SCM の紹介. Python 製で, シンプル軽量スケーラブルが売り. 開発を初めたきっかけは linux の BitKeeper 事件だという. (だから GIT がライバルらしい.) OpenSolaris や OLPC など, けっこう採用実績があるのに驚いた.

私は分散 SCM を触ったことがない. SVK をちょっとつついたくらい. 話を聞く限り Mercurial はけっこう良さそう. (スライドは Wiki に公開されている.) 分散はさておき軽量なのがいい. たとえばレポジトリのためにわざわざ svnrepos みたいな別ディレクトリを作る必要がない. 作業コピーの中に .hg ディレクトリができて, ここに履歴が収められる. つまり作業コピーのディレクトリでレポジトリが閉じている. svn だとレポジトリを作るのが面倒でバージョン管理を先延ばしするケースがあるけれど, この方式なら不安を感じた瞬間にそのコードベースをバージョン管理下に置ける. (なお Darcs もこの方式だと 知り合いの Haskeller に教わった.)

ブランチやマージも簡単...というか, 他人と共同開発をするときはいつもレポジトリのコピー=ブランチをつくるのが分散流. どのレポジトリが "本家" なのかは運用によって決まる. なかなか面白い. ブランチが必須である以上, マージも一級市民. レポジトリがブランチとマージの履歴を覚えておいてくれる. だから安心してブランチ, フォークできる. これは svn/cvs にない.

講演者の Bryan O'Sullivan は, SCM が分散であることにとても価値を置いているらしい. 講演の中で "SCM ツールの選択は, そのプロジェクトがどう発展して欲しいかという意思の表明である" と 語っている. 中央集権な SCM を使うのはコードを集権的に管理したいという意思だし, 分散 SCM を使うのは自由なブランチやフォークを勧める意図だというわけ. アジテーションにしても格好いい.

パッチスタック

Mercurial にはパッチを管理する marcurial patch queues (mq) という機能がある. これは同種のツールである quilt (紹介記事PDF)の機能を Mercurial に取り込んだものらしい.

linux kernel のような大規模プロジェクトは, いくつもの patch を自分の作業コピーにあてた上で開発を行う. 開発中で本家に入っていない機能は patch として提供されるからだ. 自分がそんな新機能を開発していることも多いだろう. 作業コピーに適用する patch がふえるにつれて, その管理は面倒になってくる. どの patch を作業コピーに適用したか. 本家に投稿した patch はどれか. 本家を pull した時に衝突は起きないか, など. quilt はこうした問題を解決する. 適用した patch を覚えておき, 必要に応じて適用したり解除したりできる.

Mercurial queue は SCM の機能としてこの patch 管理を提供する. SCM 組み込みなおかげで, 適用した patch を仮にチェックインしたものとして扱うことができる. (あとから解除もできる.) だから diff をとったり rollback をしたりが楽になる, らしい. その他にも quilt にない機能をいくつか提供している.

私は quilt も初耳. mq にしろ quilt にしろ, patch 管理の機能は使いこなすと便利そうだ. でも, 私はそもそも patch があまり好きでない. 不自然な感じがする. 結局 SCM というのは patch, 差分情報の集合体であるはずなのに, patch の扱いがいまいち統合されていない. mq は Mercurial に組込まれているけれど, 本体の分散管理のモデルからはちょっと浮いている. SCM はもっと自然な形で patch を扱えてほしい.

バージョン管理のメンタルモデル

自分が patch に感じる不自然さの原因を少し考えてみる. そもそも私は SCM をどのようなものだと捉えているのだろう.

SCM の説明を眺めていると, よく次のような図を見かける.

この図は更新の進むレポジトリの様子をあらわしている. 数字はリビジョン. さて, この図のマルは何を表しているんだろう. 矢印は? レポジトリが差分の集合だという考えに従うなら, 図全体をレポジトリ, マルを差分と考えると自然な気がする. そして 1->2->3->4 という連鎖がファイルをあらわす.

ちょっとブランチしてみよう.

自然だよね. ブランチしたファイルは 1->2->5->6 で構築できる. (数字はチェックインした順番くらいのつもり, 大小には特別な意味はない.) 次にマージをしてみる.

このとき "7" は差分だろうか? ちょっと苦しい. マージした以上, 1->2->3->4->7 も 1->2->5->6->7 も同じファイルになってほしい. でも二つの異なるファイル 1->2->3->4 と 1->2->5->6 に同じ 7 という差分を 適用することはできない. 違うファイルに同じ差分を足したら結果は同じにならないから.

差分ではなく, その時点でのファイルがマルだと考えてみる. すると無理がなくなる. マルがファイルなら, 矢印が差分になる. 世の中の解釈はこっちなのかも. 私は勘違いしていた...

一方で, 差分をマルにするのにもいいところはある, と思う. パッチがわかりやすく表現できる.

このとき, 4 は不具合修正だとする. これは 6 のいるツリーにも適用できるものだった, なので 6 のツリーに 4 をパッチする. それが上の図. 4 と 8 は同じ内容かもしれないし. ブランチでの作業に追従してちょっと変更が入ったかもしれない.

同じことをファイルをマルとみなして図にしてみる.

矢印が差分だから, 矢印同士を矢印でつないでみた. やっぱり図としてヘンだよな... もっともパッチは隣接するリビジョン間から作るとは限らない. 2 と 4 の差分をパッチすることもある.

こっちの方が標記としては自然かな. まだヘンだけれど, それは patch というものが持つおかしさの表れな気がする. たぶん. なんとなく.

バージョン管理の数学

マルがファイルなのはいいとして, 矢印を差分とみなすのはまずいのかもしれない. それに差分と patch は同じものなんだろうか. それとも違うのか.

"A Principled Approach to Version Control" という記事を読んだあと, そんなことを考えた. この記事ではバージョン管理の概念を形式化しようと試みている. 実用的に役立つ話はないけれどいくつか洞察はあった.

この記事では, レポジトリを patch の重集合(multiset)として定義する. リストではないのがミソ. 順序がわからないとファイルが復元できない気がするけれど, できる. 実は patch の定義に細工がある. ここでは patch を二つのファイル(の中味)の対として定義している. 絵をかくとこうなる.

復元する時は, まず 0 (空のファイル) からはじまる patch を探す. つぎに適用できるものを探して適用していく. patch の間に順序はない(だから数字のかわりにアルファベットを使った)が, 適用可能性という形での関連がある. 0->A の次には A から始まる A->B が適用できる, A->B の次は B->C, という風に. その関連を使うことで repository as multiset of patches が実現できる. それが記事の主張だった.

この patch の定義のおかげで色々と謎が解けた気がする. 私は patch のことをを差分 A\B だと思っていた. でも順序対 A->B だと考える方が筋は通る. 一見オマケっぽい unified diff の文脈情報が, SCM理論(?)では必須要素だということになる.

このモデルなら, たとえば一般にいう patch の不自然さがうまく説明できる. patch が文脈を持つものなら, 既にあるブランチでの文脈を持った patch を別の文脈にもちこもうとする patch で 齟齬ができるのは仕方ない. SCM 理論そのままの patch はこうした文脈で適用できない(説明がつかない)けれど, unified diff は文脈がルーズなおかげで別のブランチにも適用できてしまう. これにはハック的な便利さがあり, しかし先に述べた SCM の枠組みからは外れている.

OSS レビューにおける patch 添付方式が気にくわない理由も腑におちる. レビュー待ちの patch はふつうまだ SCM での文脈を持っていない. だから(おそらくブランチとして)レポジトリに追加することができる. にもかかわらず cvs や svn が軽量なブランチを許さないため, 仕方なく不完全な文脈しか持たない unified diff の patch に頼っている. それが嫌なんだな.

要するに, 世の中の patch は SCM との関係からふたつに分類することができる. 1. 既に SCM に含まれている "束縛された" patch と, 2. まだ SCM に入っていない "自由な" patch だ.

前者の典型は, 安定板への back port や不具合修正だ. これらにいわゆる patch (unified diff) を使うのはある程度仕方ない. 束縛された patch を別の文脈に束縛しなおすのは, SCM のモデルを外れた不自然な状態だからだ. もちろん SCM が適用した patch を覚えておいてくれるに 越したことはないけれど, それは ChangeLog に記録を残すのと大差ない気がする. すくなくとも SCM に先のモデルを採用する限りは.

後者の典型はレビュー待ちの patch. こいつが問題だ. 強力な SCM を使えばこの patch を留保しておく必要はない. たとえば Mercurial のような分散 SCM は, 実質上開発者ごとにブランチができる. だから各人は作業をしたらそのブランチにチェックイン(束縛)してレビューに備えればいい. レビュアはレビュー要求のあったブランチをレビューし, 合格したらトランクにマージする. 修正が必要ならそのブランチ上で作業を続ける. レビューを待つ間も作業を続けたいなら更にブランチをつくる. ブランチが十分に軽量なら, こうしたスタイルでの開発が可能になる. レビューを含めた共同開発のタスクはずっと楽になると思う.

もちろんディスク容量, 帯域, セキュリティ機能や UI など, 既存の SCM は (Mercurial を含めて) まだ制限が多い. そのせいでブランチが軽量になりきっていない面はある. 古株の集権型 SCM も根強く支持を集めている. 開発プロセスもそれに最適化されている. だからしばらくは unified diff patch を頼ったレビューがなくなることはないだろう. それでもソフトウェア開発の理想形を考えるのは楽しいし, その理想を目指すものがあるなら応援したい.

だから, よし, しばらく分散 SCM 学派に宗旨替えしてみよう ...と思ってからまだ一度も趣味コードをチェックインしていない私は よいプログラマとは言えません. ビデオをみてる場合じゃないか...