章 ▾ 第2版

7.8 Git ツール - 高度なマージ

高度なマージ

Git でのマージは、通常非常に簡単です。Git は別のブランチを複数回マージすることを容易にするため、非常に長期間生きるブランチを持ちながらも、小さな競合を頻繁に解決していくことで常に最新の状態に保つことができ、一連の作業の最後に巨大な競合に驚くことはありません。

しかし、時には厄介な競合が発生することもあります。他のバージョン管理システムとは異なり、Git はマージ競合の解決において過度に賢く振る舞おうとはしません。Git の哲学は、マージの解決が曖昧でないと判断できる場合には賢くあることですが、競合がある場合には自動的に解決しようとはしません。したがって、急速に分岐する2つのブランチをマージするのを長期間待ってしまうと、いくつかの問題に遭遇する可能性があります。

このセクションでは、そのような問題がどのようなものであるか、そして Git がこれらのより厄介な状況を処理するために提供するツールについて説明します。また、実行できる非標準のマージの種類と、実行したマージを元に戻す方法についても説明します。

マージ競合

基本的なマージ競合ではマージ競合の解決の基本について説明しましたが、より複雑な競合の場合、Git は何が起こっているのかを把握し、競合によりうまく対処するためにいくつかのツールを提供します。

まず、可能な限り、競合が発生する可能性のあるマージを行う前に、作業ディレクトリがクリーンであることを確認してください。進行中の作業がある場合は、一時的なブランチにコミットするか、スタッシュしてください。これにより、ここで試したすべてを元に戻すことができます。マージを試したときに作業ディレクトリに保存されていない変更がある場合、これらのヒントのいくつかはその作業を維持するのに役立つかもしれません。

非常に簡単な例を見てみましょう。 'hello world' を出力する超シンプルな Ruby ファイルがあります。

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()

リポジトリで、`whitespace` という新しいブランチを作成し、すべての Unix の行末を DOS の行末に変更します。これは実質的にファイルのすべての行を変更しますが、空白文字のみです。次に、"hello world" という行を "hello mundo" に変更します。

$ git checkout -b whitespace
Switched to a new branch 'whitespace'

$ unix2dos hello.rb
unix2dos: converting file hello.rb to DOS format ...
$ git commit -am 'Convert hello.rb to DOS'
[whitespace 3270f76] Convert hello.rb to DOS
 1 file changed, 7 insertions(+), 7 deletions(-)

$ vim hello.rb
$ git diff -b
diff --git a/hello.rb b/hello.rb
index ac51efd..e85207e 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 #! /usr/bin/env ruby

 def hello
-  puts 'hello world'
+  puts 'hello mundo'^M
 end

 hello()

$ git commit -am 'Use Spanish instead of English'
[whitespace 6d338d2] Use Spanish instead of English
 1 file changed, 1 insertion(+), 1 deletion(-)

次に `master` ブランチに戻り、関数にいくつかのドキュメントを追加します。

$ git checkout master
Switched to branch 'master'

$ vim hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index ac51efd..36c06c8 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello world'
 end

$ git commit -am 'Add comment documenting the function'
[master bec6336] Add comment documenting the function
 1 file changed, 1 insertion(+)

ここで `whitespace` ブランチをマージしようとすると、空白文字の変更により競合が発生します。

$ git merge whitespace
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

マージの中止

これでいくつかの選択肢があります。まず、この状況から抜け出す方法を説明します。競合を予期していなかった場合や、まだこの状況に対処したくない場合は、単純に `git merge --abort` でマージを中止できます。

$ git status -sb
## master
UU hello.rb

$ git merge --abort

$ git status -sb
## master

`git merge --abort` オプションは、マージを実行する前の状態に戻そうとします。完璧にできない唯一のケースは、実行時に作業ディレクトリにスタッシュされていない、コミットされていない変更があった場合であり、それ以外の場合は問題なく動作するはずです。

何らかの理由で最初からやり直したい場合は、`git reset --hard HEAD` を実行することもできます。これにより、リポジトリは最後にコミットされた状態に戻ります。コミットされていない作業は失われるため、変更が必要ないことを確認してください。

空白の無視

この特定のケースでは、競合は空白文字に関連しています。ケースが単純なのでこれがわかりますが、競合を見れば、片側ですべての行が削除され、もう片側で再度追加されているため、実際のケースでも非常に簡単にわかります。デフォルトでは、Git はこれらすべての行が変更されたと認識するため、ファイルをマージできません。

