gitのマージとコンフリクトを理解する


gitを使ってトピックブランチをメインブランチにマージするときにコンフリクトが起きることがある。

今の環境ではコンフリクトが起きないようなタスク管理をしているので滅多には起きないけど、それ故に起きたときに毎回どうすればいいのか迷ってしまう。それを避けるために今回、gitのマージとコンフリクトについてまとめた。

3ウェイマージ

gitのマージでは3ウェイマージを使用しているということで、まずは3ウェイマージについて確認する。

上図の場合、CからAの変更点(CA)、CからBの変更点(CB)があり、それらの変更点を両方取り込んだものがDになる。

Dが問題なく作成できるかどうかは、CAの変更点とCBの変更点の重複の有無とその内容による。

重複する点がなければそれぞれの変更を適用でき、問題なくDが作成される。
また、重複する点があるときでもその変更内容が同じであれば、問題なくDが作成される。

問題となるのは、重複する点の変更内容が異なるとき。このときはどちらを適用すべきか判断するために、手動で対応しなければならない。これがコンフリクト、変更点が衝突したという状態である。

CAの変更点、CBの変更点、CAとCBの変更点の重複度合い、という3つの比較に基づいてマージを行うということで、3ウェイマージと呼ばれている。

実作業でコンフリクトが起きるとき

3ウェイマージの図は、マージとコンフリクトを理解する上で十分でシンプルな図ではあるが、実環境ではどうなるのか。
ここでは自分の環境に考えられるケースを少し単純化して理解する。

上図はメインブランチmasterにトピックブランチtopicをマージしたことを示す図である。
masterはCからA1、A2とコミットがされており、トピックブランチはCからB1、B2、B3とコミットがされている。

Dはmasterブランチ上で

git checkout master
git merge topic

topicブランチをマージしたことによってできたマージコミットである。

3ウェイマージの図との違いは、それぞれのブランチにおいて変更点が複数あること。しかし、四角で囲んでいるようにそれら全体を一つの変更点として捉えると、3ウェイマージの図と同じである。

よって実環境でのマージでは、CからマージコミットのDまでにおいて、masterブランチ上で行われた変更点(A1,A2)の全体CA、topicブランチ上で行われた変更点(B1,B2,B3)の全体CB、そしてCAとCBの変更点の重複度合いによって判定され、gitが変更点CAとCBのどちらを適用するか判断できないときにコンフリクトが発生する。

git diffで確認する

ここからgit diffコマンドを使って、コンフリクトの状況を確認する。

gitのバージョンは2.10.2。

$ git --version
git version 2.10.2

上図の状況を再現してgit logコマンドを実行した結果。

$ git log --all --graph --pretty=format:'%h %d %s' --abbrev-commit
* c2c5bbc  (HEAD -> master) A2
* d0f6a9e  A1
| * 8b72a7a  (topic) B3
| * e1f1e2e  B2
| * 961b394  B1
|/
* 85a2869  C

masterブランチの変更点(CからA2まで)。

$ git diff 85a2869..master
diff --git a/merge.txt b/merge.txt
index e69de29..5556ed8 100644
--- a/merge.txt
+++ b/merge.txt
@@ -0,0 +1,2 @@
+A1
+A2

topicブランチの変更点(CからB3まで)。

$ git diff 85a2869..topic
diff --git a/merge.txt b/merge.txt
index e69de29..facd3ff 100644
--- a/merge.txt
+++ b/merge.txt
@@ -0,0 +1,3 @@
+B1
+B2
+B3

この状態でtopicブランチをマージするとコンフリクトが起きる。

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

コンフリクトが起きたときの状態。

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:   merge.txt

no changes added to commit (use "git add" and/or "git commit -a")

git diffコマンドでは、オプションなし、--ours--theirs--baseオプションの4つの方法で状況を確認できる。

git diff

オプションなしで実行すると、差分をcombined diff formatで表示する。

$ git diff
diff --cc merge.txt
index 5556ed8,facd3ff..0000000
--- a/merge.txt
+++ b/merge.txt
@@@ -1,2 -1,3 +1,8 @@@
++<<<<<<< HEAD
 +A1
 +A2
++=======
+ B1
+ B2
+ B3
++>>>>>>> topic

差分の1列目は、a/merge.txtに対する変更点の情報。2列目はb/merge.txtに対する変更点の情報。

a/merge.txtに対する変更点はB1,B2,B3の行とコンフリクトの印が追加されており、b/merge.txtに対する変更点は、A1,A2の行とコンフリクトの印が追加されたことがわかる。

git diff --ours

--oursオプションをつけて実行すると、手元のブランチとコンフリクトが起きたコミットとの差分を表示する。手元のブランチというのは、masterブランチのこと。

$ git diff --ours
* Unmerged path merge.txt
diff --git a/merge.txt b/merge.txt
index 5556ed8..1045802 100644
--- a/merge.txt
+++ b/merge.txt
@@ -1,2 +1,8 @@
+<<<<<<< HEAD
 A1
 A2
