ブランチをまたいで変更をコピーすること

さて、あなたとSallyはプロジェクト上の平行したブランチで作業しています。 あなたは自分のプライベートなブランチで作業していて、Sally は trunk、あるいは、開発の主系の上で作業していると します。

たくさんの貢献者がいるようなプロジェクトでは、ほとんどの人たちは trunkのコピーを持っているのが普通です。trunk を壊してしまうかも知れない ような長い期間をかけての変更を加える必要がある場合は常に、標準的な手続き としてはまずプライベートなブランチを作り、すべての作業が完了するまで変更 点をそのブランチにコミットします。

そのようなやり方の利点としては、二人の作業はお互いに干渉しないところです。 欠点は二人の作業内容はすぐにひどく 違っていって しまうことです。「引きこもり」戦略の問題の一つは自分のブランチの作業が 完了するときに起こることを思い出してください。恐ろしくたくさんの衝突 なしに、あなたの変更をtrunkにマージするのはほとんど不可能でしょう。

そのかわりに、作業中に、あなたとSallyは変更を共有し続けるのが良い でしょう。どのような変更が共有する価値があるのかはあなたが決める ことです。Subversionを使うとブランチ間の選択的な「コピー」ができます。 そしてブランチ上での作業が完全に終ったら、ブランチ上にした変更点の 全体をtrunkに書き戻すことができます。

特定の変更点のコピー

前の節で、あなたとSallyは別ブランチ上でinteger.c に変更を加えたと言いました。もしリビジョン344のSallyのログメッセージ を見れば、何かのスペルミスを直したことがわかるかも知れません。 この場合間違いなく、同じファイルのあなたのコピーもやはり同じスペルミスが あるはずです。このファイルに対する今後のあなたの修正はスペルミスのある 場所に影響を与えるかも知れず、自分のブランチをいつかマージするときに は衝突が起こってしまいます。そうなるくらいなら、あまりひどいことになる 前に、Sallyの修正をいま受け取ったほうが良いでしょう。

svn merge コマンドを使うときがやってきました。 このコマンドは、 svn diff に非常に近い 親戚だということがわかります。(このコマンドは第3章で説明しました)。 両方ともリポジトリ中の二つのオブジェクトを比較して、その差を 調べることができます。たとえばsvn diff に Sallyがリビジョン344でやった変更点を正確に表示することができます:

$ svn diff -r 343:344 http://svn.example.com/repos/calc/trunk

Index: integer.c
===================================================================
--- integer.c	(revision 343)
+++ integer.c	(revision 344)
@@ -147,7 +147,7 @@
     case 6:  sprintf(info->operating_system, "HPFS (OS/2 or NT)"); break;
     case 7:  sprintf(info->operating_system, "Macintosh"); break;
     case 8:  sprintf(info->operating_system, "Z-System"); break;
-    case 9:  sprintf(info->operating_system, "CPM"); break;
+    case 9:  sprintf(info->operating_system, "CP/M"); break;
     case 10:  sprintf(info->operating_system, "TOPS-20"); break;
     case 11:  sprintf(info->operating_system, "NTFS (Windows NT)"); break;
     case 12:  sprintf(info->operating_system, "QDOS"); break;
@@ -164,7 +164,7 @@
     low = (unsigned short) read_byte(gzfile);  /* read LSB */
     high = (unsigned short) read_byte(gzfile); /* read MSB */
     high = high << 8;  /* interpret MSB correctly */
-    total = low + high; /* add them togethe for correct total */
+    total = low + high; /* add them together for correct total */
 
     info->extra_header = (unsigned char *) my_malloc(total);
     fread(info->extra_header, total, 1, gzfile);
@@ -241,7 +241,7 @@
      Store the offset with ftell() ! */
 
   if ((info->data_offset = ftell(gzfile))== -1) {
-    printf("error: ftell() retturned -1.¥n");
+    printf("error: ftell() returned -1.¥n");
     exit(1);
   }
 
