CH 24 / 75第4部 ブランチとマージ

コンフリクトを解決する

このページで分かること

  • コンフリクトがどんなときに起きるかが分かる
  • <<<<<<<・=======・>>>>>>> の印を読んで、どちらの変更かを見分けられる
  • ファイルを直して git add → git commit でマージを仕上げられる
  • git merge --abort でマージ前に戻せ、zdiff3 で元の内容も表示できる
このページの目次12 items

23で、両方のブランチが同じ行を変えているとマージが止まると書きました。これがコンフリクト(conflict、衝突)です。このページでは、わざとコンフリクトを起こし、印の読み方と、止まったマージを自分の手で仕上げる手順を練習します。

コンフリクトはなぜ起きるか#

23の 3-way マージでは、マージベースから見て「片方だけが変えた部分」はその変更を採用しました。では、同じ部分を両方が別々に変えていたらどうでしょうか。どちらを採るべきかは、内容の意味が分かる人にしか決められません。そこで Git は自動で決めずに、マージを途中で止めて人に任せます。

コンフリクトは失敗やエラーではありません。「ここだけは判断してください」という Git からの問い合わせです。

練習用のリポジトリで起こしてみます。ブランチでは豆乳をオーツミルクに、main では豆乳を2本に変えます。どちらも買い物メモの1行目です。

text7 lines
cd ~/git-practice
git switch -c oat-milk
sed -i.bak 's/豆乳を買う/オーツミルクを買う/' shopping.txt && rm shopping.txt.bak
git commit -am "豆乳をオーツミルクに変更"
git switch main
sed -i.bak 's/豆乳を買う/豆乳を2本買う/' shopping.txt && rm shopping.txt.bak
git commit -am "豆乳を2本に変更"

マージを止めて、状況を読む#

text1 lines
git merge oat-milk
text3 lines
Auto-merging shopping.txt
CONFLICT (content): Merge conflict in shopping.txt
Automatic merge failed; fix conflicts and then commit the result.

CONFLICT と出て、マージが止まりました。こういうときは、まず git status です。

text1 lines
git status
text10 lines
On branch main
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:   shopping.txt

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

読むべきところは3つです。

表示意味
You have unmerged paths.マージの途中で、まだ解決していないファイルがある
both modified: shopping.txt両方が変えて衝突したファイル。ここを直す
fix conflicts and run "git commit" / git merge --abort次にすること:直してコミットするか、やめるか

印の読み方#

衝突したファイルの中を見ます。

text1 lines
cat shopping.txt
text8 lines
<<<<<<< HEAD
豆乳を2本買う
=======
オーツミルクを買う
>>>>>>> oat-milk
卵を買う
パンを買う
りんごを買う

Git が、衝突した部分に印(コンフリクトマーカー)を書き込みました。

  • <<<<<<< HEAD から ======= まで:今いるブランチ(main)の内容
  • ======= から >>>>>>> oat-milk まで:取り込もうとしているブランチ(oat-milk)の内容

衝突しなかった「卵」以下の行は、そのまま残っています。

コンフリクトしたファイルで、<<<<<<< HEAD から ======= までが今いるブランチ main の内容、||||||| のあとがマージベースの内容、======= から >>>>>>> oat-milk までが取り込むブランチの内容であることを示し、右側に git merge で止まる、ファイルを書き直す、git add、git commit の4ステップを並べた図
印の範囲を読み分け、4つのステップでマージを仕上げる

いったんやめる:git merge --abort#

どう直すか迷ったら、マージそのものを取り消して、始める前の状態に戻れます。

text3 lines
git merge --abort
git status -s
cat shopping.txt
text4 lines
豆乳を2本買う
卵を買う
パンを買う
りんごを買う

git status -s は何も表示せず、ファイルもマージ前の main の内容に戻りました。コンフリクトが起きても、--abort で何もなかったことにできると知っておくと、落ち着いて向き合えます。

元の内容も表示する:zdiff3#

さきほどの印では、「2本」と「オーツミルク」のどちらが元の内容から変わったのかが分かりません。設定を1つ変えると、マージベースの内容も一緒に表示されます。

text3 lines
git config --global merge.conflictStyle zdiff3
git merge oat-milk
cat shopping.txt
text10 lines
<<<<<<< HEAD
豆乳を2本買う
||||||| ca9807e
豆乳を買う
=======
オーツミルクを買う
>>>>>>> oat-milk
卵を買う
パンを買う
りんごを買う

||||||| から ======= までが、マージベース(ca9807e)の内容です。元は「豆乳を買う」で、main は本数を、oat-milk は種類を変えたのだと読み取れます。両方の意図が分かるので、両方を生かした直し方を選べます。 このガイドでは、この設定をおすすめします。

直して、仕上げる#

1. ファイルを書き直す#

エディタで shopping.txt を開き、印の行をすべて消して、残したい内容に書き直します。今回は両方の意図を生かして「オーツミルクを2本買う」にします。

text4 lines
オーツミルクを2本買う
卵を買う
パンを買う
りんごを買う

片方だけを採る、両方の行を残す、まったく別の内容にする、のどれでも構いません。書き直した結果が、そのままマージの結果になります。

2. git add で「解決した」と伝える#

text2 lines
git add shopping.txt
git status
text6 lines
On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
	modified:   shopping.txt

コンフリクトの途中での git add は、「このファイルは解決した」という合図を兼ねています。All conflicts fixed(すべて解決済み)になりました。

3. git commit でマージを完了する#

text1 lines
git commit --no-edit
text1 lines
[main 52db35e] Merge branch 'oat-milk'

--no-edit は、Git が用意した Merge branch 'oat-milk' というメッセージをそのまま使うオプションです。付けなければエディタが開くので、どう解決したかを書き足すこともできます。

text1 lines
git log --oneline --decorate --graph -n 4
text7 lines
*   52db35e (HEAD -> main) Merge branch 'oat-milk'
|\  
| * 2eff5e0 (oat-milk) 豆乳をオーツミルクに変更
* | 085ffbb 豆乳を2本に変更
|/  
*   ca9807e Merge branch 'shopping-apple'
|\  

23と同じ形のマージコミットができました。取り込み済みの oat-milk は片付けておきます。

text1 lines
git branch -d oat-milk

よくある疑問#

Q. コンフリクトを減らす方法はありますか?
A. ブランチを長く放置しないことが一番効きます。分かれている期間が長いほど、同じ場所を触る可能性が高くなるからです。作業を小さく分けて早めに取り込む、こまめに main の変更を取り込む(25の rebase も使えます)、の2つを心がけてください。

Q. VS Code などのエディタでも解決できますか?
A. できます。VS Code は印のある部分に「Accept Current Change」(今いるブランチ側を採る)、「Accept Incoming Change」(取り込む側を採る)などのボタンを表示します。押すと印も一緒に消えます。そのあとの git add と git commit は同じです。

まとめ#

  • 両方のブランチが同じ部分を別々に変えると、コンフリクトでマージが止まる。エラーではなく、判断の依頼
  • git status の both modified が直すべきファイル
  • <<<<<<< HEAD 〜 ======= が今いるブランチ、======= 〜 >>>>>>> が取り込む側。zdiff3 にすると元の内容も見える
  • 印を消して書き直し、git add → git commit で仕上げる
  • 迷ったら git merge --abort でマージ前に戻れる

次のページ#

25. rebase を理解する ── マージとは別の取り込み方である rebase で、枝分かれした履歴を1本にまっすぐ並べ直します。