2006-11-04

ベンダーブランチと svn:externals

svn:externals って何? と訊かれたので説明します. ほぼ私信です.

subversion を使ってプロジェクトのコードを管理しているとき, 他のプロジェクトやサードパーティといった外のコードも一緒に使いたいことがある. そういう時は相手のコードを自分のレポジトリにとりこむといい. そのための機能として, svn には ベンダーブランチsvn:externals, 二つの方法がある. (もっとあるかもしれない.) ベンダーブランチはコピーで, svn:externals がリンクだと思えばいい.

ベンダーブランチ

ベンダーブランチは自分のツリーに外のコードをチェックインする仕組みのこと. 仕組みといってもシステムからの支援は特にない. 流儀と言った方がいいかもしれない.

まずサードパーティのコードを /vendor 以下にインポートしてバージョンでタグを切り

# マニュアルから抜粋
$ svn import /path/to/libcomplex-1.0 \
             http://svn.example.com/repos/vendor/libcomplex/current \
             -m 'importing initial 1.0 vendor drop'
$ svn copy http://svn.example.com/repos/vendor/libcomplex/current  \
           http://svn.example.com/repos/vendor/libcomplex/1.0      \
           -m 'tagging libcomplex-1.0'

そのタグから trunk にコピーする

# よくみると trunk じゃない気も...
$ svn copy http://svn.example.com/repos/vendor/libcomplex/1.0  \
           http://svn.example.com/repos/calc/libcomplex        \
           -m 'bringing libcomplex-1.0 into the main branch'

端的に言うと, この方法は変更に強いのが利点. trunk にコピーされたコードは, 他への影響なく書き換える(=変更する)ことができる. 要するにローカルでパチれる. 逆にコピー元のコードがバージョンアップで変更されても関係ない.

ただしそのバージョンアップを反映しようと思うと少し面倒だ. 流儀としては, まずベンダーブランチのバージョンをあげ, それを trunk にマージする. 実際にやってみると衝突, 登録もれ, 削除わすれ, API 非互換でビルドできない ... など, けっこう面倒で複雑. 手強い. チェックアウトしてビルドを繰り返し, 調整が終わって気がつくと 10 くらいリビジョンを浪費している.

マニュアルには, この手間を省く svn_load_dirs.pl というスクリプトが紹介されている. 私はまだ使ったことがない.

svn:externals

ファイルをコピーするベンダーブランチに対して, リンクをはるのが svn:externals. これは subversion クライアント組込みの機能で, あるディレクトリの下に自動で別のレポジトリをチェックアウトできる.

まず, リンクしたいディレクトリに subversion の プロパティ でリンク先を指定しておく.

# またマニュアルから抜粋...
# リビジョンも指定できるのがミソ.
# calc ディレクトリの svn:externals プロパティに
# ディレクトリ名と URL の対が登録されている
$ svn propget svn:externals calc
third-party/sounds             http://sounds.red-bean.com/repos
third-party/skins              http://skins.red-bean.com/repositories/skinproj
third-party/skins/toolkit -r21 http://svn.red-bean.com/repos/skin-maker

するとチェックアウトの際, 指定されたレポジトリも連鎖的にチェックアウトされる

$ svn checkout http://svn.example.com/repos/calc
A  calc
A  calc/Makefile
A  calc/integer.c
A  calc/button.c
Checked out revision 148.

Fetching external item into calc/third-party/sounds
A  calc/third-party/sounds/ding.ogg
A  calc/third-party/sounds/dong.ogg
A  calc/third-party/sounds/clang.ogg
...
A  calc/third-party/sounds/bang.ogg
A  calc/third-party/sounds/twang.ogg
Checked out revision 14.

プロパティの使い方はマニュアルをよんでください...

一見便利な svn:externals にも問題はある. まず, 参照先のコードはうかつに変更できない. リンクはふつう相手の本家レポジトリを参照しているから, その変更は影響範囲が広い. 誰かのビルドを壊してしまうかもしれない. そもそも自分にコミット権のないレポジトリかもしれない.