+=======
+B1
+B2
+B3
+>>>>>>> topic

masterブランチとの差分なので、topicブランチの変更点とコンフリクトの印が差分として表示される。

git diff --theirs

--theirsオプションは、--oursオプションとは逆。手先のブランチとコンフリクトが起きたコミットとの差分を表示する。手先のブランチというのは、topicブランチのこと。

$ git diff --theirs
* Unmerged path merge.txt
diff --git a/merge.txt b/merge.txt
index facd3ff..1045802 100644
--- a/merge.txt
+++ b/merge.txt
@@ -1,3 +1,8 @@
+<<<<<<< HEAD
+A1
+A2
+=======
 B1
 B2
 B3
+>>>>>>> topic

topicブランチとの差分なので、masterブランチの変更点とコンフリクトの印が差分として表示される。

git diff --base

--baseオプションをつけて実行すると、それぞれのブランチの分岐元のコミット(C)とコンフリクトが起きたコミットとの差分が表示される。

$ git diff --base
* Unmerged path merge.txt
diff --git a/merge.txt b/merge.txt
index e69de29..1045802 100644
--- a/merge.txt
+++ b/merge.txt
@@ -0,0 +1,8 @@
+<<<<<<< HEAD
+A1
+A2
+=======
+B1
+B2
+B3
+>>>>>>> topic

ブランチの分岐元から見た差分なので、masterブランチ、topicブランチの変更点とコンフリクトの印が差分として表示される。

git checkoutで解決する

git checkoutコマンドを使用すると、どちらか一方の変更点のみを適用することができる。
git checkoutコマンドで使用するオプションは、--ours--theirsの2つ。

git checkout --ours

--oursオプションを使用すると、手元のブランチの変更点のみを適用した状態にする。手元のブランチとはmasterブランチのこと。

これは

$ git diff --ours merge.txt
* Unmerged path merge.txt
diff --git a/merge.txt b/merge.txt
index 5556ed8..1045802 100644
--- a/merge.txt
+++ b/merge.txt
@@ -1,2 +1,8 @@
+<<<<<<< HEAD
 A1
 A2
+=======
+B1
+B2
+B3
+>>>>>>> topic

git diffコマンドを--oursオプションをつけて実行したときに現れる変更点をなくした状態になる。

$ git checkout --ours merge.txt
$ cat merge.txt
A1
A2

git checkout --theirs

--theirsオプションを使用すると、手先のブランチの変更点のみを適用した状態にする。手先のブランチとはtopicブランチのこと。

これは

$ git diff --theirs merge.txt
* Unmerged path merge.txt
diff --git a/merge.txt b/merge.txt
index facd3ff..1045802 100644
--- a/merge.txt
+++ b/merge.txt
@@ -1,3 +1,8 @@
+<<<<<<< HEAD
+A1
+A2
+=======
 B1
 B2
 B3
+>>>>>>> topic

git diffコマンドを--theirsオプションをつけて実行したときに現れる変更点をなくした状態になる。

$ git checkout --theirs merge.txt
$ cat merge.txt
B1
B2
B3

git mergetoolで解決する

大抵は、メインブランチの変更点を壊さないようにトピックブランチの変更点を適用する。

git diffコマンドで差分を確認しながらエディターで編集するというのは面倒なので、git mergetoolコマンドを使いマージツールを起動して解決する。ここでは、vimdiffを使って解決する。

$ git mergetool merge.txt

This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
tortoisemerge vimdiff emerge
Merging:
merge.txt

Normal merge conflict for 'merge.txt':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (vimdiff):

git mergetoolを実行して起動したvimdiffに、3ウェイマージの図を当てたものが上図になる。

上段の真ん中がC、左がA、右がB。vimdiffではそれぞれBASE、LOCAL、REMOTEとなる。
そして下段がDになり、ここを編集してコンフリクトを解消する。

実作業ではAがメインブランチでBがトピックブランチとなる。普段はCからAの変更点に影響がないように、CからBの変更点を適用するようにしてDを編集する。

編集したらvimを終了する。以下は編集後のステータス。

$ git status
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

    modified:   merge.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    merge.txt.orig

最後にgit commitコマンドでコミットし、変更点を反映する。

$ git commit -m 'merged'
[master 8d7794b] merged

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    merge.txt.orig

nothing added to commit but untracked files present (use "git add" to track)

以上でマージ作業は終了。

git logコマンドで、topicブランチがmasterブランチにマージされたことが確認できる。

$ git log --all --graph --pretty=format:'%h %d %s' --abbrev-commit
*   8d7794b  (HEAD -> master) merged
|\
| * 8b72a7a  (topic) B3
| * e1f1e2e  B2
| * 961b394  B1
* | c2c5bbc  A2
* | d0f6a9e  A1
|/
* 85a2869  C

参考