Git の 3-way マージで共通祖先が 2 つの場合, 共通祖先はどんな感じになるのか調べてみた
マージされる 2 つのブランチの共通祖先が 2 つの場合, マージコンフリクトが発生した場合の共通祖先はどんな感じになるのか, サクッとはいかなかったのですが, それなりに調べてみました.
ヒストリを意図的に作る
共通祖先が 2 つあるマージを行うため, 次のようなヒストリを意図的に作ってみました:
B---E <- master <- HEAD
/ \ /
A X
\ / \
C---D <- hotfix
このようなヒストリで E
と D
をマージさせると, 交差マージ (criss-cross merge) となり E
と D
の共通祖先は B
と C
の 2 つなので, その 2 つが更にマージされて 1 つの共通祖先となります.
実際の A
, B
, C
, D
, E
の 5 つのコミットは次のように作りました:
echo 'A' > example.txt
git add example.txt
git commit -m 'A'
echo 'B' >> example.txt
git commit -am 'B'
git checkout -b hotfix HEAD^
echo 'C' >> example.txt
git commit -am 'C'
git merge master
echo "C\nB\nA" > example.txt
git commit -am 'D'
git checkout master
git merge hotfix^
echo "A\nB\nC" > example.txt
git commit -am 'E'
それぞれのコミットの example.txt
の内容は次のようになります.
A
の example.txt
:
A
B
の example.txt
:
A
B
C
の example.txt
:
A
C
D
の example.txt
:
C
B
A
E
の example.txt
:
A
B
C
実際にマージしてみる
そして次のコマンドで hotfix
(D
) を master
(E
) にマージさせてみます:
git merge hotfix
すると merge.verbosity
の値を 5
にしているので, 次のような詳細情報が表示されます:
Merging:
71d4d37 E
virtual hotfix
found 2 common ancestors:
c8d7410 B
945be04 C
Merging:
c8d7410 B
945be04 C
found 1 common ancestor:
02b437c A
Auto-merging example.txt
CONFLICT (content): Merge conflict in example.txt
Auto-merging example.txt
CONFLICT (content): Merge conflict in example.txt
Automatic merge failed; fix conflicts and then commit the result.
この情報から E
と hotfix
をマージして, B
と C
の 2 つの共通祖先が見つかり, B
と C
をマージして, 共通祖先 A
が見つかり, マージコンフリクトが 2 回発生してオートマージが失敗したということがわかります.
マージコンフリクトの内容
マージコンフリクトが 2 回発生した example.txt
の内容は次のようになります:
<<<<<<< HEAD
A
B
||||||| merged common ancestors
A
<<<<<<<<< Temporary merge branch 1
B
||||||||| merged common ancestors
=========
=======
>>>>>>> hotfix
C
<<<<<<< HEAD
||||||| merged common ancestors
>>>>>>>>> Temporary merge branch 2
=======
B
A
>>>>>>> hotfix
マージコンフリクトが 2 回発生しただけあって, なかなかやばいですね
しかも merge.conflictStyle
を diff3
に設定しているので余計です.
このことから共通祖先が 2 つあるマージを行うと, このような事態になりかねないということがわかりました.
また実験している分にはいいですが, 実際のマージで共通祖先が 2 つ見つかるという事は出来れば避けたいなという気持ちになりました
マーカの意味を紐解いてみる
せっかくなので, これらの難解なマーカを頼りに, どのように example.txt
でマージコンフリクトが発生しているのか一つ一つ紐解いてみたいと思います.
まず example.txt
から次の部分を切り取り:
||||||| merged common ancestors
A
<<<<<<<<< Temporary merge branch 1
B
||||||||| merged common ancestors
=========
=======
>>>>>>> hotfix
C
<<<<<<< HEAD
||||||| merged common ancestors
>>>>>>>>> Temporary merge branch 2
次の部分から:
<<<<<<< HEAD
A
B
=======
B
A
>>>>>>> hotfix
HEAD
と hotfix
に共通する部分はなく, この <<<<<<<
と =======
の間の二行と, =======
と >>>>>>>
の間の二行による競合が最初のマージコンフリクトとわかります.
そして先ほど切り取った次の部分から:
||||||| merged common ancestors
A
<<<<<<<<< Temporary merge branch 1
B
||||||||| merged common ancestors
=========
=======
>>>>>>> hotfix
C
<<<<<<< HEAD
||||||| merged common ancestors
>>>>>>>>> Temporary merge branch 2
一行目の |||||||
より下の部分が, 最初のマージコンフリクトの共通先祖の部分になり, その共通先祖は B
と C
マージから成るので, Temporary merge branch 1
と Temporary merge branch 2
という文が表示されています.
つまりはその部分が二番目のマージコンフリクトになります.
また二行目の A
は競合していないということもわかります.
そして競合している次の部分ですが:
<<<<<<<<< Temporary merge branch 1
B
||||||||| merged common ancestors
=========
=======
>>>>>>> hotfix
C
<<<<<<< HEAD
||||||| merged common ancestors
>>>>>>>>> Temporary merge branch 2
Temporary merge branch 1
の次の部分はいいのですが:
B
||||||||| merged common ancestors
その下の =========
の下の部分, つまりは Temporary merge branch 2
の部分:
=======
>>>>>>> hotfix
C
<<<<<<< HEAD
||||||| merged common ancestors
この部分には, 正直戸惑いました.
えっ, 次のような内容ではないのかと:
C
最初の二行や:
=======
>>>>>>> hotfix
最後の二行はなんなのかと:
<<<<<<< HEAD
||||||| merged common ancestors
どうして最初のマージコンフリクトの HEAD
と hotfix
のマーカがここに再度書き込まれているのだろうかと.
それなりに悪戦苦闘したのですが, 結局それらの 4 行の意味はわかりませんでした.
申し訳ありません.
マージコンフリクト解決ツールを使っみた
僕は Neovim を使っているので git mergetool
で, その example.txt
で発生しているるマージコンフリクトの内容を表示してみたところ:
画面上半分の真ん中のウィンドウが E
と D
の共通祖先, つまりは B
と C
のマージされた内容になり:
A
<<<<<<<<< Temporary merge branch 1
B
||||||||| merged common ancestors
=========
C
>>>>>>>>> Temporary merge branch 2
このように, Temporary merge branch 2
の方には, 僕を混乱に陥れた先ほどの 4 行がなく, 極めてわかりやすいです.
このように表示してくれればいいのになと思ってしまうのですが, 何かしらの理由があるのでしょう…
まとめ
Git でマージする時に共通祖先が 2 つ見つかった場合, その 2 つの共通祖先がマージされたものが, 実際の共通祖先となるので, その 2 つの共通祖先のマージでマージコンフリクトが発生した場合は, 多少頭を悩ますことになるかもしれません.
なのでできれば, このような交差マージにならないように気をつけることができればいいですね.
参考資料
関連記事
Git の mergetool で vimdiff を指定して Vim でマージコンフリクトを解決してみた2018.08.20
Git のコミット履歴を大胆に書き換えるなら git rebase -i がオススメ2018.08.23
Git の merge.conflictStyle を merge から diff3 に変更したら, マージコンフリクトがより解決しやすくなった2018.08.19
Git で再帰的ストラテジでマージする時のストラテジオプション ours と theirs の違い2018.08.18
Git のファストフォワードマージとは2018.08.15