Meta-CVS-PAPER

Meta-CVS --- CVS 上のディレクトリ構造履歴管理レイヤー
Kaz Kylheku
初出 2002-01-25
改編 2004-02-03
"ディレクトリの履歴管理は難しい問題である" -- Subversion FAQ
"計算機科学のどんな問題も間接化技法を使ったもう一つのレイヤーによって解決することができる" -- David Wheeler

要約

これは Meta-CVS、Meta-(Concurrent Versions System) という CVS のフロントエンドである。Meta-CVS は複数の人々による協働とファイルのバージョンへの依存からの解放を支援し、ディレクトリ構造についても同じように機能する。私は数週間の間これを主に Meta-CVS 自身のソースを管理するのに利用しているところである。これは cvs プログラムをファイルの内容のバージョンのみならず、move や rename にも使えるようにするような方法である。変更はリポジトリに commit され、update によってそれを取得でき、working copy を再構成することでそれに合わせてリポジトリに組み込むことができる。構造に対して並行した変更が衝突した場合も他の衝突と同じように解決することができる。すべて Lisp で書かれている。

内容

1. はじめに

CVS として知られている 1986年から存在しているソフトウェア -- 最初のバージョンでは RCS コマンドのフロントエンドとして動く shell スクリプトで成り立っていた -- は Dick Grune によって Usenet に投稿された。それから 15年間に渡って、CVS は C のプログラムとして改良され、拡張され、デバッグされた。しかし現在の方式、バージョン 1.11 では面白くない変わった仕様といくつかの致命的な制限が存在している。

CVS の最も大きな制限の一つは、ディレクトリ構造をバージョン付けされたオブジェクトとして扱うことができないというものである。Meta-CVS はこの問題をよくデバッグされて実績のある CVS コードに出しゃばることなく、仮想化レイヤーを追加することで解決する。Meta-CVS は CVS の基本的な能力を保っている。並行してネットワーク越しに多様な作業ができるブランチ、マージの能力はそのままである。CVS は RCS のフロントエンドである。同じように Meta-CVS は CVS のフロントエンドである。

結局のところ、Meta-CVS は CVS のいくつかの不幸をうまく解決するものである。CVS でトラブルを生むいくつかの手の込んだシナリオも、Meta-CVS ではもはや問題ではない。2人の開発者が同時に同じファイルを add するとか、1人の開発者が別の1人が作業中のファイルを削除するとか。

Meta-CVS はバージョン付けしたファイルツリーの特殊な表現を作り出すことで動作する。そしてこの特殊な表現が CVS に保存される。したがってバージョン付けされたツリーとリポジトリ内のツリーのダイレクトマッピングは回避される。

この文書の目的はこの単純な表現を記述し、ディレクトリのバージョン管理操作をどのようにサポートするのかを説明することである。

2. データ表現概要

CVS からあらゆるオブジェクトに対して並行したバージョンコントロールを実現する能力を得るためには、そのオブジェクトはテキストファイルで記述されている必要がある。CVS はマージや衝突識別のアルゴリズムにおいて、テキストの入力のみ効果的に扱うことができる。Meta-CVS の必要要件の中で決定的な非機能的制限は CVS にまったく変更が加えられていないということである。Meta-CVS を使うためにクライアントにもサーバにも何か新しい CVS のコードをインストールしなければいけない人は誰もいない。さらに、CVS コードは華奢な C でできており、10年に渡ってデバッグが続けられている。(さらに増えている。)

ファイル構造をバージョン付けされた実体として扱うためには、したがってテキストファイルとして記述する必要がある。どんな構造をそのようなテキストファイルは持つべきだろう?

第一に、小さな変更は非常に望ましいだろう。小さな変更とはいくつかのファイルの rename のような小さな変化を生むものである。この属性は並行した変更を最小限の衝突で許すこととなるだろう。テキストファイルで表すということはまた人間に読めて編集できるべきである。なぜなら人間がその中の衝突の解決をしなければいけないことになるのだから。

第二に、ファイルはそのパス名が変わったときにも、どうにかして識別できて CVS の履歴を保たなければいけない。このことは、少なくとも CVS が分かっているファイルの名前を決して変更してはいけないということを意味する。