ただし、デフォルトのマージ戦略は引数を受け取ることができ、その中には空白文字の変更を適切に無視するものがあります。マージで多くの空白文字の問題があることがわかった場合は、単純にそれを中止して再度実行し、今度は `-Xignore-all-space` または `-Xignore-space-change` を使用します。最初のオプションは行を比較するときに空白文字を完全に無視し、2番目のオプションは1つ以上の空白文字のシーケンスを同等として扱います。

$ git merge -Xignore-space-change whitespace
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

このケースでは実際のファイルの変更は競合していなかったため、空白文字の変更を無視すると、すべてが正常にマージされます。

これは、チームの誰かが時折、すべてをスペースからタブへ、またはその逆に再フォーマットするのが好きな場合に、非常に役立ちます。

手動でのファイルのリマージ

Git は空白文字の事前処理をかなりうまく扱いますが、Git が自動的に処理できないが、スクリプトで修正できる他の種類の変更もあります。たとえば、Git が空白文字の変更を処理できなかったと仮定し、手動でそれを行う必要があったとします。

実際に行う必要があるのは、実際のファイルマージを試みる前に、マージしようとしているファイルを `dos2unix` プログラムを通して実行することです。では、どうすればよいでしょうか?

まず、マージ競合状態に入ります。次に、自分のバージョンのファイル、相手のバージョン (マージ中のブランチから)、および共通のバージョン (両側が分岐した場所から) のコピーを取得します。次に、相手側または自分の側のいずれかを修正し、この単一のファイルに対して再度マージを試みます。

3つのファイルバージョンを取得するのは実は非常に簡単です。Git はこれらのすべてのバージョンをインデックスに「ステージ」として保存し、それぞれに番号が関連付けられています。ステージ 1 は共通の祖先、ステージ 2 はあなたのバージョン、ステージ 3 は `MERGE_HEAD` (マージ中のバージョン、「彼らの」) からのものです。

`git show` コマンドと特殊な構文を使用すると、競合したファイルのこれらの各バージョンのコピーを抽出できます。

$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb

もう少し本格的にやりたい場合は、`ls-files -u` プラミングコマンドを使用して、これらの各ファイルの Git ブロブの実際の SHA-1 を取得することもできます。

$ git ls-files -u
100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1	hello.rb
100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2	hello.rb
100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3	hello.rb

`:1:hello.rb` は、そのブロブ SHA-1 を検索するための単なる略記です。

これで、作業ディレクトリに3つのステージすべてのコンテンツができたので、手動で相手側を修正して空白文字の問題を修正し、そのためのあまり知られていない `git merge-file` コマンドを使用してファイルを再マージできます。

$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...

$ git merge-file -p \
    hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb

$ git diff -b
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
  #! /usr/bin/env ruby

 +# prints out a greeting
  def hello
-   puts 'hello world'
+   puts 'hello mundo'
  end

  hello()

この時点で、ファイルはきれいにマージされました。実際、これは `ignore-space-change` オプションよりも優れています。なぜなら、これは空白文字の変更をマージする前に実際に修正するもので、単に無視するだけではないからです。`ignore-space-change` マージでは、結果的にいくつかの行が DOS の行末になり、混在した状態になりました。

コミットを確定する前に、一方の側ともう一方の側との間で実際に何が変更されたかを把握したい場合は、`git diff` に、マージの結果としてコミットしようとしている作業ディレクトリの内容をこれらのいずれかのステージと比較するように要求できます。すべて見ていきましょう。

結果をマージ前のブランチの状態と比較する、つまりマージが何を導入したかを確認するには、`git diff --ours` を実行します。

$ git diff --ours
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index 36c06c8..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -2,7 +2,7 @@

 # prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

したがって、ここでは、ブランチで何が起こったのか、このマージでこのファイルに実際に何が導入されているのか、つまりその単一行の変更を簡単に確認できます。

マージの結果が相手側とどのように異なったかを確認したい場合は、`git diff --theirs` を実行します。この例と次の例では、Git 内のコンテンツと比較しているため、空白文字を取り除くために `-b` を使用する必要があります。クリーンアップされた `hello.theirs.rb` ファイルと比較しているわけではありません。

$ git diff --theirs -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index e85207e..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello mundo'
 end

最後に、`git diff --base` を使用して、両側からのファイルの変更を確認できます。

$ git diff --base -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index ac51efd..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

