典型的な利用方法

ブランチの作り方とsvn mergeにはいくつもの異なったやり方があり、この節では あなたが出くわしそうな一番よくあるパターンについて説明します。

ブランチ全体を別の場所にマージすること

いま、考えてきた例を完結させるため、少し時間が経過したとします。何日か 経過し、たくさんの変更がtrunkにもあなたのプライベートなブランチにも 起こったとましす。そしてあなたはプライベートなブランチ上での作業を 終えたとしましょう; 機能追加、またはバグフィッックスが完了し、他の 人がその部分を使えるようにするために、あなたのブランチ上の変更点の すべてを trunk にマージしたいとします。

さて、このような状況では、どのようにしてsvn merge を使えば良いのでしょうか? このコマンドは二つのツリーを比較し、その差分を 作業コピーに適用するものであったことを思い出してください。変更点 を受け取るためには、あなたはtrunkの作業コピーを手に入れる必要があります。 ここではあなたは(完全に更新された)もともとの作業コピーをまだ持っているか、 /calc/trunkの新しい作業コピーをチェックアウトしたもの と仮定します。

しかし、どのツリーとどのツリーを比較すれば良いのでしょうか? ちょっと考えると、 その答えは明らかに思えます: 単にtrunkの最新のツリーと、あなたのブランチの最新の ツリーです。しかし、気をつけてください — この仮定は間違い です。そしてこの間違いに、たいていの初心者はやられてしまいます! svn mergesvn diffのように働くので 最後のトランクとブランチのツリーの比較は単にあなたが自分の ツリーに対して行った変更点のみを示すものではない のがわかります。 そのような比較は、非常にたくさんの変更を表示するでしょう: それは、あなたのブランチに対する追加点だけを表示するのではなく、 あなたのブランチでは決して起こらなかった、trunk上の変更点の 取り消しも表示してしまうことでしょう。

あなたのブランチ上に起きた変更のみをあらわすには、あなたのブランチの 初期状態と、最終的な状態を比較する必要があります。 svn logコマンドをあなたのブランチ上で使えば、 そのブランチはリビジョン341で作られたことがわかります。そして、ブランチ の最終的な状態は、単に、HEAD リビジョンを指定すればわかります。 これはブランチディレクトリのリビジョン 341 と HEAD を比較しその違いを トランクの作業コピーに適用したいと考えていることを意味します。

ティップ

ブランチが作成されたリビジョンを見つけるうまい方法は (ブランチの「ベース」リビジョンのことですが)svn log--stop-on-copyオプションを利用 することです。log サブコマンドは通常ブランチに対するすべての変更を表示 し、それはブランチが作成されたコピーよりも前にさかのぼります。このた め通常トランクの履歴も表示されてしまいます。 --stop-on-copyは、svn logがターゲットのコピーあ るいは名称変更の個所を見つけると直ちにログの出力を中止します。

それで現在の例で言うと、

$ svn log --verbose --stop-on-copy ¥
          http://svn.example.com/repos/calc/branches/my-calc-branch
…
------------------------------------------------------------------------
r341 | user | 2002-11-03 15:27:56 -0600 (Thu, 07 Nov 2002) | 2 lines
Changed paths:
   A /calc/branches/my-calc-branch (from /calc/trunk:340)

$

期待したとおり、このコマンドによって表示される最後のリビ ジョンはコピーによってmy-calc-branchが作成された リビジョンになります。

結局、最終的なマージ処理は以下のようになります:

$ cd calc/trunk
$ svn update
At revision 405.

$ svn merge -r 341:405 http://svn.example.com/repos/calc/branches/my-calc-branch
U   integer.c
U   button.c
U   Makefile

$ svn status
M   integer.c
M   button.c
M   Makefile

# ...examine the diffs, compile, test, etc...

$ svn commit -m "Merged my-calc-branch changes r341:405 into the trunk."
Sending        integer.c
Sending        button.c
Sending        Makefile
Transmitting file data ...
Committed revision 406.

ここでもトランクにマージされた変更範囲についてコミットログメッセージは 非常に具体的に触れていることに注意してください。このことを常に憶えて おいてください。後になって必要になる非常に重要な情報だからです。