@@ -249,7 +249,7 @@
   printf("I believe start of compressed data is %u¥n", info->data_offset);
   #endif
   
-  /* Set postion eight bytes from the end of the file. */
+  /* Set position eight bytes from the end of the file. */
 
   if (fseek(gzfile, -8, SEEK_END)) {
     printf("error: fseek() returned non-zero¥n");

svn merge コマンドもほとんど同じです。差分を 画面に表示するかわりに、それはローカルな 修正分として直接あなたの作業コピーに適用 します:

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

$ svn status
M  integer.c

svn merge の出力は、あなた用の integer.c のコピーがパッチされた 結果です。これでSallyの変更が含まれるようになりました— それはtrunkからあなたのプライベートなブランチの作業コピーに 「コピー」され、ローカルな修正の一部となりました。この修正を再検討し、 正しく動作することを確認するのはあなたの仕事です。

別のシナリオとして、そんなにうまくはいかず、 integer.c が衝突の状態になることもあります。 標準的な方法を使って衝突を解消するか(第3章を見てください)、 結局マージが悪いアイディアだったと思ったときには、あきらめて svn revert でローカルの変更を取り消すことも できます。

しかし、マージされた変更を確認して、svn commit をかけるのが普通です。これで、変更は自分のリポジトリブランチに マージされました。バージョン管理の言い方では、このようなブランチ間の 修正点のコピーを、普通porting による変更と いいます。

ローカルな修正をコミットするときには、あるブランチから別のブランチ に対して特定の変更を移したことを示すようなログメッセージになって いることを確認してください。たとえば:

$ svn commit -m "integer.c: ported r344 (spelling fixes) from trunk."
Sending        integer.c
Transmitting file data .
Committed revision 360.

次の節で見るように、これは参考にすべき 「最善の方法」 です。非常に重要です。

注意: svn diffsvn merge は とてもよく似たコンセプトを持っていますが、いろいろな場合で 別の構文になります。関連した第9章をよく読むか、svn help を使ってください。たとえばsvn mergeは作業コピー パスを引数とします。つまりツリーの変更を適用する場所の指定が必要 になります。この指定がなければ、よく利用される以下の操作のどちらか を実行しようとしているとみなされます:

  1. 現在の作業ディレクトリ中に、ディレクトリの変更点をマージ しようとしている。

  2. 現在の作業ディレクトリ中にある同じ名前のファイルに対して、 ある特定のファイルに起きた修正をマージしようとしている。

ディレクトリをマージしようとしている場合で、目的のパスを指定しなかった 場合、svn mergeは、上にあげた第一の場合であると みなし、現在のディレクトリ中のファイルに対して適用しようとします。 もし、ファイルをマージしようとしている場合で、そのファイル(または 同じ名前のファイル)が作業コピーディレクトリに存在している場合、 svn mergeは第二の場合であるとみなし、同じ名前の ローカルファイルに対して変更を適用しようとします。

上記以外の場所に適用したい場合には そのことを明示的に指定する必要があります。たとえば作業コピーの親ディレクトリ にいて、変更を受け取るための対象ディレクトリを指定する必要がある場合なら:

$ svn merge -r 343:344 http://svn.example.com/repos/calc/trunk my-calc-branch
U   my-calc-branch/integer.c

マージの基本的な考え方

ここまでのところで svn merge の例を見てきましたが、さらにいくつかの例を あげます。マージが本当のところどのように機能するかについて何か混乱 した気になるのは何もあなただけではありません。多くのユーザは(特に バージョン管理システムになじみのない人にとっては) まず最初にコマンド の構文に戸惑い、さらにどのようにして、またいつその機能をつかえば良い かということにも戸惑います。しかし怖がることは何もありません。このコマンドは 実際にはあなたが思っているよりずっと単純なものです。svn merge がどのように動作するかを正確に知るためのとても簡単な方法があります。

混乱の一番の原因はこのコマンドの名前です。「 マージ(merge)」という言葉は、何か二つのブランチが統合されたり、 データ同士が、何か神秘的な方法で混ぜ合わされてしまったりするような表現 です。しかし、そんなことがおこるわけではありません。多分このコマンドに 対するもっとふさわしい名前はsvn diff-and-apply(差分 をとってから、それを適用する)かも知れません。実際、起こることは本当に それだけなのですから: つまり、二つのリポジトリのツリーが比較され、その 差分が、作業コピーに適用されるのです。

このコマンドは三つの引数をとります:

  1. 最初の状態を示すリポジトリ・ツリー ( 比較時の左側 などとよく言われます),

  2. 最終的な状態を示すリポジトリ・ツリー (often called the 比較時の右側 などとよく言われます),

  3. 上記二つの間の差分をローカルな変更として受け入れる作業コピー (マージの ターゲットなどとよく言われます).

この三つの引数が指定されると二つのツリーが比較され、結果の 差分がターゲットの作業コピーに対して、ローカルな修正点の形で反映されま す。この結果はあなた自身が手作業でファイルを編集したり、svn addsvn deleteコマンドをいろいろと実行 したのとなんら変わるところはありません。結果の修正内容が満足のいくもの であれば、それをコミットすることができます。気に入らなければ、単に svn revertを実行しさえすればすべての変更は元に戻り ます。

svn merge の構文は必要な三つの引数をある程度 柔軟に指定できるようになっています。以下がその例です:

      
$ svn merge http://svn.example.com/repos/branch1@150 ¥
            http://svn.example.com/repos/branch2@212 ¥
            my-working-copy
            
$ svn merge -r 100:200 http://svn.example.com/repos/trunk my-working-copy

$ svn merge -r 100:200 http://svn.example.com/repos/trunk

最初の構文は三つのすべての引数を明示的に指定するもので、ツリーについては それぞれ URL@REV の形で指定し、ターゲットの作業コピー はその名前で示します。二番目の構文は、同じ URL 上にある異なるリビジョンを 比較する場合の略記法です。最後の構文は作業コピーを省略した場合の例です; デフォルトではカレントディレクトリが指定される決まりです。

マージの一番うまいやり方

手でマージする方法

変更のマージは非常に単純なことに思えますが実際には厄介な ものです。問題は、もし一つのブランチを別のブランチに対して 変更点を繰り返しマージすると、間違って同じ変更を 二度やってしまうかも知れないということです。 こういうことが起こっても、問題が起こらないこともあります。 ファイルをパッチするとき、Subversion はファイルが既に変更されている 場合にはそれに気がついて、何もしません。しかし、既に存在している 変更が何らかの方法で修正されていた場合、衝突が起こります。

理想的には、バージョン管理システムはブランチに対して変更点の重複 した適用を回避すべきです。ブランチが既に受け取った変更点を自動的に 記憶し、その一覧を表示できるようにすべきです。そしてバージョン管理システム は自動マージを支援するために可能な限りこの情報を利用すべきです。

残念ながら Subversion はそのようなシステムではありません。CVS と同様 Subversion はまだマージ操作に関するどのような情報も記録しません。 ローカルな修正をコミットしても、リポジトリはそれがsvn merge を実行したものによるのか、あるいは単に手でファイルを修正した ものによるのか区別できません。

これはユーザにとって何を意味するのでしょうか? それはSubversionにこの 機能がいつか実装されるまではマージの情報を自分で記録しておく必要が あるということです。一番良い場所はコミットログメッセージ中でしょう。 以前の例で説明したように、あなたのブランチにマージした特定のリビジョン 番号(あるいはリビジョン番号の範囲)をログメッセージ中で示しておくこと をお勧めします。あとでsvn logを実行してあなたの ブランチがどの変更点を既に含んでいるかを知ることができます。これで svn mergeコマンドを繰り返し実行する際に以前に 取り込んだ変更点を再び取り込むことがないように注意することが できます。

次の節ではこの技法の例を実際にお見せします。

マージ内容の確認

マージは作業コピーを変更するだけなので、それほど危険な操作では ありません。マージに失敗しても、単にsvn revertを実行すれば元に戻せるのでもう一度 やり直すことができます。

しかし作業コピーには既にローカルな修正が加えられていることもあります。 マージによって適用された修正は既に加えていた修正と混じってしまう のでこの場合にはsvn revertは使えません。 この二つの修正の組を分離することは不可能です。

このような場合には、実際にマージする前に、マージしたとしたらどうなるか を調べておくべきです。このための一つの簡単な方法としてはsvn mergeに渡そうとしているのと同じ引数でsvn diff を実行する方法があります。それは既にマージの最初の例で見たものです。 もう一つの方法は、マージコマンドに対して --dry-run オプションを渡す方法です:

$ svn merge --dry-run -r 343:344 http://svn.example.com/repos/calc/trunk
U  integer.c

$ svn status
#  nothing printed, working copy is still unchanged.

--dry-runオプションは、実際には作業コピーに対してローカルな 修正を適用しません。実際のマージで表示されるであろう 状態コードを表示するだけです。これはsvn diffではあまりにも詳細 な内容が表示されてしまうような場合に、潜在的なマージの概要を確認するための 「高度な」方法です。

マージの衝突

svn update コマンドと同様 svn merge は変更を作業コピーに対して行うので 衝突を起こすこともあります。しかし svn mergeによっておきた衝突については様子が 違うこともあり、以下ではこの違いについて説明します。

まず、作業コピーにはローカルな修正が加えられていないとします。 特定のリビジョンに対してsvn updateを実行すると サーバから送られてきた変更点は作業コピーに対して常に 「きれいに 」適用されます。サーバは二つのツリーを比較することで差分を 生成します: 作業コピーの仮想的なスナップショットと、適用しようとして いるリビジョンとの間の差分です。前者は作業コピーと全く同じものなので この差分が作業コピーをきれいに後者に変換することは保証されています。

しかしsvn mergeの場合はそのような保証はなく、 結果はもっと混沌としたものになる可能性もあります: ユーザは全く 任意の二つのツリーの比較をするようサーバに 指示することもでき、作業コピーとは全く無関係なものであるかも知れないのです!。 これは人間の側の操作ミスが起こる潜在的な可能性が大きいことを 意味します。場合によってはユーザは間違った二つのツリーを比較し、 きれいに適用できないような差分を作ってしまうかも知れません。 svn merge はできる限りこの差分を適用しようと しますが、ある部分は不可能かも知れません。ちょうど Unix の patchコマンドが「適用できなかったハンク 」について文句を言ってくることがあるのと同じように svn mergeは「処理を飛ばしたファイル」 について文句を言うかも知れません:

$ svn merge -r 1288:1351 http://svn.example.com/repos/branch
U  foo.c
U  bar.c
Skipped missing target: 'baz.c'
U  glub.c
C  glorb.h

$

この例では比較対象となる二つのブランチのスナップショットの両方に baz.cが存在していたため、生成された差分もその ファイルの内容を変更しようとしますが、作業コピー中には対応するファイル が存在しなかったような場合だと考えられます。いずれにせよ 「スキップ」のメッセージはユーザが間違った二つのツリーを 比較してしまったことを意味することがほとんどです。ユーザ側のエラー を示す典型的な状況です。こうなった場合でも(svn revert --recursiveを使って)、マージによって実行されたすべての変更 点を再帰的に元に戻し、バージョン化されていないファイルやディレクトリ が残っている場合にはそれらも削除し、正しい引数でsvn mergeを再実行するのは難しいことではありません。

前の例ではglorb.hに衝突が起きたことにも注意 してください。今回の場合作業コピーに対してローカルな修正がされていない ことはすでに述べました: ではなぜ衝突が起きるのでしょうか? この場合 でもやはりユーザはsvn mergeで古い差分を作ってから 作業コピーに適用することができるので、ローカルな修正がなかったと しても、その差分が作業コピーに対してきれいに適用できないような変更を 含んでしまうことはありうるのです。

その他svn updatesvn mergeの 小さな違いとしては衝突がおきたときにできるテキストファイルの名前です。 衝突の解消(他の人の変更点のマージ)項で見たように、update の場合には filename.mine, filename.rOLDREV, filename.rNEWREVという名前のファイルができます。 これにたいしてsvn mergeの場合には filename.working, filename.left, filename.rightという名前になります。 この場合「left」 と 「right」 は、それぞれの ファイルが比較した二つのツリーのどちら側に由来するものかを示しています。 いずれにせよファイル名称の違いは、衝突が update コマンドの結果である のか merge コマンドの結果であるかを区別する助けになるでしょう。

系統(Ancestry)を考慮することと無視すること

Subversion 開発者と会話するとき系統 (ancestry)という言葉を非常によく耳にするでしょう。 この言葉はリポジトリ中の二つのオブジェクト間の関係を記述するた めに用いられるものです:もし両者が互いに関係している場合、ある オブジェクトはもう一方の祖先(ancestor)といわれます。

例えば、リビジョン100をコミットし、それが foo.cというファイルへの変更を含んでいると します。するとfoo.c@99foo.c@100の「祖先」ということ になります。一方リビジョン 101 でfoo.cを 削除するコミットがあり、リビジョン102 で同じ名前の新しいファイ ルを追加したとしましょう。この場合 foo.c@99foo.c@102 は関係しているように見えます(なぜなら同じファイル名なのですか ら)が、実際にはリポジトリ中ではまったく別のオブジェクトです。 両者は履歴、あるいは「系統」を共有していないからで す。

ここでこんな話をするのは、svn diffsvn mergeの間の重要な違いを指摘したいからです。前者 は系統を無視しますが、後者は系統を非常に慎重に考慮します。例えば svn diffでリビジョン99 と102 の foo.cを比較した場合、行単位の差分を見ることになる でしょう; diff コマンドは二つのファイル名を無条件に比較するからです。 しかしsvn mergeを使っていまと同じ二つのオブジェクトを比較す るとそれらが無関係であることを検知し古いファイルをいったん削除し、それ から新しいファイルを追加しようとするでしょう; 出力は追加のあとに削除した ことを示すものとなるでしょう:

D  foo.c
A  foo.c

ほとんどのマージはお互いに系統上関係したツリーを比較する ので、svn mergeはデフォルトで上記のような動 作になります。しかし、二つの無関係なツリーを比較するために mergeコマンドを使いたいと思うこともあるかも知れません。 たとえばあるソフトウェアプロジェクトの、異なる 二つのベンダーリリースを表すようなソースコードツリーをインポー トするかも知れません(ベンダーブランチ項参照)。 この二つのツリーをsvn mergeで比較すると最初 のツリー全体がいったん削除され、次いで後のツリー全体が追加され たように見えるでしょう!

このような場合、svn mergeは単にファイ ル名ベースの比較のみを実行し、ファイルやディレクトリの系統上の 関係を無視したいと考えるでしょう。こんなときはマージコマンドに --ignore-ancestryオプションをつければ ちょうどsvn diffと同じように振舞うようにな ります。(逆にsvn diffコマンドに --notice-ancestryオプションをつけると svn diffコマンドはmergeコマンドと同じよう に振舞うことになります。)。



[9] 将来的にはSubversionプロジェクトはツリー構造と属性の変更点を表現する ような拡張したパッチ形式を使う(あるいは開発する)計画があります。