この時点で、`git clean` コマンドを使用して、手動マージを行うために作成したが不要になった余分なファイルをクリアできます。

$ git clean -f
Removing hello.common.rb
Removing hello.ours.rb
Removing hello.theirs.rb

競合のチェックアウト

おそらく、何らかの理由でこの時点での解決に満足できないか、あるいは片側または両側を手動で編集しても問題が解決せず、より多くのコンテキストが必要な場合があるでしょう。

例を少し変更してみましょう。この例では、それぞれにいくつかのコミットがある2つの長期間存在するブランチがあり、マージすると正当なコンテンツの競合が発生します。

$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) Update README
* 9af9d3b Create README
* 694971d Update phrase to 'hola world'
| * e3eb223 (mundo) Add more tests
| * 7cff591 Create initial testing script
| * c3ffff1 Change text to 'hello mundo'
|/
* b7dcc89 Initial hello world code

現在、`master` ブランチにのみ存在する3つのユニークなコミットと、`mundo` ブランチに存在する他の3つのコミットがあります。`mundo` ブランチをマージしようとすると、競合が発生します。

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

マージ競合が何であるかを確認したいとします。ファイルを開くと、次のような内容が表示されます。

#! /usr/bin/env ruby

def hello
<<<<<<< HEAD
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> mundo
end

hello()

マージの両側でこのファイルにコンテンツが追加されましたが、いくつかのコミットが同じ場所でファイルを変更したため、この競合が発生しました。

この競合がどのようにして発生したのかを判断するために利用できるようになったいくつかのツールを見てみましょう。この競合を正確にどのように修正すべきかは、明らかではないかもしれません。より多くのコンテキストが必要です。

役立つツールの一つは、`--conflict` オプションを付けた `git checkout` です。これにより、ファイルが再度チェックアウトされ、マージ競合マーカーが置き換えられます。これは、マーカーをリセットして再度解決を試みる場合に役立ちます。

`--conflict` には `diff3` または `merge` (デフォルト) を渡すことができます。`diff3` を渡すと、Git は少し異なるバージョンの競合マーカーを使用し、「ours」と「theirs」のバージョンだけでなく、「base」バージョンもインラインで表示して、より多くのコンテキストを提供します。

$ git checkout --conflict=diff3 hello.rb

これを実行すると、ファイルは次のように表示されます。

#! /usr/bin/env ruby

def hello
<<<<<<< ours
  puts 'hola world'
||||||| base
  puts 'hello world'
=======
  puts 'hello mundo'
>>>>>>> theirs
end

hello()

この形式が気に入った場合、`merge.conflictstyle` 設定を `diff3` に設定することで、今後のマージ競合のデフォルトとして設定できます。

$ git config --global merge.conflictstyle diff3

`git checkout` コマンドは `--ours` および `--theirs` オプションも受け入れます。これは、何もマージせずにどちらか一方を選択する非常に迅速な方法です。

これは、バイナリファイルの競合で一方を単純に選択できる場合や、別のブランチから特定のファイルのみをマージしたい場合に特に便利です。マージを実行し、コミットする前に一方または他方の特定のファイルをチェックアウトできます。

マージログ

マージ競合を解決する際に役立つもう一つのツールは `git log` です。これは、競合に貢献した可能性のあるコンテキストを得るのに役立ちます。2つの開発ラインがなぜ同じコード領域に触れていたのかを思い出すために、少し履歴をレビューすることは、時には本当に役立ちます。

このマージに関与した両方のブランチに含まれるすべてのユニークなコミットの完全なリストを取得するには、トリプルドットで学んだ「トリプルドット」構文を使用できます。

$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 Update README
< 9af9d3b Create README
< 694971d Update phrase to 'hola world'
> e3eb223 Add more tests
> 7cff591 Create initial testing script
> c3ffff1 Change text to 'hello mundo'

これは、関与した合計6つのコミットの素晴らしいリストであり、各コミットがどの開発ラインにあったかも示されています。

ただし、これをさらに簡略化して、より具体的なコンテキストを提供できます。`git log` に `--merge` オプションを追加すると、現在競合しているファイルに影響を与えるマージのいずれかの側のコミットのみが表示されます。

$ git log --oneline --left-right --merge
< 694971d Update phrase to 'hola world'
> c3ffff1 Change text to 'hello mundo'

代わりに `-p` オプションを付けて実行すると、競合したファイルへの差分のみが得られます。これは、なぜ競合が発生したのか、そしてどのようにしてより賢く解決できるのかを理解するのに必要なコンテキストをすばやく提供するのに本当に役立ちます。

