miauのブログ

はてなダイアリー「miauの避難所」をはてなブログに移行しました

ベンダブランチの管理方法

サードパーティ製のコード(たとえば CakePHP)をプロジェクトのリポジトリに取り込んで使うような場合、バージョンアップが結構手間になります。バージョンアップの度に増減したファイルを svn add や svn delete するのは面倒ですし、ファイル名の変更をちゃんとトレースしたいなら svn rename もやりたくなるでしょう。また、CakePHP のソース自体に手を加えたいような場合、マージ作業も面倒になります。

こういう場合ベンダブランチや svn_load_dirs.pl というスクリプトを使うと、比較的楽に管理できます。svnbook でも

という感じで軽く触れられているのですが、どうも利用している方が少ない気がするので、CakePHP を例にシナリオベースで利用方法を書いてみます。Windows での実行方法を書きますけど、他の環境ならもっと楽だと思います。

(2011-08-09 追記)

長い手順を書いたので、この管理方法が面倒だという印象を与えてしまっている気がしてきました・・・。svn_load_dirs.pl を使うのはバージョンアップのときだけです。とりあえずこの運用を始めたい、というだけであれば TortoiseSVN だけでも完結する作業ですので、気軽にお試しください。

(2011-08-09 追記 ここまで)

参考書籍

Subversion実践入門:達人プログラマに学ぶバージョン管理(第2版)

Subversion実践入門:達人プログラマに学ぶバージョン管理(第2版)

この本の「第11章 サードパーティ製のコード」(初版では第10章)で詳しく解説されています。

以下は自分なりにノウハウ等を取り込んでまとめなおしたつもりですが、これ以外の情報源がほとんどないので、丸コピーに近い感じになっていたらごめんなさい。

前準備

perlsvn のインストール

実行には perl の実行環境と svnCUI クライアントが必要です。

perl の環境には ActivePerl をよく使っていますが、Strawberry Perl を使ってもいいでしょう。

svn クライアントは CollabNet Subversion を使っていますが、ユーザ登録が必要なので、subversion: Subversion Packages から適当に他のものを拾ってもいいかもしれません。

svn_load_dirs.pl の取得

で公開されていますので、このディレクトリをチェックアウトしてしまいます。

# 2010-03-23 リポジトリ移動につき URL 修正
# svn co http://svn.collab.net/repos/svn/trunk/contrib/client-side/svn_load_dirs/
svn co http://svn.apache.org/repos/asf/subversion/trunk/contrib/client-side/svn_load_dirs/


svn で取得するのが面倒な場合は、

を直接ダウンロードする形でもいいでしょうし、svn のソースが手元にある場合は、その中にも含まれているはずです。

svn_load_dirs.pl の編集

取得ファイルはファイル名末尾に .in がついていますが、このまま実行せずに sed なんかで書き換えて使えということみたいです。
Windowssed を入れている方も少ないでしょうし、ここでは svn_load_dirs.pl という名称でコピーして、そのファイルを編集することにします。svn で取得した場合は、diff が行いやすいように以下のようにしてコピーするのがオススメです。

>svn cp svn_load_dirs.pl.in svn_load_dirs.pl
A         svn_load_dirs.pl


編集内容ですが、svn_load_dirs.pl の 26 行目くらいに、

# Specify the location of the svn command.
my $svn = '@SVN_BINDIR@/svn';

のような箇所があるので、svn.exe のパスに書き換えてしまいます。

my $svn = 'D:\CollabNet Subversion\svn';

のような感じです。


また、186 行目に

elsif ( -f "$ENV{HOME}/.subversion/config" )

という行がありますが、環境変数 HOME を設定していな環境の場合

Use of uninitialized value in concatenation (.) or string at svn_load_dirs.pl.in line 178.

みたいな警告が出て鬱陶しいので、以下のように書き換えておいたほうがいいかもしれません。

elsif ( exists($ENV{HOME}) && "$ENV{HOME}/.subversion/config" )


さらに、Perl のバージョンによっては(?)

Argument "2.07_02" isn't numeric in subroutine entry at D:\svn_load_dirs\svn_load_dirs.pl line 16.

こんな警告も発生します。このままでも動くのですが、気持ち悪いので 16 行目の

use File::Path   1.0404;

use File::Path;

に書き換えてしまいます。たぶんそんな古いバージョンの File::Path を入れている人はいないから大丈夫でしょう。

リポジトリの初期設定

