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

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

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

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

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

特定の変更点のコピー

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

It's time to use the svn merge command. This command, it turns out, is a very close cousin to the svn diff command (which you read about in 第2章). Both commands are able to compare any two objects in the repository and describe the differences. For example, you can ask svn diff to show you the exact change made by Sally in revision 344:

$ svn diff -c 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 -c 344 http://svn.example.com/repos/calc/trunk
U  integer.c

$ svn status
M  integer.c

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

In another scenario, it's possible that things may not have gone so well, and that integer.c may have entered a conflicted state. You might need to resolve the conflict using standard procedures (see 第2章), or if you decide that the merge was a bad idea altogether, simply give up and svn revert the local change.

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

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

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

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

A word of warning: while svn diff and svn merge are very similar in concept, they do have different syntax in many cases. Be sure to read about them in 第9章 for details, or ask svn help. For example, svn merge requires a working-copy path as a target, i.e. a place where it should apply the tree-changes. If the target isn't specified, it assumes you are trying to perform one of the following common operations:

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

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

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

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

$ svn merge -c 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 はファイルが既に変更されている 場合にはそれに気がついて、何もしません。しかし、既に存在している 変更が何らかの方法で修正されていた場合、競合が起こります。

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

Unfortunately, Subversion is not such a system; it does not yet record any information about merge operations. [22] When you commit local modifications, the repository has no idea whether those changes came from running svn merge, or from just hand-editing the files.

What does this mean to you, the user? It means that until the day Subversion grows this feature, you'll have to track merge information yourself. The best place to do this is in the commit log-message. As demonstrated in prior examples, it's recommended that your log-message mention a specific revision number (or range of revisions) that are being merged into your branch. Later on, you can run svn log to review which changes your branch already contains. This will allow you to carefully construct a subsequent svn merge command that won't be redundant with previously ported changes.

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

マージ内容の確認

First, always remember to do your merge into a working copy that has no local edits and has been recently updated. If your working copy isn't 「clean」 in these ways, you can run into some headaches.

Assuming your working copy is tidy, merging isn't a particularly high-risk operation. If you get the merge wrong the first time, simply svn revert the changes and try again.

If you've merged into a working copy that already has local modifications, the changes applied by a merge will be mixed with your pre-existing ones, and running svn revert is no longer an option. The two sets of changes may be impossible to separate.

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

$ svn merge --dry-run -c 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を実行すると サーバから送られてきた変更点は作業コピーに対して常に 「きれいに 」適用されます。サーバは二つのツリーを比較することで差分を 生成します: 作業コピーの仮想的なスナップショットと、適用しようとして いるリビジョンとの間の差分です。前者は作業コピーと全く同じものなので この差分が作業コピーをきれいに後者に変換することは保証されています。

But svn merge has no such guarantees and can be much more chaotic: the user can ask the server to compare any two trees at all, even ones that are unrelated to the working copy! This means there's large potential for human error. Users will sometimes compare the wrong two trees, creating a delta that doesn't apply cleanly. svn merge will do its best to apply as much of the delta as possible, but some parts may be impossible. Just as the Unix patch command sometimes complains about 「failed hunks」, svn merge will complain about 「skipped targets」:

$ 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

$

In the previous example it might be the case that baz.c exists in both snapshots of the branch being compared, and the resulting delta wants to change the file's contents, but the file doesn't exist in the working copy. Whatever the case, the 「skipped」 message means that the user is most likely comparing the wrong two trees; they're the classic sign of user error. When this happens, it's easy to recursively revert all the changes created by the merge (svn revert --recursive), delete any unversioned files or directories left behind after the revert, and re-run svn merge with different arguments.

前の例では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

Most merges involve comparing trees that are ancestrally related to one another, and therefore svn merge defaults to this behavior. Occasionally, however, you may want the merge command to compare two unrelated trees. For example, you may have imported two source-code trees representing different vendor releases of a software project (see ベンダーブランチ項). If you asked svn merge to compare the two trees, you'd see the entire first tree being deleted, followed by an add of the entire second tree! In these situations, you'll want svn merge to do a path-based comparison only, ignoring any relations between files and directories. Add the --ignore-ancestry option to your merge command, and it will behave just like svn diff. (And conversely, the --notice-ancestry option will cause svn diff to behave like the merge command.)

マージと移動

A common desire is to refactor source code, especially in Java-based software projects. Files and directories are shuffled around and renamed, often causing great disruption to everyone working on the project. Sounds like a perfect case to use a branch, doesn't it? Just create a branch, shuffle things around, then merge the branch back to the trunk, right?

Alas, this scenario doesn't work so well right now, and is considered one of Subversion's current weak spots. The problem is that Subversion's update command isn't as robust as it should be, particularly when dealing with copy and move operations.

When you use svn copy to duplicate a file, the repository remembers where the new file came from, but it fails to transmit that information to the client which is running svn update or svn merge. Instead of telling the client, 「Copy that file you already have to this new location」, it instead sends down an entirely new file. This can lead to problems, especially because the same thing happens with renamed files. A lesser-known fact about Subversion is that it lacks 「true renames」—the svn move command is nothing more than an aggregation of svn copy and svn delete.

For example, suppose that while working on your private branch, you rename integer.c to whole.c. Effectively you've created a new file in your branch that is a copy of the original file, and deleted the original file. Meanwhile, back on trunk, Sally has committed some improvements to integer.c. Now you decide to merge your branch to the trunk:

$ cd calc/trunk

$ svn merge -r 341:405 http://svn.example.com/repos/calc/branches/my-calc-branch
D   integer.c
A   whole.c

This doesn't look so bad at first glance, but it's also probably not what you or Sally expected. The merge operation has deleted the latest version of integer.c file (the one containing Sally's latest changes), and blindly added your new whole.c file—which is a duplicate of the older version of integer.c. The net effect is that merging your 「rename」 to the branch has removed Sally's recent changes from the latest revision!

This isn't true data-loss; Sally's changes are still in the repository's history, but it may not be immediately obvious that this has happened. The moral of this story is that until Subversion improves, be very careful about merging copies and renames from one branch to another.



[22] However, at the time of writing, this feature is being worked on!