結合差分形式

Git は成功したマージ結果をすべてステージングするため、競合したマージ状態で `git diff` を実行すると、まだ競合しているものだけが得られます。これは、まだ解決すべきものを見るのに役立ちます。

マージ競合の直後に `git diff` を実行すると、ややユニークな差分出力形式で情報が表示されます。

$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,11 @@@
  #! /usr/bin/env ruby

  def hello
++<<<<<<< HEAD
 +  puts 'hola world'
++=======
+   puts 'hello mundo'
++>>>>>>> mundo
  end

  hello()

この形式は「結合差分 (Combined Diff)」と呼ばれ、各行の横に2列のデータが表示されます。最初の列は、「ours」ブランチと作業ディレクトリ内のファイルの間でその行が異なる(追加または削除された)かどうかを示し、2番目の列は「theirs」ブランチと作業ディレクトリのコピーの間で同じことを示します。

したがって、その例では、`<<<<<<<` と `>>>>>>>` の行が作業コピーにはあるが、マージの両側にはなかったことがわかります。これは、マージツールが私たちのコンテキストのためにそれらをそこに配置したが、私たちはそれらを削除することが期待されているため、理にかなっています。

競合を解決して `git diff` を再度実行すると、同じ内容が表示されますが、少し役立ちます。

$ vim hello.rb
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

これにより、「hola world」が私たちの側にはあったが作業コピーにはなかったこと、「hello mundo」が彼らの側にはあったが作業コピーにはなかったこと、そして最後に「hola mundo」がどちらの側にもなかったが現在作業コピーにあることが示されます。これは、解決策をコミットする前に確認するのに役立ちます。

これは、マージコミットに対して `git show` を実行した場合、または `git log -p` に `--cc` オプションを追加した場合(デフォルトでは非マージコミットのパッチのみを表示します)にも、後から解決方法を確認するために `git log` から取得できます。

$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Sep 19 18:14:49 2014 +0200

    Merge branch 'mundo'

    Conflicts:
        hello.rb

diff --cc hello.rb
index 0399cd5,59727f0..e1d0799
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

マージの取り消し

マージコミットを作成する方法を学んだので、誤っていくつか作成することもあるでしょう。Git を使用する上で素晴らしいことの一つは、間違いを犯しても大丈夫だということです。なぜなら、間違いを修正することは可能であり(多くの場合、簡単です)、そのための手段が提供されているからです。

マージコミットも例外ではありません。例えば、トピックブランチで作業を開始し、誤って `master` にマージしてしまい、コミット履歴が次のようになったとします。

Accidental merge commit
図 155. 誤ったマージコミット

この問題に対処する方法は、望む結果に応じて2つあります。

参照を修正する

不要なマージコミットがローカルリポジトリにのみ存在する場合、最も簡単で最善の解決策は、ブランチを目的の場所を指すように移動することです。ほとんどの場合、誤った `git merge` の後に `git reset --hard HEAD~` を実行すると、ブランチポインタは次のようになります。

History after `git reset --hard HEAD~`
図 156. `git reset --hard HEAD~` 後の履歴

Reset の解明で `reset` について説明したので、ここで何が起こっているのかを理解するのはそれほど難しくないはずです。簡単に復習すると、`reset --hard` は通常、次の3つのステップを実行します。

  1. HEAD が指すブランチを移動します。この場合、`master` をマージコミット前(`C6`)の位置に移動させたいとします。

  2. インデックスを HEAD と同じ状態にします。

  3. 作業ディレクトリをインデックスと同じ状態にします。

このアプローチの欠点は、履歴を書き換えることであり、共有リポジトリでは問題となる可能性があります。リベースの危険性で何が起こるかについて詳しく説明しています。要するに、他の人があなたが書き換えているコミットを持っている場合、`reset` は避けるべきだということです。このアプローチは、マージ後に他のコミットが作成された場合にも機能しません。参照を移動すると、これらの変更が実質的に失われることになります。

コミットを元に戻す

ブランチポインタを移動する方法がうまくいかない場合、Git には既存のコミットからのすべての変更を元に戻す新しいコミットを作成するオプションがあります。Git はこの操作を「リバート (revert)」と呼び、この特定のシナリオでは次のように呼び出します。

$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"