変更できるにしても, レポジトリをまたいだコミットはアトミックにならない. (そもそもレポジトリが違うからリビジョンに相関がない.) 関連した二つの変更を紐づけて記録できないのは気分が悪い.

自分の変更が誰かに影響するということは, 誰かの変更が自分に影響するということでもある. つまり他人のチェックインで自分のビルドが壊れうる. これはとても不安だ. その相手が同じチームでない限り, こちらのビルドを気にしてくれるはずもない. 継続的統合で検出できると思うかもしれないけれど, あれはあくまで安全弁. 相手がこちらのコードをビルドしていない以上, エラーは許容できないほど頻繁に起こる.

そんなわけで, svn:externals を使えるのはリンク先がコードが stable な場合に限られる. 私は断然ベンダーブランチが好みだな.

個人ユーザの中には, 自分のコードをレポジトリをわけて管理しているけれど 実際はぜんぶ一緒に面倒を見ていることもあるだろう. そういう場合も svn:externals を使える. でも, それならいっそ全てのコードを一つのレポジトリにまとめてしまえばいい気はする.

プロジェクト単位でレポジトリをわけるか全部まとめるかの選択については "Subversion Best Practices" という記事が参考になるかもしれない.

メタプロジェクトとしての svn:externals

さて番外編.

必要悪としての svn:externals というのがある. たとえばレポジトリの容量が限られており, 不要なものをチェックインしたくないことがある. こういう時には svn:externals が有用だろう. 仕事ならケチるなといえるけれど, 趣味のコードを持ち歩くべく USB メモリにレポジトリを作っている, なんてケースなら妥協していいとおもう. テストデータや素材がやたらにでかいときにも同情の余地はある.

あとは既にあるレポジトリを相手にする場合. たとえば, 途中参加したプロジェクトのレポジトリが SCM 的にぐだぐだだった... というシナリオを考えてみよう.

$ svn co http://hoge.example/repos/foo
A foo.c
A foo.h
.....

trunk/ も src/ もなしにいきなりコードかよ!

こうなるとベンダーブランチのつくりようがない. (普通のブランチすら切れない.) 下手に巨大なコードをチェックインすると, 普段の ci や up が遅くなって困る. まっとうな解決策はレポジトリを作りなおすことだろう. ただ納期が一ヶ月後に迫っていて, しかも同僚はコードをまめにチェックインせず手元にためるタイプだったとする. うかつに引越しするのは混乱を招きそうで気がひける.

更に, ぐだぐだプロジェクトには当然ワンショットのビルドスクリプトなんてない. 依存ライブラリは自分でチェックアウトする必要がある. チェックアウトしてみると案の定ビルドに失敗する. 最新バージョンでは互換性が失なわれているらしい. 作業コピーのリビジョンを同僚にたずね, それをもってきてビルドすると動いたりする. ふー.

仕方なく svn:externals の出番となる.

まず, foo を包含するための meta-foo レポジトリをつくる.

...
$ svnadmin create meta-foo
...
$ svn mkdir http://hoge.example/repos/meta-foo/trunk

依存するレポジトリ(foo を含む)をまとめるディレクトリをつくり, svn:externals でリンクする. 安全なリビジョンを参照すること.

$ svn mkdir http://hoge.example/repos/meta-foo/trunk/proj/
$ svn co http://hoge.example/repos/meta-foo/trunk/
...
$ vi extern.txt
$ svn propset svn:externals -F extern.txt trunk/proj
$ svn ci trunk
...
$ svn up
...

ビルドスクリプトは meta-foo にチェックインすればいい.

$ vi trunk/Makefile
$ svn add trunk/Makefile
...
$ svn ci

このほかにメモや便利スクリプトなども meta-foo にチェックインしておく. 一ヶ月くらいならこれでしのげるんじゃないかしら...

実際には別の同僚が依存ライブラリのバージョンを上げてしまうだとか, テストデータがローカルにしかないだとか, release build に失敗するだとか, コンパイラの警告が 100 件くらい出るだとか, バグトラックがないだとか, 単体テストしにくい面倒なつくりだとか, 可搬なはずが可搬じゃないだとか, そもそも仕様がよくわかんないだとか, 積もる話は色々ありうるけれど, フィクションです.