Meta-CVS はプロジェクトのファイル構造を ``ファイルマッピング'' と呼ばれるシンプルなものとして記述する。ファイルマッピングはパス名をフラットなファイルデータベースに関連付ける。マッピングもファイルも CVS に保存される。ファイルは機械の生成した名前を持つ。機械の生成する名前とは、ユーザーの目にする実際の名前から、マッピングを通じてのみ生成される。CVS が解釈する名前は ``F-files'' と呼ばれる。

Meta-CVS はマッピングをシンプルな Lisp 言語のデータ構造として操作する。Lisp はリストオブジェクトの印刷された表現を読むために解析し、書式化する仕掛けおよび印刷された表現を生成する仕掛けを組み込みで持っている。したがって Meta-CVS マッピングのためのテキストファイルフォーマットは、テキストのそれぞれの行のそれぞれの関連付けの要素を出力し、順番に一貫性を保持し続けるように特別な注意を払いながら、単純に Lisp の連想リストを持つものである。

フラットなファイルデータベースからディレクトリ構造を分離するのは何も新しいものではない。フラットファイルサービスからディレクトリサービスを分離するのはファイルシステムデザインの共通テーマである。(訳注:なんか意味がおかしいような気もする。)

Meta-CVS は、unlink してしまったファイルを見つけて lost+found ディレクトリに ID(これは inode 番号と同じように振る舞う!) 由来の暗号めいた名前で置く restore コマンドの程度まで、UNIX ファイルシステムを模倣している。UNIX ではファイルはディレクトリ構造から削除されてもまだ使える。そして同じことが Meta-CVS についても言える。Meta-CVS でのファイルの削除はディレクトリ構造からの非破壊的な unlink である。

2.1 ファイルマップの例

あるプロジェクト foo は以下のようなファイルで成り立っている。

foo/README
foo/inc/foo.h
foo/src/Makefile
foo/src/foo.c

Meta-CVS の表現はどのように見えるだろうか? この点について最もよく分かるのは CVS から Meta-CVS を通じて check out した working copy である。それはこれらを含んでいる。

foo/MCVS/CVS/Entries
foo/MCVS/CVS/... other CVS metadata ...

foo/MCVS/F-123D61C8FE942733281D2B08C15CD438
foo/MCVS/F-156CAB88D4EEE703E8C4B4146B5094E2.h
foo/MCVS/F-15EA9689ACF749C314CE6FC5255DC4B0
foo/MCVS/F-1C43C940D8745CAA78752C1206316B55.c
foo/MCVS/MAP
foo/MCVS/MAP-LOCAL

foo/README
foo/inc/foo.h
foo/src/Makefile
foo/src/foo.c    

MCVS と呼ばれるサブディレクトリがある。その中には CVS ディレクトリがある。MCVS サブディレクトリは言い換えれば CVS の ``サンドボックス'' である。foo 以下の他のすべてが機能しているファイルである。したがって全ての Meta-CVS working copy は、最上位のディレクトリに興味深い内容を含んだ MCVS サブディレクトリがある点を除けば、単に普通のファイルツリーである。

MCVS 以下にはどんなファイルがあるのだろう? いくつか F-123D...438 のような暗号めいた名前のファイルがある。そして MAP と MAP-LOCAL という2つのファイルがある。

まず、F-files とMAP は CVS でバージョン管理されていることを理解すべきである。もう一方で、MAP-LOCAL は CVS は分かっていないが、Meta-CVS が働くために重要なものである。

F-files は foo/README, foo/src/foo.c, foo/src/Makefile and foo/inc/foo.h の実際の CVS 上での表現である。

F- から始まる名前と人間の読めるパスの間に作られている関係は MAP ファイルの中の連想リストである。これは初期のバージョンの Meta-CVS では以下のようになっている。

((:FILE
  "MCVS/F-123D61C8FE942733281D2B08C15CD438" 
  "README")
 (:FILE
  "MCVS/F-156CAB88D4EEE703E8C4B4146B5094E2.h" 
  "inc/foo.h")
 (:FILE
  "MCVS/F-15EA9689ACF749C314CE6FC5255DC4B0" 
  "src/Makefile")
 (:FILE
  "MCVS/F-1C43C940D8745CAA78752C1206316B55.c" 
  "src/foo.c"))