たとえば、独自の機能拡張やバグフィックスなどのために、もう一週間自分の ブランチ上で作業を続けることにしたとしましょう。リポジトリの HEAD リビジョンはいま 480 となり、あなたは自分のプライベートなブランチから トランクに対するマージの用意ができています。しかしマージの一番うまいやり方項で議論したように既に以前マージした 変更を再びマージしたくはありません; 最後にマージしてからブランチ上に 「新しく起きた」変更だけをマージしたいのです。問題はどうやって 新しい部分を見つけるかです。

最初のステップはトランク上でsvn logを 実行し最後にブランチからマージしたときのログメッセージを見ます:

$ cd calc/trunk
$ svn log
…
------------------------------------------------------------------------
r406 | user | 2004-02-08 11:17:26 -0600 (Sun, 08 Feb 2004) | 1 line

Merged my-calc-branch changes r341:405 into the trunk.
------------------------------------------------------------------------
…

ああ、なるほど。341 と 405 の間のリビジョンに起きたすべてのブランチ上での 変更はリビジョン 406 として既にトランクにマージされているので、それ以降にブランチ上で起きた 変更のみをマージすれば良いことがわかります— つまり、リビジョン 406 から HEAD までです。

$ cd calc/trunk
$ svn update
At revision 480.

# 現在の HEAD が 480 であることがわかったので、以下のようにマージすれ
# ばよいことになります

$ svn merge -r 406:480 http://svn.example.com/repos/calc/branches/my-calc-branch
U   integer.c
U   button.c
U   Makefile
 
$ svn commit -m "Merged my-calc-branch changes r406:480 into the trunk."
Sending        integer.c
Sending        button.c
Sending        Makefile
Transmitting file data ...
Committed revision 481.

これでトランクはブランチに起きた変更の第二波全体を含むことになりました。 この時点でブランチを削除する(これについては後で議論します)ことも、 ブランチ上で引き続き作業し、以降のマージについて上記の手続きを繰り返す こともできます。

変更の取り消し

svn mergeのほかのよくある使い方としては、既にコミット した変更をもとに戻したい場合です。/calc/trunkの 作業コピー上で作業中に、integer.cを修正したリビジョン 303 は完全に間違いであったことを発見したとしましょう。それはコミットすべき ではありませんでした。作業コピーの変更を「取り消す」のにsvn mergeを使い、その後リポジトリに対してローカルな変更を コミットすることができます。やらなくてはならないことは反対向きの 差分を指定することだけです:

$ svn merge -r 303:302 http://svn.example.com/repos/calc/trunk
U  integer.c

$ svn status
M  integer.c

$ svn diff
…
# verify that the change is removed
…

$ svn commit -m "Undoing change committed in r303."
Sending        integer.c
Transmitting file data .
Committed revision 350.

リポジトリリビジョンについてのもう一つの考え方は、それを 特定の変更のあつまりと考えることです(いくつかのバージョン管理 システムでは、これを、changesetsと 呼んでいます)。-r スイッチを使って svn merge を呼び出すことで、あるチェンジ セットを適用するか、もしくはある範囲のチェンジセット全部を作業コピーに 適用することができます。私たちの場合だとsvn merge を使ってチェンジセット#303を作業コピーに反対向きに 適用します。

このような変更の取り消しは、普通のsvn merge の操作にすぎないので、作業コピーが望む状態になったかどうかは svn statussvn diff を 使うことができ、その後svn commit でリポジトリに 最終的なバージョンを送ることができるのだ、ということを押さえておいて ください。コミット後はこの特別なチェンジセットはもはや HEADリビジョンには反映されません。

こう思うかも知れません: とすると、それは「取り消し」じゃない じゃないか。変更はまだリビジョン303に存在しているのでは、と。 もし誰かがcalc プロジェクトのリビジョン303 と 349の間のバージョンをチェックアウトしたとしたら、間違った 変更を受け取るのではないか、違うか、と。

おっしゃる通り。私たちが、変更の「取り消し」について語るとき、 本当はHEADから取り除くことを言っています。もともとの変更は リポジトリの履歴に依然として残っています。ほとんどの状況では これで十分です。とにかくほとんどの人たちはプロジェクトの HEADを追いかける ことだけに興味があるからです。しかし、コミットに関するすべての 情報を削除したいという例外的な状況もあるでしょう。(多分、誰かが 極秘のドキュメントをコミットしてしまった、など) これはそんなに やさしいことではありません。Subversionは意図的に決して情報が 失われないように設計されているからです。履歴からのリビジョンの 削除は、連鎖的な影響を与え、すべての後続リビジョンと、多分 すべての作業コピーに混乱を起こします。 [10]

