Git 🌙
Chapters ▾ 2nd Edition

7.8 Git のさまざまなツール - 高度なマージ手法

高度なマージ手法

Git を使うと、大抵の場合マージは簡単です。違うブランチを何度もマージすることも簡単なので、一度作ったブランチで延々と作業を続けながら、常に最新の状態に保っておけます。そうすれば、マージのたびに少しずつコンフリクトを解消することになるので、作業の最後で一度だけマージする場合のように、膨大なコンフリクトにあっけにとられることもなくなるでしょう。

とはいえ、ややこしいコンフリクトは発生してしまうものです。他のバージョン管理システムとは違い、Git は無理をしてまでコンフリクトを解消しようとはしません。Git は、マージの内容が明確かどうか正確に判断できるよう作られています。しかし、コンフリクトが発生した場合は、わかったつもりになってコンフリクトを解消してしまうようなことはしません。すぐに乖離してしまうようなブランチをいつまでもマージしないでおくと、問題になる場合があります。

この節では、どういった問題が起こりうるのか、そしてそういった状況を解決するのに役立つ Git のツールを見ていきます。また、いつもとは違う方法でマージを行うにはどうすればいいか、マージした内容を元に戻すにはどうすればいいかも見ていきましょう。

マージのコンフリクト

マージのコンフリクトをどのように解消するか、基本的なところを マージ時のコンフリクト で紹介しました。ここでは、複雑なコンフリクトの場合に、状況を把握しコンフリクトを上手に解消するための Git ツールを紹介します。

まず、可能な限り、作業ディレクトリがクリーンな状態であることを確認しましょう。コンフリクトを起こす可能性のあるマージを実行するのはその後です。作業中の内容があるのなら、一時保存用のブランチを作ってコミットするか stash に隠してしまいましょう。こうしておけば、何が 起こってもやり直しがききます。以下で説明するヒントのなかには、作業ディレクトリの変更を保存せずにマージを行うと未保存の作業が消えてしまうものもあります。

では、わかりやすい例を見てみましょう。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 'converted hello.rb to DOS'
[whitespace 3270f76] converted 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 'hello mundo change'
[whitespace 6d338d2] hello mundo change
 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 'document the function'
[master bec6336] document 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文字以上の空白文字が連続している場合にそれを同一であるとみなして処理します。

$ 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 は空白文字を前もって上手に処理してくれます。ただし、自動で処理するのは難しいけれど、変更の内容によっては処理をスクリプトに落とし込める場合があります。ここでは例として、空白文字がうまく処理されず、手動でコンフリクトを解消することになったとしましょう。

その場合、マージしようとしているファイルを前もって dos2unix プログラムで処理しておく必要があります。どうすればいいでしょうか。

手順はこうです。まずはじめに、実際にコンフリクトを発生させます。次に、コンフリクトしているファイルを、自分たちの分・相手側(マージしようとしているブランチ)の分・共通(両方のブランチの共通の祖先)の分の3バージョン用意します。最後に、自分たちか相手側、どちらかのファイルを修正し、該当のファイル1つだけを改めてマージします。

なお、この手順で使う3バージョンは簡単に用意できます。Git は、これらのバージョンを “stages” というインデックスに番号付きで保存してくれているのです。Stage 1 は共通の祖先、stage 2 は自分たちの分、Stage 3は MERGE_HEAD (マージしようとしている、“theirs” にあたる)の分になります。

コンフリクトが発生したファイルの3バージョンを用意するには、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

そんな簡単なの?と拍子抜けしたのなら、Git の配管コマンドである ls-files -u を使ってみましょう。各ファイルの 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 を調べるための簡易記法です。

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 コマンドを使います。そうすれば、作業ディレクトリにあるコミット予定のファイルを、上述の3ステージと比較できるのです。実際にやってみましょう。

まず、マージ前のブランチの状態を手元の現状と比較する(マージが何をどう変更したのか確認する)には、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()

そうすると、作業中のブランチがどう変更されたか(マージすることでこのファイルがどう変更されるか)がすぐわかります。この例では、変更されるのは1行だけです。

次に、相手側のファイルがマージ前後でどう変わったかを確認するには、git diff --theirs を使います。なお、この例と次の例では、空白文字を除外するために -b オプションを使用しています。これから比較するのは空白文字が処理済みの手元のファイル hello.theirs.rb ではなく、Git のデータベースに格納されているデータだからです。

$ 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

