Subversionアーキテクチャ でぶあんの締切ももう過ぎてしまっているのだが、なんかネタがないというか 原稿の神がおりてこないので、subversionアーキテクチャとプロトコルについて とりあえず書いてみることにする。ここに書いてある情報は Subversionの ソースをさぐるとのっている内容なので、興味がわいた人はぜひソースを 読みまくってハックしていただきたい。 * Subversionはいくつかのレイヤに分割できる。 それぞれのレイヤのプログラミングインターフェースがちゃんと定義されていれば システムの別の部分をカスタマイズするのは簡単になる。レイヤのインターフェース の変更がすくなければ、新しいクライアントアプリケーションを書いたり、 新しいネットワークプロトコルを作ったり、新しいサーバプロセスを作ったり、 サーバに新しい機能を追加したり、新しいストーレジのバックエンドを作ったり することができる。 次の図がレイヤーアーキテクチャを示している。それぞれのインターフェースが どこにあるかも示してある。 ================================================================ +--------------------+ | commandline or GUI | | client app | +----------+--------------------+--------------+ <== Client interface | Client Library | | | | +--+ | | | | | +-------+--------+ +----------------------------------+ | Working Copy | | repository access | | Management lib | +--------------+--------+----------+ <== Network interface +----------------+ | WebDAV/DeltaV| SVN | local | +--------------+ proto. | access | | neon | | | +--------------+--------+ | ^ ^ | | / | | | DAV / | | | / | | | v subversion| | | +---------+ protocol| | | | | | | | | Apache | | | | | | | | | | | | | | | | | | | +-------------+ v | | | mod_DAV_SVN | svnserve | | +----------+-------------+------------------+----------+ <= Filesystem | | interface | Subversion Filesystem | | | +------------------------------------------------------+ ================================================================ * クライアントレイヤ Subversionのクライアントは3つのライブラリを使って作られる。 SubversionのクライアントはコマンドラインもあるしGUIもある。 ワーキングコピーライブラリ `libsvn_wc' がクライアントのワーキングコピーを 管理するためのAPIを提供している。これに、ファイルのリネームや削除、 ファイルにパッチをあてること、ローカルのdiffをとることといったオペレーション や、.svn/ ディレクトリにある管理ファイルを操作するルーティングが含まれている。 オフラインで操作できる処理などがこのライブラリに含まれている。 このAPIは svn_wc.hに記述されており、svn_wc_* という名前になっている リポジトリアクセスライブラリ `libsvn_ra' が Subversionリポジトリと情報を やりとりするためのAPIを提供している。これに、ファイルを読んだり、ファイルの 新しいリビジョンを書いたり、ワーキングコピーとリポジトリとの比較をしたり する機能がある。このインターフェースにはいくつかの実装がある。 WebDAV経由でリモートにアクセスするもの(libsvn_ra_dav)と、 SVN protocolでリモートにアクセスするもの(libsvn_ra_svn)と、 ローカルディスクのリポジトリにアクセスするもの(libsvn_ra_local)が現在存在 しているが、その他の実装もありうる。 このAPIは svn_ra.h に記述されており、svn_ra_* という名前になっている。 クライアントライブラリ `libsvn_client' が一般的なクライアントの機能、 update()やcommit()といった機能を提供している。これは上述した libsvn_wc とか libsvn_ra などを使って実装されている。`libsvn_client' が Subversionクライアントアプリケーションを実装するために必要な APIを提供しているということになる。 このAPIは svn_client.hに記述されており、svn_client_* という名前になっている。 ================================================================ * ネットワークレイヤ ネットワークレイヤのやることはリポジトリAPIのリクエストをネットワーク上 でやりとりすることである。 クライアント側では、ネットワークライブラリ `libneon' が、これらの リクエストを HTTP WebDAV/DeltaVリクエストに変換している。この情報が TCP/IPをつかって Apacheサーバに送られる。 Apacheモジュール `mod_dav_svn' が 送られてきた WebDAV/DeltaVリクエストを リポジトリに対するAPIコールに変換している。 ================================================================ * ファイルシステムレイヤ リクエストがリポジトリに到達すると、Subversionファイルシステムライブラリ `libsvn_fs' によって解釈される。Subversionファイルシステムは、Unixの ファイルシステムに似ているが、書きこみにリビジョンがつくのとアトミックに おこなわれ、データが削除されることがないという点が違う。このファイルシステムは 現在普通のファイルシステムの上のBerkeley DBファイルとして実装されている。 このAPIは svn_fs.h で定義されており、svn_fs_* という名前になっている。 ================================================================ * クライアント Subversionクライアントは3つのライブラリをつかって作られる。 - ワーキングコピーを操作するライブラリ libsvn_wc - リポジトリとやりとりするライブラリ libsvn_ra - libsvn_wc と libsvn_wc をつかってクライアントとしてのオペレーションを 実装したライブラリ libsvn_client ** ワーキングコピーとそれを操作するためのライブラリ ワーキングコピーは、クライアント側のディレクトリツリーで、バージョン付け されたデータとSubversionの管理ファイルが含まれている。Subversionの中で ワーキングコピーを管理するライブラリだけがこれらのファイルに対する操作を おこなう。 ** ワーキングコピーのレイアウト ワーキングコピーがどのように配置されるかを説明する。 CVSの場合は CVSというディレクトリに管理ファイルがおかれていたが、 Subversionの場合は .svn というディレクトリに管理ファイルがおかれる。 myproj / | \ _____________/ | \______________ / | \ .svn src doc ___/ | \___ /|\ ___/ \___ | | | / | \ | | base ... ... / | \ myproj.texi .svn / | \ ___/ | \___ ____/ | \____ | | | | | | base ... ... .svn foo.c bar.c | ___/ | \___ | | | | | base ... ... myproj.texi ___/ \___ | | foo.c bar.c dir/.svn/ディレクトリはそれぞれ dirディレクトリにあるファイルの情報、 つまりリビジョン番号やプロパティのリスト、update,checkoutした時の 元のリビジョンのファイル(これはクライアント側で差分を生成するために 使われる)、dirディレクトリのリポジトリ、dirディレクトリにくわえられた ローカルな変更(追加、削除、移動)などといった情報をふくんでいる。 親ディレクトリで保持されている情報などを参照するようにすればこれらの情報を 減らすことはできないことはないが、それぞれのディレクトリが独立して扱える ようにするために、個々のディレクトリで必要な情報はそれぞれのディレクトリで 保持している。 例えば、チェックアウトした直後なら、全ワーキングツリーに必要な管理情報は そのトップディレクトリにある情報さえあればよい。しかしサブディレクトリは それぞれ独立して管理情報をもつようにしてある。変更がくわえられていくに したがってトップディレクトリの情報だけでは扱えなくなるからである。 またサブツリーの一部のみをとりだした時も、それがワーキングコピーとして 問題なく使えるようにするためでもある。 .svnディレクトリは以下のものを含んでいる - formatファイル。ワーキングコピーの管理ファイルのバージョン番号。 - text-baseディレクトリ。ワーキングコピーの元のリポジトリの内容 - entriesファイル。このディレクトリのファイルのリビジョン番号などの情報 やサブディレクトリの情報など。リポジトリのURLなども含む。 CVS/Entriesに相当する。 - propsディレクトリ。プロパティ名とその値 - prop-baseディレクトリ。元のリポジトリでのプロパティ名とその値。 - dir-propsファイル。ディレクトリのプロパティ - dir-props-baseファイル。元のリポジトリのディレクトリでのプロパティ - lockファイル。 - tmpディレクトリ。 - logファイル。ワーキングコピーでくわえられてまだコミットしていない変更 詳しくは subversionのソースの subversion/libsvn_wc/README に書いてある。 ================================================================ * プロトコル libsvn_ra_dav が WebDAV/DeltaV プロトコルの実装、 libsvn_ra_svn が subversionプロトコルの実装である。 ** WebDAV/DeltaV SubversionがWebDAV/DeltaV をどう使っているかは http://svn.collab.net/repos/svn/trunk/www/webdav-usage.html に書いてある。 WebDAVと DeltaV拡張についてはそれぞれ http://www.webdav.org と http://www.webdav.org/deltav に書かれている。 ** Subversionプロトコル Subversionプロトコルは次のようなsyntaxになっている。 item = word / number / string / list word = ALPHA *(ALPHA / DIGIT / "-") space number = 1*DIGIT space string = 1*DIGIT ":" *OCTET space ; digits give the byte count of the *OCTET portion list = "(" space *item ")" space space = 1*(SP / LF) itemは最後にspaceが必須なのに注意。 command-responseは次のようなかんじ command-response: ( success params:list ) | ( failure ( err:error ... ) ) error: ( apr-err:number message:string file:string line:number ) クライアントはサーバのポート3690(デフォルト)にコネクトしにいく。 コネクションがはられると、サーバはgreetingをおくる。 greeting: ( minver:number maxver:number ( mech:word ... ) ( cap:word ... ) ) 例) % svnserve -t ( success ( 1 1 ( ANONYMOUS EXTERNAL ) ( ) ) ) minver, maxverはサーバがしっているsubversionのプロトコル番号、 mechはサーバがしっているSASLメカニズムである。capはサーバの ケーパビリティであるが定義されているものは現在存在しない。 クライアントがサーバのおくってきたプロトコル番号、もしくはSASLメカニズムに 対応できない場合はコネクションをきる。さもなければクライアントは次のような メッセージをおくる response: ( version:number mech:word [ mecharg:string ] ( cap:word ... ) ) 例) % svnserve -t ( success ( 1 1 ( ANONYMOUS EXTERNAL ) ( ) ) ) ( 1 ANONYMOUS ( ) ( ) ) ( success ( ) ) 他のSASLの場合は、ごちゃごちゃとやりとりをしてネゴする。 SASLのセキュリティレイヤのネゴが成功すればクライアントはURLをサーバに おくる url: ( client-url:string ) 例) % svnserve -t ( success ( 1 1 ( ANONYMOUS EXTERNAL ) ( ) ) ) ( 1 ANONYMOUS ( ) ( ) ) ( success ( ) ) ( 25:svn://localhost/tmp/repos ) ( success ( 36:2134bca4-82c2-0310-86d8-bbb6bcd82072 ) ) ここで 25: はその後のURLの文字数である。成功すればUUIDとよばれるものを かえす。 コマンドは基本的に次の形式となっている command: ( command-name:word params:list ) コマンドによらずよくつかうデータとしては次のものがある。 proplist: ( ( name:string value:string ) ... ) node-kind: none|file|dir|unknown bool: true|false *** コマンドセット コマンドセットはおおきく3つにわけられる。 メインコマンドセット・エディタコマンドセット・レポートコマンドセット **** メインコマンドセット メインコマンドセットは libsvn_ra のインターフェースに対応したコマンド である。 get-latest-rev params: ( ) response: ( rev:number ) get-dated-rev params: ( date:string ) response: ( rev:number ) change-rev-prop params: ( rev:number name:string value:string ) response: ( ) rev-proplist params: ( rev:number ) response: ( props:proplist ) rev-prop params: ( rev:number name:string ) response: ( [ value:string ] ) commit params: ( logmsg:string ) response: ( ) レスポンスをうけるとクライアントはエディタコマンドセットにうつる。 エディットが終了するとcommit-infoをおくって終了する commit-info: ( new-rev:number date:string author:string ) get-file params: ( path:string [ rev:number ] want-props:bool want-contents:bool ) response: ( [ checksum:string ] rev:number props:proplist ) want-contentsが指定されるとサーバはファイルの内容を空文字列で終了する 文字列のリストでおくってくる get-dir params: ( path:string [ rev:number ] want-props:bool want-contents:bool ) response: ( rev:number props:proplist ( entry:dirent ... ) )] dirent: ( name:string kind:node-kind size:number has-props:bool created-rev:number [ created-date:string ] [ last-author:string ] ) update params: ( [ rev:number ] target:string recurse:bool ) クライアントはレポートコマンドセットにうつり、finish-reportのあと サーバはエディタコマンドセットにうつる response: ( ) switch params: ( [ rev:number ] target:string recurse:bool url:string ) クライアントはレポートコマンドセットにうつり、finish-reportのあと サーバはエディタコマンドセットにうつる response: ( ) status params: ( target:string recurse:bool ) クライアントはレポートコマンドセットにうつり、finish-reportのあと サーバはエディタコマンドセットにうつる response: ( ) diff params: ( [ rev:number ] target:string recurse:bool url:string ) クライアントはレポートコマンドセットにうつり、finish-reportのあと サーバはエディタコマンドセットにうつる response: ( ) log params: ( ( target-path:string ... ) [ start-rev:number ] [ end-rev:number ] changed-paths:bool strict-node:bool ) レスポンスをおくる前に、サーバは"done"でおわるlogエントリをおくる log-entry: ( ( change:changed-path-entry ... ) rev:number [ author:string ] [ date:string ] [ message:string ] ) | done changed-path-entry: ( path:string A|D|R|M [ copy-path:string ] [ copy-rev:number ] ) response: ( ) **** エディタコマンドセット target-rev params: ( rev:number ) response: ( ) open-root params: ( [ rev:number ] root-token:string ) response: ( ) delete-entry params: ( path:string rev:number dir-token:string ) response: ( ) add-dir params: ( path:string parent-token:string child-token:string [ copy-path:string copy-rev:number ] ) response: ( ) open-dir params: ( path:string parent-token:string child-token:string rev:number ) response: ( ) change-dir-prop このコマンドにはレスポンスはなく、エラーはclose-dirで報告される params: ( dir-token:string name:string [ value:string ] ) close-dir params: ( dir-token:string ) response: ( ) add-file このコマンドにはレスポンスはなく、エラーはclose-fileで報告される params: ( path:string dir-token:string file-token:string [ copy-path:string copy-rev:number ] ) open-file このコマンドにはレスポンスはなく、エラーはclose-fileで報告される params: ( path:string dir-token:string file-token:string rev:number ) apply-textdelta: このコマンドにはレスポンスはなく、エラーはclose-fileで報告される params: ( file-token:string [ base-checksum:string ] ) コマンドをおくったあと、クライアントはsvndiffデータをストリングとして おくる。 change-file-prop: このコマンドにはレスポンスはなく、エラーはclose-fileで報告される params: ( file-token:string name:string [ value:string ] ) close-file: params: ( file-token:string [ text-checksum:string ] ) response: ( ) close-edit: params: ( ) response: ( ) abort-edit: params: ( ) response: ( ) **** レポートコマンドセット パイプライン的につかえるようにレポートコマンドはレスポンスをかえさない。 エラーはレポートを発行したコマンドのレスポンスで報告される。 abort-reportでエラーは無視される。 set-path: params: ( path:string rev:number start-empty:bool ) delete-path: params: ( path:string ) link-path: params: ( path:string url:string rev:number start-empty:bool ) finish-report: params: ( ) abort-report params: ( ) 例) svn ls svn://localhost/tmp/repos 相当の処理 ( success ( 1 1 ( ANONYMOUS EXTERNAL ) ( ) ) ) ( 1 ANONYMOUS ( ) ( ) ) ( success ( ) ) ( 25:svn://localhost/tmp/repos ) ( success ( 36:2134bca4-82c2-0310-86d8-bbb6bcd82072 ) ) ( get-dir ( 0: ( 1 ) false true ) ) ( success ( 1 ( ) ( ( 5:trunk dir 0 false 1 ( 27:2003-07-15T18:01:17.778008Z ) ( 4:ukai ) ) ) ) ) ちなみにSubversionプロトコルは libsvn_ra_svn/marshal.c の svn_ra_svn_write_cmd() と svn_ra_svn_read_cmd_response()を つかって読みかきをしている。ここで特殊なformat stringを実装し ていて write_cmdが printfのようにformatの形式にしたがって commandをかきだす処理を read_cmd_response が scanf のように formatの形式にしたがって response をよみとる処理をしている。 例えば get-dir のあたりはこのようなコードになっている subversion/libsvn_ra_svn/client.c ra_svn_get_dir() SVN_ERR(svn_ra_svn_write_cmd(conn, pool, "get-dir", "c(?r)bb", path, rev, (props != NULL), (dirents != NULL))); "c(?r)bb" がformat文字列である。c が path, (?r) に rev, 最初の b に (props != NULL)がboolで、次のbに (dirents != NULL)がboolで出力される。 ( get-dir ( c ( ?r ) b b ) ) ^ ^ ^ ^ | | | +- (dirents != NULL) | | +--- (props != NULL) | +-------- rev +-------------- path ↓ ( get-dir ( 0: ( 1 ) false true ) ) SVN_ERR(svn_ra_svn_read_cmd_response(conn, pool, "rll", &rev, &proplist, &dirlist)); ( success ( 1 ( ) ( ( 5:trunk dir 0 false 1 ( 27:2003-07-15T18:01:17.778008Z ) ( 4:ukai ) ) ) ) ) ↓ ( success ( 1 ( ) ( ( 5:trunk ... ) ) ) ) | ^^^ ^^^^^^^^^^^^^^^^^^^ | | | r l l <--- format | | | v | v rev | dirlist v proplist responseのほうでは "rll"がformat文字列である。response をよみとって、 r が rev に、最初の l がproplist に、次の l が dirlist に設定される。 なお、svnserve自体は stdin, stdout をよみかきしているだけなので svnserve を tee とかでかこんでやると subversionプロトコルをのぞきみ することは簡単にできる。 # cd /usr/bin # mv svnserve svnserve.bin # cat > ./svnserve #!/bin/sh tee /tmp/svnserve.in | /usr/bin/svnserve.bin | tee /tmp/svnserve.out ^D # chmod +x ./svnserve これで svn: (もしくは svn+ssh: )でアクセスしてみると /tmp/svnserve.{in,out} に client->server、server->clientそれぞれの内容が記録される。 SubversionプロトコルはこのようなSyntaxなので、lisp処理系でおしゃべりするのが 楽そうな感じである。gaucheのsubversion bindingもこれを使えば簡単なのでは ないか とフと思ったりもしたが、libsvn_wc相当の処理が必要となるので libsvn_clientのbindingを作るのはそれなりに手間がかかりそうである。 やはり swig を使ってがんばるのがよいのだろうか? 誰かやりませんか? Ruby bindingもゼヒ。