削除されたアイテムの復活

バージョン管理システムの偉大なところは情報が決して失われないという ところです。ファイルやディレクトリを削除した場合でもそれは HEAD リビジョン から消えただけであり、以前のリビジョン中には依然として存在し続けます。 新規ユーザからの一番よくある質問の一つは: 「どうやって古いファイルや ディレクトリを戻せば良いのですか?」 というものです。

最初のステップはあなたが復活させようとしているものは正確には何であるかをはっきりさせることです。 うまいたとえがあります: リポジトリ中のそれぞれのオブジェクトは 一種の二次元座標系の中に存在していると考えることができます。第一の軸 は特定のリビジョンツリーで第二の軸はそのツリー中のパスです。すると ファイルあるいはディレクトリのそれぞれのバージョンは特定の座標の組で 定義することができます。

Subversion は CVS のような Attic ディレクトリを持ちません [11] ので復活させたいと思う正確な座標ペアを見つける のにsvn logを使わなくてはなりません。うまいやり方 としては削除されたアイテムがあったディレクトリでsvn log --verbose を実行することです。--verboseオプションはそれぞれの リビジョン中でのすべての変更アイテムのリストを表示します; 必要なことは ファイルやディレクトリをどのリビジョンで削除したかを調べることです。 これはビジュアルにやることもできますし、ログ出力を解析する別のツールを 使うこともできます(grepコマンドを通じて、あるいは エディタでのインクリメンタル検索機能を使う形かも知れません)。

$ cd parent-dir
$ svn log --verbose
…
------------------------------------------------------------------------
r808 | joe | 2003-12-26 14:29:40 -0600 (Fri, 26 Dec 2003) | 3 lines
Changed paths:
   D /calc/trunk/real.c
   M /calc/trunk/integer.c

Added fast fourier transform functions to integer.c.
Removed real.c because code now in double.c.
…

例では削除してしまったファイル real.cを探している とします。親ディレクトリのログを見ることでこのファイルはリビジョン 808 で削除されたことを突き止めました。それでこのファイルが存在していた 最後のバージョンはそのリビジョンの直前であることになります。結論: リビジョン 807 から /calc/trunk/real.cのパスを 復活させれば良いことになります。

これが面倒な部分です — つまりファイルを見つける作業です。 これで復元したいものが何であるか突き止めました。後は二つの方法が あります。

最初のやり方はリビジョン 808 を 「逆向きに」 適用するために svn mergeを利用することです。(変更の取り消し の仕方については既に議論しました。 変更の取り消し項 を参照してください。) これはローカルな変更としてreal.c をもう一度追加する効果があります。ファイルは追加予告され、コミット後には HEAD 上に再び存在するようになります。

しかしこの例は多分最善の方法ではないでしょう。リビジョン 808 の逆向き の適用はreal.cの追加予告だけではなく、ログメッセージ が示すように、今回必要としないinteger.cへの変更点 も取り消してしまいます。確かにリビジョン 808 を逆向きにマージした後 integer.cのローカル変更を svn revert することもできますが、この技法はファイルが多くなるとうまくスケールしません。 リビジョン 808 で 90 個のファイルが変更されていたとしたらどうなりますか?

もっと洗練された二番目の方法はsvn mergeは利用せず、 そのかわりにsvn copyコマンドを使います。正確な リビジョンとパスの 「座標の組」 を指定してリポジトリから自分の作業コピーに 単にコピーするだけです:

$ svn copy --revision 807 ¥
           http://svn.example.com/repos/calc/trunk/real.c ./real.c

$ svn status
A  +   real.c

$ svn commit -m "Resurrected real.c from revision 807, /calc/trunk/real.c."
Adding         real.c
Transmitting file data .
Committed revision 1390.

ステータス表示中のプラス記号はそのアイテムは単に追加予告されただけではなく 「履歴と共に」 追加予告されたことを示しています。Subversion はどこからそれが コピーされたかを記憶しています。今後このファイル上に svn logを実行するとファイルの復活についてと、リビジョン 807 以前のすべての履歴をたどることができます。言いかえるとこの新しい real.cは本当に新しいわけではありません;それは 削除されたもとのファイルの直接の子孫になっています。

私たちの例はファイルの復活でしたが、同じ技法が削除されたディレクトリ の復活についても利用できることに注意してください。

ブランチの作り方

