チャプター ▾ 第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を実行します。この例と以下の例では、空白文字を取り除くために-bを使用する必要があります。なぜなら、Gitに存在する内容と比較しているためであり、私たちのクリーンアップされた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()

マージの両側がこのファイルにコンテンツを追加しましたが、一部のコミットがこの衝突を引き起こしたのと同じ場所でファイルを変更しました。

この衝突がどのようにして発生したかを特定するために利用できるいくつかのツールを探ってみましょう。この衝突を正確にどのように修正すべきかは、おそらく明らかではありません。もっとコンテキストが必要です。

役に立つツールの1つは、--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オプションも受け入れます。これは、全くマージせずにどちらか一方の側を選択する非常に高速な方法となりえます。

これは、バイナリファイルの衝突で片側だけを選択できる場合や、別のブランチから特定のファイルのみをマージしたい場合に特に便利です。マージを実行した後、コミットする前に、片側またはもう片側から特定のファイルをチェックアウトできます。

マージログ

マージ衝突を解決する際に役立つもう1つのツールは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()

この形式は「結合差分」と呼ばれ、各行の隣に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 logからでも、後で何が解決されたかを確認できます。Gitは、マージコミットでgit showを実行した場合、またはgit log -p--ccオプションを追加した場合にこの形式を出力します(デフォルトでは、非マージコミットのパッチのみを表示します)。

$ 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を使って作業する上で素晴らしいことの1つは、間違いを犯しても大丈夫だということです。なぜなら、それらを修正することが可能(そして多くの場合簡単)だからです。

マージコミットも例外ではありません。トピックブランチで作業を開始し、誤って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 --hardは通常、次の3つのステップを実行します。

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

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

  3. ワーキングディレクトリをインデックスと同じにします。

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

コミットを元に戻す

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

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

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

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

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

新しいコミット^MC6とまったく同じ内容なので、ここから始めれば、マージは起こらなかったかのようです。ただし、現在マージされていないコミットは依然としてHEADの履歴に残っています。topicmasterに再度マージしようとすると、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は実質的にC3C4からの変更をマージし、C8C7からの変更をマージするので、これでtopicは完全にマージされました。

その他のマージタイプ

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

自社または相手の好み

まず、通常のマージの「再帰的」モードでできるもう1つの便利なことがあります。-Xで渡されるignore-all-spaceignore-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」再帰マージ_オプション_とは異なります。

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

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

現在いるブランチとマージの結果の間には差がないことがわかります。

これは、後でマージを行う際に、あるブランチがすでにマージされているとGitを欺くのに役立つことがあります。たとえば、releaseブランチから分岐し、いずれmasterブランチにマージしたい作業を行ったとします。その間に、masterでのバグ修正をreleaseブランチにバックポートする必要があります。バグ修正ブランチをreleaseブランチにマージし、同じブランチをmerge -s oursmasterブランチにもマージできます(修正はすでにそこにあるにもかかわらず)。そうすることで、後で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ブランチに戻ったところで、rack_branchブランチをメインプロジェクトのmasterブランチのrackサブディレクトリに取り込みます。

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

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

$ git checkout rack_branch
$ git pull

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

$ 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