以降 svn コマンドで処理していますが、svn_load_dirs.pl 以外の箇所は TortoiseSVN で実行しても大丈夫です。

src ディレクトリの作成

自分のプロジェクト用にソース用ディレクトリを作成します。構成は色々あると思いますが、ここでは深く考えずに src 配下に trunk/branches/tags を作っておきます。(プロジェクト名が hoge で、http://svn_server/hogeリポジトリの URL です。)

>svn co http://svn_server/hoge
>cd hoge
>svn mkdir src src/trunk src/branches src/tags
A         src
A         src\trunk
A         src\branches
A         src\tags

>svn ci -m "ソースファイル格納用のディレクトリを作成。"
Adding         src
Adding         src\branches
Adding         src\tags
Adding         src\trunk

Committed revision 1.
サードパーティ製コードの格納先を作成

サードパーティ製のコードをうまく扱いたいわけですが、まず原則として「入手したサードパーティ製のコードは、手を加えずにそのままリポジトリに格納する」というのがあります。そうしないとバージョンアップの際に変更点をマージできません。

また、サードパーティ製のコードは自プロジェクトのソースとライフサイクルが異なるので、trunk/tags/branches の外に配置します。参考書籍では、複数の供給元からインポートする場合は

というような名称を推奨しています。もし供給元がひとつだけだとわかっている場合は /cake のようにそのプロジェクト用のディレクトリを作成する形でもいいでしょう。

扱う言語が PHP の場合、/vendor はあまり必要ありませんので /vendorsrc だけ作っておきます。

>svn mkdir vendorsrc
A         vendorsrc

>svn ci -m "ベンダブランチ用のディレクトリを作成。"
Adding         vendorsrc

Committed revision 2.

CakePHP のソースを取り込み

では早速 CakePHPソースコードを vendorsrc に取り込んでみましょう。

書籍によると

/vendorsrc/(ベンダ名)/(プロダクト名)/current

のような名称で取り込むことになっていますが、ここではベンダ名とプロダクト名を一緒くたに「cake」として取り込むことにします。ちなみにこの「/vendorsrc/(ベンダ名)」のようなベンダ/プロダクト毎のディレクトリを「ベンダブランチ」、そこに格納するコード(current 等)を「ベンダドロップ」と呼ぶようです。

CakePHP 1.2.2.8120 のソース(.zip や .tar.gz を展開したもの)が E:\sources\cake_1.2.2.8120 にあるとして・・・

>svn import --no-auto-props -m "CakePHP 1.2.2.8120 をインポート。"  E:\sources\cake_1.2.2.8120 http://svn_server/hoge/vendorsrc/cake/current
E:\sources\cake_1.2.2.8120 http://svn_server/hoge/vendorsrc/cake/current
Adding         E:\sources\cake_1.2.2.8120\app
Adding         E:\sources\cake_1.2.2.8120\app\locale
Adding         E:\sources\cake_1.2.2.8120\app\locale\eng
:
Adding         E:\sources\cake_1.2.2.8120\README

Committed revision 3.

ここで current という名称を使っていますが、通常でいうところの trunk のイメージです。サードベンダの人がここを trunk として使っている様をシミュレートしていると思えばいいでしょう。

なお、ここで --no-auto-props オプションを指定していますが、

  • CVS で管理されているソースを取り込むような場合、ソースに $Author$ や $Id$ が含まれているケースがある
  • auto-props で svn:keywords を自動的に設定している場合はこれらがキーワード展開されてしまう
  • --no-auto-props を指定することでそれを抑止できる

という理由で指定しているそうです。上記の条件に当てはまらないなら指定しなくても問題ないでしょう。(なので、TortoiseSVN で実行しても問題ありません。)

取り込んだバージョンは 1.2.2.8120 なので、このバージョンのタグも作成しておきます。current と同階層に置くことを除けば、普通の trunk/tags の運用と同じですね。

>svn copy -m "CakePHP のベンダドロップに 1.2.2.8120 とタグ付け。" http://svn_server/hoge/vendorsrc/cake/current http://svn_server/hoge/vendorsrc/cake/1.2.2.8120

Committed revision 4.

この時点でリポジトリの構成はこんな感じになっています。

/vendorsrc
└─cake
    ├─1.2.2.8120
    └─current

CakePHP のソースを trunk 配下にコピー

普通に SVN 上でコピーします。

>svn copy -m "CakePHP を src/trunk/htdocs として取り込み。" http://svn_server/hoge/vendorsrc/cake/1.2.2.8120 http://svn_server/hoge/src/trunk/htdocs

Committed revision 5.

以降バージョンアップまでは trunk/htdocs 配下で普通に開発を進めます。

CakePHP のバージョンアップ

ここからが本題。CakePHP 1.2.2.8120 で開発を進めていたところに、CakePHP 1.2.4.8284 が公開されて、バージョンアップしたくなったとします。

そのような場合にまず最新のソースを取り込む先は /vendorsrc/cake/current です。/vendorsrc/cake/1.2.4.8284 のような形で格納したくなるかもしれませんが、バージョン表記のディレクトリはタグのように使っているわけですから、まずは trunk と同じように使われている current に変更を取り込み、それにタグ付けする形で 1.2.4.8284 のディレクトリを作成すべきです。

こうすることの利点は(普通のタグの利点と同じですが)、SVN 上で同一ファイルの変更として扱えることです。1.2.4.8284 にいきなり放り込んだ場合は別ファイルとして扱われるので差分情報が取得できませんし、リポジトリ上で余計な容量を食うことになります。

バージョンアップによりファイルの追加や削除が無い場合は /vendorsrc/cake/current を上書きしてしまえばいいんですが、そうでない場合は

  • 追加されたファイルを svn add
  • 削除されたファイルを svn delete
  • リネームされたファイルを svn rename

する必要があって面倒です。svn_load_dirs.pl を使うことでこの作業を自動化できますので、これを使うことにします。

svn_load_dirs.pl の実行

svn_load_dirs.pl で指定する引数は以下のとおりです。(使い方は svn_load_dirs.README にも詳しく載ってます。)

  • -t オプション
    • 作成するタグの名前。ここでは取り込むバージョンである 1.2.4.8284 を指定。
  • 第一引数
  • 第二引数
  • 第三引数

ということで、今回の場合は

cd /d E:\sources
perl D:\svn_load_dirs\svn_load_dirs.pl -t 1.2.4.8284 http://svn_server/hoge/vendorsrc/cake/ current cake_1.2.4.8284

こんな感じで実行します。引数の説明を見る限りは cd でカレントディレクトリを変更せずに

perl D:\svn_load_dirs\svn_load_dirs.pl -t 1.2.4.8284 http://svn_server/hoge/vendorsrc/cake/ current E:\sources\cake_1.2.4.8284

こんな感じにしても実行できそうなもんですが、第三引数で指定した値はそのままコミットログで使われてしまいます。後者の実行方法だと、

Load E:\sources\cake_1.2.4.8284 into vendorsrc/cake/current.

のように絶対パスの情報がコミットログに残ってなんだか嫌なので、前者の方法で実行しています。

実行すると

Directory cake_1.2.4.8284 will be tagged as 1.2.4.8284
Please examine identified tags.  Are they acceptable? (Y/n)

と聞かれます。ここで Enter を押せば、最新版が /vendorsrc 配下に取り込まれ、1.2.4.8284 のタグも作成されます。

最新バージョンの trunk への取り込み

まだ trunk 上の CakePHP は古いままなので、/vendorsrc からマージしてしまいます。

まず、マージ先が変更されていない状態であることを確認しておきます。

svn stat src/trunk/htdoc

もし変更されていたら、コミットなり取り消しなり別ディレクトリにチェックアウトしなおすなりしてください。

で、マージです。

svn merge http://svn_server/hoge/vendorsrc/cake/1.2.2.8120 http://svn_server/hoge/vendorsrc/cake/1.2.4.8284 src/trunk/htdocs

もし CakePHP のソースに手を加えている場合は衝突が発生する場合もありますが、普通はすんなりマージできると思います。

問題なければコミットです。

svn commit -m "src/trunk/htdocs 配下の CakePHP を 1.2.2.8284 に更新。" src/trunk/htdocs

このように svn_load_dirs.pl を使って /vendorsrc 上に各バージョンのファイルを配置することで、trunk 上の CakePHP の更新をマージで行うことができるようになります。そのため、CakePHP の本体に手を加えた場合でも、特に意識することなくバージョンアップが行えることになります。

そんなわけで私は CakePHP に含まれている app ディレクトリをそのまま使って&必要に応じて cake 配下にも手を入れて開発することが多いです。複数のアプリケーションを管理する場合は別の手段を考えたほうがいいのかもしれませんけど。

CakePHP 以外のファイル

上記は CakePHP の例で説明しましたが、実際はそれ以外のライブラリのほうが多いです。ちょっと前のプロジェクトの例だとこんな感じです。(実際はこれ以外のディレクトリもありますが、省略しています。)

/
├─src
│  ├─branches
│  ├─tags
│  └─trunk
│      └─htdocs
│          ├─app
│          ├─cake
│          └─vendors
└─vendorsrc
    ├─cake
    │  ├─1.2.1.8004
    │  ├─1.2.2.8120
    │  └─current
    ├─cakephp-db-migrations
    │  ├─2009-02-23
    │  └─current
    ├─pear
    │  ├─MDB2
    │  │  ├─2.5.0b2
    │  │  └─current
    │  ├─MDB2_Driver_pgsql
    │  │  ├─1.5.0b2
    │  │  └─current
    │  ├─PEAR
    │  │  ├─1.7.2
    │  │  └─current
    │  └─PHP_CodeSniffer
    │      ├─1.1.0
    │      ├─1.1.0RC2
    │      └─current
    ├─prototype
    │  ├─1.6.0.3
    │  └─current
    ├─selenium
    │  ├─core
    │  │  ├─1.0-beta-2
    │  │  └─current
    │  └─remote-control
    │      ├─1.0-beta-2
    │      └─current
    └─simpletest
        ├─1.0.1
        └─current

こういった諸々のライブラリは /src/trunk/htdocs ではなく /src/trunk/htdocs/vendors に取り込んだり、Selenium Core だったら /src/trunk/htdocs/app/webroot/selenium に取り込んだり色々ですが、まあ手順は上記のとおりです。

リネームが発生するケース

svn_load_dirs.pl を使えばリネームも管理できる」と言いつつ、上記ではそのようなケースがなかったので別の例で説明します。

受領ファイルの管理

/vendorsrc の運用に慣れてくると、ソースコード以外の外部リソースについて似たような管理をしたくなってきたりします。たとえば別会社にデザインを依頼していて、定期的でメールで受け取るような場合にも上記の方法は有効です。

このようなファイルについても trunk/branches/tags とライフサイクルが異なる&マージが必要になるものですので、/received(受領ファイル)のようなディレクトリを作成し、そこで管理するようにしています。今回はデザイン関連のファイルなので /received/design のような形ですね。

svn_load_dirs.pl によるリネーム

さて、svn_load_dirs.pl で最新のデザインを取り込むところから。

>perl D:\svn_load_dirs\svn_load_dirs.pl -t 2008-10-30 http://svn_server/hoge/received/design/ current design/2008-10-30

もし特定のファイルがなくなり、代わりに新規ファイルが追加されていた場合、svn_load_dirs.pl.in はそれがリネームされたものかどうかの確認を行います。

The following table lists files and directories that
exist in either the Subversion repository or the
directory to be imported but not both.  You now have
the opportunity to match them up as renames instead
of deletes and adds.  This is a Good Thing as it'll
make the repository take less space.

The left column lists files and directories that
exist in the Subversion repository and do not exist
in the directory being imported.  The right column
lists files and directories that exist in the
directory being imported.  Match up a deleted item
from the left column with an added item from the
right column.  Note the line numbers on the left
which you type into this script to have a rename
performed.

     Deleted     Added
   0 login.html_ common/images/arrow_process.gif
   1 ___________ common/images/arrow_top.gif
(snip)
  17 ___________ c001.html
  18 ___________ c099.html
(snip)
  36 ___________ common/images/title_top.gif
  37 ___________ common/images/title_top_small.gif
Enter two indexes for each column to rename, (R)elist, or (F)inish: 

このケースでは、login.html が c001.html にリネームされています(これは内容から判断してください)ので、0 17 のように入力します。すると svn rename login.html c001.html が実行された後でファイルの更新が行われますので、変更がちゃんとトレースできるというわけです。

vendorsrc のときと同じように、これを trunk 配下の app/views ディレクトリにマージして使ったりします。

SVN 以外の場合は・・・

使ったことないんですけど、vcs-load-dirs というのを使えば Mercurial や Git その他もろもろでも同じ処理ができるらしいです。

でも Mercurial には hg addremove -s があるし、Git はファイルのリネームはトレースしない設計のはずだし、本当に意味があるのかイマイチわかってません。詳しい方は解説していただけると。