バージョン管理システムはソフトウェア開発で一番よく使われるので、ここで 何かの開発チームによって利用される典型的なブランチ化/マージのパターン をちょっと見てみましょう。Subversion をソフトウェア開発に使うのでなけ ればこの節は読み飛ばしてもかまいません。ソフトウェア開発にバージョン 管理システムを使うのが初めてなのであれば、よく読んでください。 ここでのパターンは経験を積んだ多くの開発者によって最良の方法だと考えられて いるからです。このようなやり方は Subversion に限った話ではありません; どのようなバージョン管理システムにでも応用できる考え方です。また同時に 他のシステムのユーザに対しては Subversion ではどんな言葉を使ってこの標 準的なやり方を表現するかを理解する手がかりになるでしょう。

リリースブランチ

ほとんどのソフトウェアは典型的な作業サイクルがあります: コーディング、 テスト、リリース、この繰り返しです。このようなやり方には二つの問題が あります。まず開発者は新しい機能を追加し続けなくてはならない一方で 品質保証チームはそのソフトウェアの安定版だと考えられるバージョンを テストするのに時間をついやさなくてはなりません。テスト途中だからといっ て新しい機能追加を中断することはできません。次に開発チームはほとんど の場合、すでにリリースされた古いバージョンのソフトウェアを保守しなくては なりません; もし最新のコードにバグが見つかった場合、すでにリリースしている バージョンにも同じバグが潜んでいる可能性は高く、利用者は次のリリースを 待たずにこのバグを修正して欲しいと望んでいることでしょう。

バージョン管理システムの出番です。典型的なやり方は以下のようなものです:

  • 開発者は新規開発部分をトランクにコミットします。 日々の変更点は/trunkにコミットされます: 新しい機 バグ修正、その他もろもろです。

  • トランクの内容は「リリース」ブランチにコピーされます。 あるチームが、そのソフトウェアがリリースできる状態になったと考えた 時点で(つまり、1.0 のリリースのような場合)、/trunk/branches/1.0のような名前でコピーされる ことになります。

  • これと並行して、他のチームが作業を続けます。 あるチームがリリースブランチの内容を徹底的なテストを開始する一方で 他のチームは新規開発分(つまり、バージョン 2.0 に向けた作業)を /trunk上で継続して行います。どちらかの場所で バグが見つかれば、必要に応じてその修正がお互いの間を行き来します。 しかしこの作業もやがては終わります。このブランチはリリース直前の 最終的なテストに向けて「凍結」されます。

  • ブランチはタグづけされ、リリースされます。 テストが完了したら/branches/1.0/tags/1.0.0にコピーされ、これが参照用のスナップ ショットになります。このタグの内容はパッケージ化され、利用者に対して リリースされます。

  • ブランチはその後も保守されます。 バージョン 2.0に向けた作業が/trunk上で進む一方、 バグ修正個所については/trunkから /branches/1.0に引き続き反映されます。 十分なバグ修正が反映されたら、管理者は 1.0.1 をリリースする決断をする かも知れません: /branches/1.0/tags/1.0.1にコピーされ、このタグはパッケージ 化されてからリリースされます。

このような作業の流れを繰り返すことでソフトウェアは安定していきます: 2.0 の開発が完了したら新しい 2.0 のリリースブランチが作られ、テスト され、タグがつけられ、最終的にリリースされることになります。 何年かしてリポジトリは「保守対象」の状態になったいくつかの リリースブランチと最終的にリリースされたバージョンを示すタグの集まり になるでしょう。

(特定機能の)開発用ブランチ

開発用ブランチ(feature branch) はこの章での 例として中心的な役割を果たしてきたようなタイプのブランチで、その ブランチ上であなたが作業をするのと同時に並行してSally は /trunk上で作業を継続することができるような ものでした。それは一時的なブランチで、安定している /trunkに影響を与えることなく複雑な変更をする ためのものです。リリース用ブランチ(これはずっと保守しつづけなければ ならないかも知れません)とは違って、開発用ブランチは作成されたあと ある程度の期間利用され、変更部分がトランクに反映された後で完全に 削除されてしまいます。利用されるのは、ある決まった期間の中だけです。