コンフリクトのチェックアウト

ここで、さきほど試したコンフリクトの解決方法があまりよくなかった、もしくはマージ対象の一方(あるいは両方)を編集してもコンフリクトをうまく解消できず、これまでの流れを詳しく把握する必要が生じたとします。

これを解説するには、先程の例を少し変更しておくほうがいいでしょう。今回は両方のブランチそれぞれにコミットが数回なされており、かつマージ時にはコンフリクトが発生するような状態だと仮定します。

$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) update README
* 9af9d3b add a README
* 694971d update phrase to hola world
| * e3eb223 (mundo) add more tests
| * 7cff591 add testing script
| * c3ffff1 changed 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つめは、git checkout コマンドの --conflict オプションです。これを実行すると、指定したファイルをもう一度チェックアウトし、コンフリクトマーカーを書きなおします。コンフリクトを手で直していてうまくいかず、最初からやり直すためにリセットしたいときに便利です。

--conflict オプションには diff3merge が指定できます(デフォルトは merge)。前者を指定すると、コンフリクトマーカーが少し変わってきます。通常のマーカーである “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.conflictstylediff3 に変更してみましょう。

$ git config --global merge.conflictstyle diff3

git checkout コマンドには --ours--theirs オプションを指定することもできます。これを使うと、何かをマージする代わりに、どちらか一方を選択して簡単にチェックアウトできます。

これは、バイナリデータのコンフリクトを解消するとき(使いたい方を選べばよい)や、他のブランチから特定のファイルを取り込みたいときに便利でしょう。後者であれば、マージコマンドを実行してから該当のファイルを --ours--theirs を指定してチェックアウトし、コミットしてください。

マージの履歴

もう一つ、コンフリクトの解決に使える便利なツールが git log です。どういった流れでコンフリクトが発生したのかを追跡するときに使えます。というのも、歴史を少し紐解いてみると、平行して進行していた2つの開発作業がなぜコードの同じ部分を編集するに至ったか、その理由を思い出せたりするからです。

マージ対象のブランチに含まれるコミットを重複分を除いて表示させるには、トリプルドット で触れた「トリプルドット」記法を使います。

$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 update README
< 9af9d3b add a README
< 694971d update phrase to hola world
> e3eb223 add more tests
> 7cff591 add testing script
> c3ffff1 changed text to hello mundo

この例では、全部で6コミットがわかりやすい状態でリスト表示されています。それぞれのコミットがどちらのブランチのものかもわかるようになっています。

また、より細かく流れを把握するために、表示内容を絞り込むこともできます。git log コマンドの --merge オプションを使うと、表示されるのはコンフリクトが発生しているファイルを編集したコミットだけになるのです。

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

また、このコマンドに -p オプションを追加すると、表示される内容がコンフリクトしているファイルの差分だけになります。コンフリクトの原因を把握して賢明な方法でそれを解消するために、必要な背景や経緯をすばやく理解したいときに とても 役に立つでしょう。

Combined 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()

これは “Combined Diff” という形式で、各行の行頭2文字を使って関連情報を表示します。具体的には、作業ディレクトリの内容とマージ元のブランチ(「ours」)の内容に差分があれば1文字目を、作業ディレクトリとマージの相手側のブランチ(「theirs」)に差分があれば2文字目が使われます。

この例では、作業ディレクトリには存在する >>>>> の行が、マージ対象のブランチどちらにも存在していないことがわかります。それもそのはず、これらの行はマージによって挿入されたからです。差分をわかりやすくするために挿入されたこれらの行は、手動で削除する必要があります。

このコンフリクトを解消してから 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 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 にマージしてしまったとしましょう。コミット履歴は以下のようになっているはずです。

間違って作成したマージコミット
図 137. 間違って作成したマージコミット

この状況を修正するには2通りのやり方があります。どのように修正したいかに応じて使い分けましょう。

参照の修正

不要なマージコミットをまだプッシュしていないのなら、ブランチが指し示すコミットを変更してしまうのが一番簡単な解決方法です。 大半の場合、間違って実行した git merge の後に git reset --hard HEAD~ を実行すれば、ブランチのポインタがリセットされます。実行結果は以下のようになるでしょう。

`git reset --hard HEAD~` 実行後の歴史
図 138. git reset --hard HEAD~ 実行後の歴史