`-m 1` フラグは、どの親が「メインライン」であり、維持すべきかを指定します。`HEAD` へのマージ ( `git merge topic` ) を呼び出すと、新しいコミットは2つの親を持ちます。1つ目は `HEAD` ( `C6` )、2つ目はマージされるブランチの先端 ( `C4` ) です。この場合、親 #2 ( `C4` ) をマージすることによって導入されたすべての変更を取り消し、親 #1 ( `C6` ) のすべてのコンテンツを保持したいと考えます。

リバートコミットを含む履歴は次のようになります。

History after `git revert -m 1`
図 157. `git revert -m 1` 後の履歴

新しいコミット `^M` は `C6` とまったく同じ内容を持っているため、ここから始めると、マージは決して起こらなかったかのようですが、マージされなかったコミットはまだ `HEAD` の履歴に残っています。`topic` を `master` に再度マージしようとすると、Git は混乱します。

$ git merge topic
Already up-to-date.

`topic` には、`master` からすでに到達できないものは何もありません。さらに悪いことに、`topic` に作業を追加して再度マージした場合、Git はリバートされたマージ以降の変更のみを取り込みます。

History with a bad merge
図 158. 悪いマージを含む履歴

これを回避する最善の方法は、元に戻された変更を取り込みたいので、元のマージを元に戻すこと、そして新しいマージコミットを作成することです。

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
History after re-merging a reverted merge
図 159. リバートされたマージを再マージした後の履歴

この例では、`M` と `^M` は打ち消し合います。`^^M` は事実上 `C3` と `C4` からの変更をマージし、`C8` は `C7` からの変更をマージするため、これで `topic` は完全にマージされました。

その他のマージタイプ

これまでは、通常「recursive」戦略と呼ばれる方法で処理される、2つのブランチの通常のマージについて説明しました。しかし、ブランチをマージする方法は他にもあります。いくつか簡単に説明しましょう。

Our または Theirs の優先

まず、通常のマージ「recursive」モードでできる別の有用なことがあります。すでに `-X` と一緒に渡される `ignore-all-space` および `ignore-space-change` オプションを見てきましたが、競合が発生したときに Git に一方の側を優先するように指示することもできます。

デフォルトでは、Git はマージ中の2つのブランチ間で競合を検出すると、マージ競合マーカーをコードに追加し、ファイルを競合状態としてマークし、ユーザーに解決させます。Git が手動で競合を解決させる代わりに、特定の側を単に選択して他方を無視することを好む場合、`merge` コマンドに `-Xours` または `-Xtheirs` のいずれかを渡すことができます。

Git がこれを認識すると、競合マーカーは追加されません。マージ可能な違いはマージされ、競合する違いは、バイナリファイルを含め、指定した側全体を単純に選択します。

以前使用した「hello world」の例に戻ると、ブランチをマージすると競合が発生することがわかります。

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.

しかし、`-Xours` または `-Xtheirs` をつけて実行すると、競合は発生しません。

$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 test.sh  | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 test.sh

この場合、一方に「hello mundo」、もう一方に「hola world」という競合マーカーがファイルに表示される代わりに、「hola world」が単純に選択されます。ただし、そのブランチ上の他の競合しない変更はすべて正常にマージされます。

このオプションは、個別のファイルマージの場合、`git merge-file --ours` のように実行することで、以前に見た `git merge-file` コマンドにも渡すことができます。

これに似たことをしたいが、Git に相手側からの変更をマージしようとすらさせたくない場合、より厳格なオプションがあります。それは「ours」マージ戦略です。これは「ours」recursive マージオプションとは異なります。

これは基本的に偽のマージを行います。両方のブランチを親とする新しいマージコミットを記録しますが、マージ中のブランチをまったく見ません。現在のブランチの正確なコードをマージの結果として単に記録するだけです。

$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$

私たちがいたブランチとマージ結果との間に違いがないことがわかります。

これは、後でマージを行う際に、Gitにブランチがすでにマージされていると錯覚させるのに役立つことがよくあります。例えば、`release` ブランチを分岐させ、その上でいくつかの作業を行い、いずれ `master` ブランチにマージし直したいとします。その間に `master` 上のバグ修正が `release` ブランチにバックポートされる必要があります。バグ修正ブランチを `release` ブランチにマージし、同じブランチを `master` ブランチにも `merge -s ours` (修正がすでにそこにあるにもかかわらず) することで、後で `release` ブランチを再度マージしても、バグ修正からの競合が発生しないようにできます。

サブツリーマージ

