典型的な利用方法

ブランチの作り方と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 -v --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.

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

変更の取り消し

Another common use for svn merge is to roll back a change that has already been committed. Suppose you're working away happily on a working copy of /calc/trunk, and you discover that the change made way back in revision 303, which changed integer.c, is completely wrong. It never should have been committed. You can use svn merge to 「undo」 the change in your working copy, and then commit the local modification to the repository. All you need to do is to specify a reverse difference. (You can do this by specifying --revision 303:302, or by an equivalent --change -303.)

$ svn merge -c -303 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.

One way to think about a repository revision is as a specific group of changes (some version control systems call these changesets). By using the -r option, you can ask svn merge to apply a changeset, or whole range of changesets, to your working copy. In our case of undoing a change, we're asking svn merge to apply changeset #303 to our working copy backwards.

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

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

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

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

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

The first step is to define exactly which item you're trying to resurrect. Here's a useful metaphor: you can think of every object in the repository as existing in a sort of two-dimensional coordinate system. The first coordinate is a particular revision tree, and the second coordinate is a path within that tree. So every version of your file or directory can be defined by a specific coordinate pair. (Remember the 「peg revision」 syntax—foo.c@224 —mentioned back in ペグ・リビジョンと操作対象リビジョン項.)

First, you might need to use svn log to discover the exact coordinate pair you wish to resurrect. A good strategy is to run svn log --verbose in a directory which used to contain your deleted item. The --verbose (-v) option shows a list of all changed items in each revision; all you need to do is find the revision in which you deleted the file or directory. You can do this visually, or by using another tool to examine the log output (via grep, or perhaps via an incremental search in an editor).

$ cd parent-dir
$ svn log -v
…
------------------------------------------------------------------------
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 -r 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を実行するのに よく似ていると考えることができます。結局、作業コピーと、ちょっと作った プライベートなブランチと、他に何が違うと言うのでしょう? 作業コピー とは、一度に一つの変更しか保存できないような単なるブランチにすぎ ません。



[23] The Subversion project has plans, however, to someday implement a command that would accomplish the task of permanently deleting information. In the meantime, see svndumpfilter項 for a possible workaround.