reset コマンドについては リセットコマンド詳説 で触れましたので、ここで何が起こっているか、理解するのは難しいことではないと思います。 念のためおさらいしておきましょう。reset --hard を実行すると、通常は以下の処理が走ります。

  1. HEAD が指し示すブランチを移動する この例では、マージコミット (C6) が作成される前に master が指していたところまで戻します。

  2. インデックスの内容を HEAD と同じにする

  3. 作業ディレクトリの内容をインデックスと同じにする

この方法の欠点は、歴史を書き換えてしまう点です。共有リポジトリで作業していると、問題視される場合があります。 書き換えようとしているコミットをほかの人たちもプルしてしまっている場合は、reset は使わないほうが無難でしょう。理由については ほんとうは怖いリベース を確認してみてください。 また、新たなコミットがマージ以後に追加されている場合は、この方法はうまくいきません。参照を移動してしまうと、追加された内容を削除することになってしまうからです。

コミットの打ち消し

ブランチのポインタを動かすという上述の方法が機能しない場合、既存のコミットの内容を打ち消す新しいコミットを作ることもできます。 これは “revert” と呼ばれる操作で、今回の例では以下のようにすると呼び出せます。

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

-m 1 オプションで、保持すべき「メイン」の親がどれであるかを指定します。 HEAD に対するマージ(git merge topic)を実行すると、マージコミットには2つの親ができます。HEAD (C6) と マージされるブランチの最新コミット (C4) です。 この例では、第2の親 (C4) をマージしたことで生じた変更をすべて打ち消しつつ、第1の親 (C6) の内容は保持したままにしてみます。

revert のコミットを含む歴史は以下のようになります。

`git revert -m 1` の後の歴史
図 139. git revert -m 1 の後の歴史

新しく作成されたコミット ^M の内容はコミット C6 とまったく同じですので、歴史を今後振り返ると、マージなど一度も実施されていないかのように思えます。ただし、実際のところは HEAD の方の歴史にはマージされていないコミットが残ったままになってしまいます。 この状態で topicmaster にマージしようとすると、Git は状況を正確に判断できません。

$ git merge topic
Already up-to-date.

これは、topic ブランチにあるものは master ブランチにもすべて存在している、という状態です。 更に悪いことに、この状態の topic ブランチにコミットを追加してマージを行うと、revert されたマージ の変更だけが取り込まれることになります。

よくないマージを含む歴史
図 140. よくないマージを含む歴史

ここでは revert してしまった変更を取り戻したいわけですから、revert 済みの古いマージコミットをもう一度 revert し、 そのうえで 改めてマージするのが一番いいでしょう。

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
revert 済みのマージコミットを再度マージした後の歴史
図 141. revert 済みのマージコミットを再度マージした後の歴史

そうすると、M^M が互いを打ち消します。 ^^M によって C3C4 の変更が取り込まれたことになりますし、C8 のマージコミットによって C7 が取り込まれます。これでようやっと、topic ブランチはすべてマージされました。

他のマージ手法

ここまでは2つのブランチをマージする通常の手法を見てきました。一般的には、「再帰」 と呼ばれるマージ戦略によって処理されている手法です。これ以外にもブランチをマージする手法がありますので、いくつかをざっと紹介します。

Our か Theirs の選択

1つめに紹介するのは、マージの「再帰」モードで使える便利なオプションです。-X と組み合わせて使う ignore-all-spaceignore-space-change といったオプションは既に紹介しました。Git ではそれ以外にも、コンフリクトが発生したときにマージ対象のどちらを優先するかを指定できます。

Git のデフォルトでは、マージしようとしているブランチ間でコンフリクトがある場合、コードにはコンフリクトを示すマーカーが挿入され、該当ファイルはコンフリクト扱いとなり、手動で解決することになります。 そうではなく、マージ対象のブランチどちらかを優先して自動でコンフリクトを解消して欲しいとしましょう。その場合、merge コマンドに -Xours-Xtheirs オプションを指定できます。

これらが指定されると、コンフリクトを示すマーカーは追加されません。マージ可能な差異は通常どおりマージされ、コンフリクトが発生する差異については指定された側のブランチの内容が採用されます。これはバイナリデータについても同様です。

以前使った “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 コマンドでも使用可能です。git merge-file --ours のような形で実行すれば、ファイルを個別にマージするときに使えます。

また、同じようなことをしたいけれど、マージされる側の変更点は何一つ取り込みたくない、というようなことになったとしましょう。その場合、より強力な選択肢として “ours” というマージ 戦略 が使えます。これは “ours” を使って行う再帰的なマージ用の オプション とは異なります。

