Git
目次 ▾ 第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` plumbingコマンドを使用して、これらのファイルそれぞれのGit blobの実際のSHA-1を取得することもできます。

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

`:1:hello.rb`は、そのblob 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`を実行します。この例と次の例では、クリーンアップされた`hello.theirs.rb`ファイルではなく、Gitにあるものと比較しているため、空白文字を削除するために`-b`を使用する必要があります。

$ 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`オプションも受け入れることができ、マージせずに片側を選択する非常に高速な方法です。

これは、片側を選択できるバイナリファイルの競合、または別のブランチから特定のファイルのみをマージする場合(マージを実行してから、コミットする前に片側またはもう片側から特定のファイルをチェックアウトする)に特に役立ちます。

マージログ

マージ競合を解決する場合、もう1つの便利なツールは`git log`です。これにより、競合に寄与した可能性のあるコンテキストを取得できます。2つの開発ラインがコードの同じ領域に触れていた理由を思い出すために、少し履歴を確認すると、非常に役立つ場合があります。

このマージに関与するブランチのいずれかに含まれていた固有のコミットの完全なリストを取得するには、Triple Dotで学習した「トリプルドット」構文を使用できます。

$ 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`オプションで実行すると、競合が発生したファイルへのdiffのみが表示されます。これは、何かが競合する理由と、より賢く解決する方法を理解するのに役立つコンテキストをすばやく提供するのに**非常に**役立ちます。

結合されたdiff形式

Gitは成功したマージの結果をステージングするため、競合状態のマージ中に`git diff`を実行すると、現在競合しているものだけが表示されます。これは、まだ解決する必要があるものを確認するのに役立ちます。

マージ競合の直後に`git diff`を実行すると、かなり独特な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()

この形式は「結合された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 log`からも取得できます。`git show`をマージコミットで実行した場合、または`git log -p`に`--cc`オプションを追加した場合(デフォルトでは、マージ以外のコミットの差分のみを表示します)、Gitはこの形式を出力します。

$ 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 Demystifiedで既に説明したので、何が起こっているのかを理解するのはそれほど難しくないはずです。簡単に復習しましょう。`reset --hard`は通常、3つのステップを実行します。

  1. ブランチのHEADポインタを移動します。この場合、`master`をマージコミット(`C6`)以前の位置に移動します。

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

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

このアプローチの欠点は、履歴を書き換えることであり、共有リポジトリでは問題になる可能性があります。何が起こる可能性があるかについては、The Perils of Rebasingを参照してください。簡単に言うと、他の人が書き換えているコミットを持っている場合は、`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`)のすべてのコンテンツを保持します。

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

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つのブランチの通常のマージについて説明しました。ただし、ブランチをマージする方法は他にもあります。いくつか簡単に説明しましょう。

自分側または相手側の優先

まず、通常の「recursive」マージモードで実行できるもう1つの便利な機能があります。`-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`ブランチにマージし、同じブランチを`merge -s ours`を使用して`master`ブランチにマージすることもできます(修正は既に存在するにも関わらず)。これにより、後で`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`を使用してこれを行うことができます。Git Internalsで`read-tree`とその仲間について詳しく学習しますが、現時点では、あるブランチのルートツリーを現在のステージングエリアと作業ディレクトリに読み込むことを知っておいてください。`master`ブランチに戻り、`rack_branch`ブランチをメインプロジェクトの`master`ブランチの`rack`サブディレクトリにプルします。

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

コミットすると、tarballからコピーしたかのように、そのサブディレクトリ下にすべてのRackファイルがあるように見えます。興味深いのは、一方のブランチからもう一方のブランチに比較的簡単に変更をマージできることです。したがって、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`ブランチにマージして、メンテナンス担当者に送信したり、上流にプッシュしたりすることもできます。

これにより、サブモジュール(Submodulesで説明します)を使用せずに、サブモジュールワークフローと多少似たワークフローを実現できます。関連する他のプロジェクトを含むブランチをリポジトリに保持し、プロジェクトに定期的にサブツリーマージできます。これはいくつかの点で優れています。たとえば、すべてのコードが1か所にコミットされます。ただし、変更を再統合したり、誤ってブランチを関連のないリポジトリにプッシュしたりするのがやや複雑で、ミスをしやすくなるという欠点もあります。

もう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