サブツリーマージの考え方は、2つのプロジェクトがあり、一方のプロジェクトがもう一方のサブディレクトリにマッピングされるというものです。サブツリーマージを指定すると、Git は一方のプロジェクトがもう一方のサブツリーであることを認識し、適切にマージするほど賢いことがよくあります。

既存のプロジェクトに別のプロジェクトを追加し、その2番目のプロジェクトのコードを最初のプロジェクトのサブディレクトリにマージする例を見ていきましょう。

まず、Rack アプリケーションをプロジェクトに追加します。Rack プロジェクトを自身のプロジェクトのリモート参照として追加し、それを自身のブランチにチェックアウトします。

$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote --no-tags
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From https://github.com/rack/rack
 * [new branch]      build      -> rack_remote/build
 * [new branch]      master     -> rack_remote/master
 * [new branch]      rack-0.4   -> rack_remote/rack-0.4
 * [new branch]      rack-0.9   -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"

これで、`rack_branch` ブランチには Rack プロジェクトのルートが、`master` ブランチには自身のプロジェクトがあります。一方をチェックアウトしてからもう一方をチェックアウトすると、それぞれ異なるプロジェクトルートを持っていることがわかります。

$ ls
AUTHORS         KNOWN-ISSUES   Rakefile      contrib         lib
COPYING         README         bin           example         test
$ git checkout master
Switched to branch "master"
$ ls
README

これはある意味奇妙な概念です。リポジトリ内のすべてのブランチが、必ずしも同じプロジェクトのブランチである必要はありません。これは一般的ではありません。なぜなら、それが役立つことはめったにないからです。しかし、ブランチが完全に異なる履歴を含むことは非常に簡単です。

この場合、Rack プロジェクトを `master` プロジェクトのサブディレクトリとして取り込みたいと考えます。これは Git で `git read-tree` を使用して行うことができます。`read-tree` とその関連コマンドについては、Git の内部で詳しく学びますが、ここでは、あるブランチのルートツリーを現在のステージングエリアと作業ディレクトリに読み込むことだけを知っておいてください。私たちはちょうど `master` ブランチに戻り、メインプロジェクトの `master` ブランチの `rack` サブディレクトリに `rack_branch` ブランチを取り込みます。

$ git read-tree --prefix=rack/ -u rack_branch

コミットすると、そのサブディレクトリの下にすべての Rack ファイルがあるように見えます。まるで tarball からコピーしたかのようです。興味深いのは、一方のブランチからもう一方のブランチへの変更を非常に簡単にマージできることです。したがって、Rack プロジェクトが更新された場合、そのブランチに切り替えてプルすることで、上流の変更を取り込むことができます。

$ git checkout rack_branch
$ git pull

次に、これらの変更を `master` ブランチにマージし直すことができます。変更を取り込み、コミットメッセージを事前に入力するには、`--squash` オプションと、recursive マージ戦略の `-Xsubtree` オプションを使用します。ここで recursive 戦略はデフォルトですが、明確にするために含めています。

$ git checkout master
$ git merge --squash -s recursive -Xsubtree=rack rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested

Rack プロジェクトからのすべての変更がマージされ、ローカルでコミットする準備ができています。逆も可能です。`master` ブランチの `rack` サブディレクトリで変更を加え、後でそれらを `rack_branch` ブランチにマージして、メンテナーに提出したり、上流にプッシュしたりできます。

これにより、サブモジュールを使用せずに(これについてはサブモジュールで説明します)、サブモジュールワークフローとある程度似たワークフローを実現できます。他の関連プロジェクトを持つブランチをリポジトリに保持し、時折それらをプロジェクトにサブツリーマージできます。すべてのコードが1か所にコミットされるなど、いくつかの点で優れています。しかし、変更の再統合や無関係なリポジトリへのブランチの誤ったプッシュがより複雑で間違いを犯しやすいという他の欠点もあります。

もう一つ少し奇妙なのは、`rack` サブディレクトリの内容と `rack_branch` ブランチのコードとの差分を取得する(マージが必要かどうかを確認するため)には、通常の `diff` コマンドを使用できないことです。代わりに、比較したいブランチを指定して `git diff-tree` を実行する必要があります。

$ git diff-tree -p rack_branch

または、`rack` サブディレクトリの内容と、サーバー上の `master` ブランチが最後にフェッチされたときの状態を比較するには、次のように実行します。

$ git diff-tree -p rack_remote/master
scroll-to-top