ではその戦略が何をするかというと、偽のマージが実行されるのです。マージ対象の両ブランチを親としたマージコミットが新たに作成されますが、マージされる側のブランチの内容については一切考慮されません。現在いるブランチの内容が、マージの結果としてそのままそっくり記録されます。

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

このとおり、マージ結果とマージ直前の状態に一切変更点がないことがわかります。

これが役に立つのは、後々になってマージを行う際に Git を勘違いさせて、ブランチをマージ済みとして取り扱わせたい場合です。具体例を挙げて説明しましょう。「リリース」ブランチを作成して作業を進めているとします。そのブランチは、いずれ “master” ブランチにマージするつもりです。ここで、“master” 上で行われたバグ修正を release ブランチにも取り込む必要が出てきました。そのためには、まずはバグ修正のブランチを release ブランチにマージし、続いて merge -s ours コマンドで同じブランチを master ブランチにもマージします(修正は既に取り込まれていますが、あえて実施します)。そうしておけば、release ブランチをマージする際に、バグ修正のブランチが原因でコンフリクトが発生することはありません。

サブツリーマージ

サブツリーマージの考え方は、ふたつのプロジェクトがあるときに一方のプロジェクトをもうひとつのプロジェクトのサブディレクトリに位置づけるというものです。 サブツリーマージを指定すると、Git は一方が他方のサブツリーであることを大抵の場合は理解して、適切にマージを行います。

これから、既存のプロジェクトに別のプロジェクトを追加し、前者のサブディレクトリとして後者をマージする例を紹介します。

まずは 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 プロジェクトのルートが rack_branch ブランチに取得でき、あなたのプロジェクトが master ブランチにある状態になりました。 まずどちらかをチェックアウトしてそれからもう一方に移ると、それぞれ別のプロジェクトルートとなっていることがわかります。

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

これは、考えようによっては変な話です。リポジトリにあるブランチがすべて、同一プロジェクトのブランチである必要はない、ということなのですから。めったにない話です(ちょっとやそっとのことでは役に立たないので)が、完全に異なる歴史を持つ複数のブランチを1つのリポジトリで保持するのはとても簡単なのです。

この例では、Rack プロジェクトを master プロジェクトのサブディレクトリとして取り込みたくなったとしましょう。そのときには、git read-tree を使います。read-tree とその仲間たちについては [ch10-git-internals] で詳しく説明します。現時点では、とりあえず「あるブランチのルートツリーを読み込んで、それを現在のステージングエリアと作業ディレクトリに書き込むもの」だと認識しておけばよいでしょう。まず master ブランチに戻り、 rack_branch ブランチの内容を master ブランチの rack サブディレクトリに取り込みます。

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

これをコミットすると、Rack のファイルをすべてサブディレクトリに取り込んだようになります。そう、まるで tarball からコピーしたかのような状態です。おもしろいのは、あるブランチでの変更を簡単に別のブランチにマージできるということです。もし Rack プロジェクトが更新されたら、そのブランチに切り替えてプルするだけで本家の変更を取得できます。

$ git checkout rack_branch
$ git pull

これで、変更を master ブランチにマージできるようになりました。git merge -s subtree を使えばうまく動作します。が、Git は歴史もともにマージしようとします。おそらくこれはお望みの動作ではないでしょう。変更をプルしてコミットメッセージを埋めるには、再帰的マージ戦略を指定するオプション -Xsubtree のほかに --squash オプションを使います(再帰的戦略はこの場合のデフォルトにあたりますが、使用されるオプションを明確にするためあえて記載してあります)。

$ 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 ブランチにマージし、それをメンテナに投稿したり本家にプッシュしたりといったことも可能です。

この機能を使えば、サブモジュールを使った作業手順に似た手順(サブモジュール で紹介する予定)を、サブモジュールなしで採用できます。違うプロジェクトのデータをブランチとしてプロジェクトリポジトリ内に保持しておけますし、サブツリーマージを使ってそのブランチを取組中のプロジェクトに取り込むこともできます。これは見方によっては、例えば、すべてのコードが同じ場所にコミットされるという意味では、便利だといえるでしょう。ですが、欠点がないわけではありません。構成が複雑になり変更を取り込む際に間違いやすくなってしまうでしょう。関係ないリポジトリに誤ってプッシュしてしまうことだってあるかもしれません。

また、少し違和感を覚えるかもしれませんが、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