checkout 後の MAP-LOCAL ファイルは単純に MAP ファイルの正確なコピーである。MAP-LOCAL の目的はユーザーの check out したコピーの中に存在する実際のマッピングを追跡し続けることである。update 操作が実行された場合は、リポジトリから MAP へ変更を受け入れるだろう。そうすると MAP はもうローカルのファイルシステムを反映したものではなくなる。

実際、MAP はその時点では未解決の衝突を含み得る。その場合は Meta-CVS にとっては手作業での介入を要する有用でないものである。しかし MAP-LOCAL コピーは手付かずで一貫性のあるままである。

Meta-CVS はマッピングのローカルコピーを保つので、Meta-CVS update の操作は、リポジトリからくる新しいマッピングとローカルのマッピングの間の違いを計算することができる。これらの違いはそしてファイルシステムの再配置の実行へと翻訳することができ、working copy の形を最新の状態にスパっと変えることができる。

この再配置は Meta-CVS システムの心臓部である。他のすべてのものはだいたい、単にマッピングの操作をするだけである。例えばファイルの rename なんて単純なものだ。MCVS/MAP をテキストエディタで開いて、パスを変更してみてほしい。(重複や不正なマッピングが起きないように慎重にやってほしい。)そしてそれを保存して mcvs update を実行する。Meta-CVS はあなたの作成した変更を物理的なファイルの再配置として反映するだろう。この変更が気に入ったら単に commit とする。MCVS ディレクトリ内の CVS レベルでコミットすることができる。しかしもちろん Meta-CVS にファイル rename 操作は提供されているし、commit 操作もそうだ。加えて CVS の実行も F-files は折りたたまれていない対応物と正しく同期すると保証されている。

2002年8月、シンボリックリンクのサポートが Meta-CVS に追加された。そしてそれを反映してマッピングの書式がより複雑なものになった。文法が拡張され、将来の拡張性も加味して異なる種類のエントリを許可するようになった。それぞれのエントリは今は最初の位置に、 Lisp 用語のシンボルをそのタイプを識別するために持っている。リストの残りでタイプごとの属性を特定する。現在では、2種類のエントリがある。:FILE と :SYMLINK である。

まさにこのとき、属性リストのバージョン管理もまた加えられた。

先のセクションでの例はこのようになる。

((:FILE
  "MCVS/F-123D61C8FE942733281D2B08C15CD438" 
  "README")
 (:FILE
  "MCVS/F-156CAB88D4EEE703E8C4B4146B5094E2.h" 
  "inc/foo.h")
 (:FILE
  "MCVS/F-15EA9689ACF749C314CE6FC5255DC4B0" 
  "src/Makefile")
 (:FILE
  "MCVS/F-1C43C940D8745CAA78752C1206316B55.c" 
  "src/foo.c"))

実行可能なファイルはパス名のあとに属性が加えられている。シンボリックリンクは以下のようになる。

(:SYMLINK
 "S-DF03GA1200347CF1935509371F8C1765" 
 "src/foo.c"
 "../foo.c")

これは src/foo.c という ../foo.c を実態とするシンボリックリンクの存在を宣言するものである。

現在ではマッピングのエントリの中で、2番目の位置に ID 文字列を持つことも、、3番目のオブジェクトのパスの位置に ID 文字列を持つこともサポートされている。そのあとの文法は様々である。

ところで Meta-CVS は今も古いフォーマットを理解して解析することもできる。マッピングするオブジェクトが MAP ファイルから読み取られたらすぐに、抽象構文木は古い構文に従っているか新しい構文に従っているかの判別を検査される。誰も古い構文は使わない。それは Meta-CVS 自身のリポジトリの中の古いバージョンにだけ存在している。

2.3 同期

次に立ち向かうべき問題はどうやって F-files と作業ファイルの間のやりとりを実現するかである。Meta-CVS はプラットフォーム固有の方法で行っている。すなわち Unix のハードリンクに頼っているのだ。Meta-CVS が sandbox を check out したとき、ハードリンクが生成され、それによって F-files と対応する作業ファイルが実際に同一ファイルシステム上のオブジェクトとなる。したがってマッピングを通じて F-files を ``紐解くこと'' はファイルデータの大量のコピーを必要としない。ディレクトリの作成とリンクだけである。