プロジェクトの考え方によって、開発用ブランチをいつ作るのが適切である かにはかなりの幅があります。プロジェクトによっては開発用ブランチを 全く使いません: /trunkに対するコミットは 全員に許されています。このやり方の長所はその単純さです—誰も ブランチ化やマージについて理解する必要がありません。欠点はこの方法 だとトランクのソースコードが不安定になったりまったく利用できなく なったりしやすいことです。逆に別のプロジェクトではブランチを極端な 形で使います: どんな変更もトランクに対して直接 コミットすることは認められていません。まったくささいな変更に対しても 短い生存期間をもつブランチを作り、それを注意深く検討し、 トランクに反映させます。それから、そのブランチを削除します。この方法は トランクを常に非常に安定して利用できる状態に置くことができますが それには無視できない処理効率の低下が伴います。

ほとんどのプロジェクトではこの中間のやり方をとります。普通は /trunkは常にコンパイル可能な状態であり、 一度フィックスしたバグが元に戻っていないことを保証するためのテスト もクリアした状態にあることを要求します。ある変更をするのに プログラムを不安定にするようなコミットを何度も必要とする場合に だけ開発ブランチが作られます。基本的な方針としては次のようなことを 考えてみることです: もし開発者が孤立した状態で何日も作業した後で 一度に変更点全体をコミットしたとしたら(/trunkが 不安定にならないようにするためにそうするのでしょうが)、その変更 内容が正しいかどうかを検討するには大きすぎませんか? もし答えが 「イエス」なら、その変更は開発用ブランチでやるべきで しょう。開発者はブランチに対して変更点を少しずつコミットするので 他の人たちはそれぞれの部分について簡単に内容を検証することができ ます。

最後に、開発用ブランチでの作業が進むにつれて、どうやってそれを トランクの内容に「同期」させるのがよいかについて 考えてみます。すでに注意したようにブランチ上で数週間あるいは数ヶ月 ものあいだ作業しつづけるのには大きなリスクが伴います; トランクへの 変更はその間次々と発生し、ついには二つの開発ラインはあまりにもかけ離れて しまい、ブランチの変更内容をトランクにマージによって戻すのは全く 非現実的な話になってしまうかも知れないのです。

この状況を避けるためにはトランクの内容を定期的にブランチにマージする ことです。次のようなルールを決めておきましょう: 一週間に一度、 先週トランク上におきた変更をブランチにマージすること。これは注意して 実行する必要があります; マージは手作業で実行し、繰り返してマージする のを避ける必要があります(これについては 手でマージする方法項で説明しました)。ログメッセージ を書く時には注意して、どの範囲のリビジョンが既にマージされているか を正確に控えておきましょう(これはブランチ全体を別の場所にマージすること項 でやってみせました)。大変な作業に思えるかも知れませんが、実際には 非常に簡単なことです。

あるところまで作業が進んだら、開発用ブランチの内容をトランクに 「同期」させるためのマージの準備が整います。これには まず、最新のトランクの変更部分をブランチに取り込む最後のマージ 処理を実行することで始めます。この処理の後では、ブランチ上の最後のリビ ジョンとトランク上の最後のリビジョンは、ブランチでの変更部分を のぞけば、完全に同じ状態になります。このような特定の状況下では ブランチとトランクの内容を比較することによってマージすることが できるはずです:

$ cd trunk-working-copy

$ svn update
At revision 1910.

$ svn merge http://svn.example.com/repos/calc/trunk@1910 ¥
            http://svn.example.com/repos/calc/branches/mybranch@1910
U  real.c
U  integer.c
A  newdirectory
A  newdirectory/newfile
…

トランクのHEADリビジョンとブランチの HEADリビジョンを比較することで、ブランチにだけ 加えた修正点を含む差分を作ることができます; 両方の開発ラインとも トランクに起きた修正についてはすでに取り込んでいるからです。

このような作業パターンは、自分のブランチに対して 毎週トランクを同期させる処理は、作業コピーに対してsvn update を実行するのとよく似ていて、最後のマージ処理に ついては作業コピーからsvn commitを実行するのに よく似ていると考えることができます。結局、作業コピーと、ちょっと作った プライベートなブランチと、他に何が違うと言うのでしょう? 作業コピー とは、一度に一つの変更しか保存できないような単なるブランチにすぎ ません。



[10] しかしながら、Subversionプロジェクトはいつの日か svnadmin obliterateコマンドを 実装する計画があります。これは情報の完全な消去を実行するコマンド です。それまでは回避策としてsvndumpfilter項 の方法を利用してください。

[11] CVS はツリーのバージョン管理ができないので削除されたファイルを 記憶しておくためにリポジトリ用のディレクトリ中に Attic 領域を 作ります。