問題は、ファイルを消したり同じ名前の新しいファイルで上書きするなど、いくつかの操作がこのハードリンクの接続を ``破壊'' してしまうということである。例を挙げると、CVS update の操作はこうしたことが起きる。もし cvs up が新しい F-file を作成する場合、そのファイルはもはや作業ファイルとは結び付けられない。

2つの同期を保つために、Meta-CVS は同期の操作を行う。この操作はファイルマップ上を走査し、壊れたリンクを修復する。もし2つのファイルのどちらかを失った場合もリンクは生成される。もし両方ともはっきりしたオブジェクトとしてあるなら、より最近タイムスタンプが更新された方に取って代わられる。もう一方は消去され、新しい方のリンクで置き換えられる。

こうした同期はあらゆる ファイルの移動、消去、CVS リポジトリへの commit を生む走査の前に行われるべきである。すべての状況において F-file は正しい内容を保持していなければいけない。

Meta-CVS の update 操作は同期を2回取らなければいけない。CVS update の前には F-file がローカルの変更のすべてを扱っていることを保証するために、CVS update のあとには working copy に新しく取り込まれた変更をすべて反映し戻すために。

現在の Meta-CVS の動作は上の記述で示しているよりも複雑である。下位のツリーのみを操作するコマンドでは MAP 全体を処理せず、その代わりにマッピングで抽出された下位のツリーに一致するエントリのみを処理する。第二に、同期はその方向を判断する。例えば、CVS commit の前であればツリーから CVS サンドボックスへと同期するという意味になり、反対方向の意味にはならない。CVS commit 直後は、commit されたファイルを CVS が変更したのであれば(例えばキーワード展開による変更など)、反対方向に反映する意味になる。

2.4 パーシャルサンドボックス

(訳注:うまい訳語が見つからないが、意味としては CVS リポジトリと working copy の間に入る MCVS 用の「中間情報の格納場所」がパーシャルサンドボックスの正体である。)

ときどき大きなプロジェクトの中の一部のツリーだけをリポジトリから引っ張ってきたいことがある。ツリー全体をバージョン管理されたオブジェクトとして表すバージョン管理システムはどのようにこれを実行することができるのだろうか? ツリーの一部を check out したいという要求は大雑把に言うとファイルの半分を check out したいという要求と同じである。

Meta-CVS はこの問題をパーシャルサンドボックスという考え方をサポートすることで解決している。これは CVS サンドボックスの中に全マッピングを check out するということである。check out された部分ツリーの最上位階層の相対パス名を含む DISPLACED と呼ばれるローカルファイルが書かれる。例えば x-complier プロジェクトの testcases/optimization というサブディレクトリを check out するとしたら、DISPLACED ファイルは testcases/optimization というパスを含む。

Meta-CVS のすべてのアルゴリズムは DISPLACED パスが把握しており、マッピング情報nに含まれている /abstract/ パスとその短縮形、およびサンドボックスのツリーの中の /real/ パスの間は正しく変換される。(訳注:the shorter が何に掛かるのか自信なし)この変換は DISPLACED ファイルがない場合は行われない。すなわち、サンドボックスが完全な一つのものである場合である。

パーシャルサンドボックスはリポジトリから届いたマッピングの変更に忠実に動作する。他のユーザーが見えている部分から見ていない部分ツリーへとファイルを移動した場合も、ちゃんと動作する。反対方向も同様である。

パーシャルサンドボックスは grab コマンドで新しく外部に落ちていたものを特定のブランチや trunk 上のプロジェクトの部分ツリーにだけ保存するときに用いられる。このために grab コマンドのすでに管理されたアルゴリズムは abstract パスと real パスの間の変換とともに投入されなければならない。

3. 意外な長所

Meta-CVS の表現は、設計段階においてはすぐには分からないが開発段階において明らかになるいくつかの利点を持っている。ディレクトリ構造の履歴管理の欠如に加えて、CVS には Meta-CVS 環境では消え去ってしまう面白くない点が他にいくつかある。また、ディレクトリ構造の履歴管理の可能性を持ち込めば、新しい懸念を持ち込むことにもなる。フリーソフトウェア開発者はコードの変更の交換において patch を利用している。CVS のようなパッチを生成し、適用する伝統的なツールはディレクトリの履歴管理を扱えない。Meta-CVS はこうした問題にいくつかの解答を用意している。

3.1 ファイルの add の衝突

CVS では、2人(あるいはそれ以上)の開発者が同じモジュールで作業しているとき、同じディレクトリでファイルを add するとき、同じファイル名を扱うあらゆるときに起きる。1人めの開発者がファイルを commit し、そして続いて commit しようとする開発者のところで問題が起きる。CVS は第二者から独立して add されたと文句を言い、commit を進めることを許可しない。

Meta-CVS ではこのようなことは起こり得ない。Meta-CVS は2人がファイルを add していること、それが同じファイルでないことを理解する。名前は同じものと判断されず、意味が通るのである! Meta-CVS でファイルが add される場合はそれを表す F-file が生成される。F-file の名前はランダムに選ばれた 128ビットの 16進数で表現された数値を含んでいる。2つのある数値が一致する可能性はおおよそありそうもなく、だから実際に人は決して前述の CVS のエラーメッセージを目にすることはないだろう。

代わりに、

複数の開発者がファイルに同じパス名を選んだときに起こることは、解決しなければならない MAP ファイルの中での衝突が生じるか、マッピングがパス名の重複を含んでしまい、それは Meta-CVS によってエラーとして検出されるもので、再び起きたらそれはユーザーが解決しなければいけない。それぞれのファイルは、固有のバージョン履歴を持つオブジェクトを区別するもので、したがって2つのオブジェクトが偶発的に同じ名前にマップされることはたいしたことではなく、修正可能な問題である。

3.2 ファイルの remove の衝突

CVS は1人の開発者がファイルを削除するとき、cvs remove をして、また他の変更の commit を続けようとしたとき、決してうまい動作はしない。

これはまさにオブジェクトのライフタイムの計算についての古典的な問題をバージョン管理のドメインに変換した実例に過ぎない。

オブジェクトのライフタイムの計算の問題の最もきれいな解放はガベージコレクションである。これはオブジェクトが使われている限りライフタイムを保証するもので、その後、システムがその必要性か都合のよさを発見したときに自動的に削除される。

結果的に、Meta-CVS はガベージコレクションの考え方の類いをサポートしていることが分かる。ファイルを削除するときには cvs remove に支配される必要はない。F-file が削除されずに居る限りはマッピングから削除されるのみである。これが意味することは F-file は check out され続け、そのために帯域とスペースを占有するということである。もしユーザーが未解決の変更を持っていて、Meta-CVS update がそのファイルを remove がした場合何が起きるだろう? リンクの同期が未解決の変更を削除前に F-file に変換することを保証する。だから変更は失われない! F-file を手で MAP の中に保存することで ``寿命を延ばす''ことが可能である。これはガベージを取捨選択することに類似したもので、もう一度連絡するようにマークすることでファイルを救い出すことができる。そしてもちろん F-file それ自身の変更は、マップに入る入らないに関わらず CVS へ commit 可能でである。

スペースの問題は Meta-CVS の管理上実行され得る ``ガベージコレクション'' のルーチンによって扱われる。ガベージコレクションはすべてのマッピングを持たない F-files を識別し、除去し、そしてこれらを ``cvs remove'' するだろう。

3.3 diff と patch の動作

Meta-CVS のもう一つの意外な利点は、内容と同じようにファイルシステムに継ぎを当てる patch の配布の問題に対応している点である。

F-file および MAP ファイルは実際にプログラムソースを交換するフォーマットを構成している。このことは原則的に、フラットなファイルによって成り立つあらゆる修正管理ツールの可能性を広げている。

開発者は Meta-CVS のやり方でプロジェクトのコピーを入手できる。そしてパスの rename を含む修正作業を加える。これらの変更は新しい Meta-CVS ファイルセットでもって表現される。diff は新しいものと古いものによって算出される。オリジナルのコピーを持つ者は patch を当て、変更を再生することができる。必要なことは Meta-CVS ソフトウェアが再構成に気づくことですべてである。