1. ホーム
  2. Git

【Git】コンフリクト(衝突)の解消手順|マージで競合したときの直し方を解説

Share

git merge を実行したら CONFLICT と表示され、ファイルを開いたら見慣れない記号が並んでいた。Git を使い始めて最初につまずくのが、この「コンフリクト(衝突)」ではないでしょうか。見た目のインパクトは強いのですが、やることは「どちらの内容を残すかを決めて、印を消して、コミットする」だけです。この記事では、コンフリクトが起きる理由からマーカーの読み方、解消の手順、やり直したいときの戻し方までを順番に解説します。

コンフリクトは「Git が判断できない」ときに起きる

Git のマージは、2つのブランチで行われた変更を1つにまとめる操作です。ほとんどの場合、Git は変更された場所を見比べて自動的に合成してくれます。同じファイルでも、片方が1行目を、もう片方が100行目を書き換えただけなら、両方の変更がそのまま取り込まれます。

ところが、同じファイルの同じ行を、両方のブランチで別々に書き換えていた場合はそうはいきません。片方は背景色を白に、もう片方は同じ行を灰色に変えていたとしたら、Git には「どちらが正しいのか」を判断する材料がありません。勝手にどちらかを選べば変更が失われてしまうので、Git は手を止めて「ここは人間が決めてください」と申し送りをします。これがコンフリクトの正体です。

つまりコンフリクトはエラーでも故障でもなく、Git が安全側に倒した結果です。実際にマージを実行すると、ターミナルには次のような表示が出ます。

ターミナル
$ git merge feature/header
Auto-merging style.css
CONFLICT (content): Merge conflict in style.css
Automatic merge failed; fix conflicts and then commit the result.

CONFLICT (content): Merge conflict in style.css が「style.css の中身がぶつかった」という報告です。最後の行にあるとおり、直してからコミットすればマージは続きから完了します。

まず git status で「今どうなっているか」を確認する

コンフリクトが起きると、リポジトリはマージの途中で止まった状態になります。何がぶつかっているのかを知るために、まず git status を実行します。

ターミナル
$ git status
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:   style.css

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

読むべきところは3つです。You have unmerged paths. は「まだ決着していないファイルがある」という意味で、マージが途中で止まっていることを表します。Unmerged paths: の下に並ぶのが、ぶつかっているファイルの一覧です。そして各行の頭に付いた both modified が「両方のブランチで変更された」という印になります。

ここに挙がっていないファイルは、Git がすでに自動でマージしてステージに入れてくれています。手を入れる必要があるのは Unmerged paths: のファイルだけなので、この一覧をメモしておきましょう。

コンフリクトマーカーの読み方

コンフリクトしたファイルをエディタで開くと、ぶつかった箇所に Git が目印を書き込んでいます。これをコンフリクトマーカーと呼びます。

style.css
.site-header {
  padding: 24px 16px;
<<<<<<< HEAD
  background-color: #ffffff;
  border-bottom: 1px solid #dddddd;
=======
  background-color: #f5f5f5;
>>>>>>> feature/header
}

記号は3つあり、それぞれが区切りの役割を持っています。

マーカー意味
<<<<<<< HEADここから下が取り込み先(今いるブランチ)の現在の内容。この例では main 側
=======2つの内容の境目。ここより上が自分側、下が相手側
>>>>>>> feature/headerここまでが取り込もうとしているブランチの内容。末尾にそのブランチ名が付く

上の例は、main では背景を白にしてボーダーを足したのに対し、feature/header では同じ場所で背景を薄いグレーにしていた、という状況です。HEAD は「いま自分がいる場所」を指す Git の呼び名なので、上側が取り込み先、下側が持ってこようとしている側と覚えておけば迷いません。

マーカーは Git が書き加えた目印であって、ファイルの正しい内容ではありません。解消するときは、残したい内容を決めたうえでマーカーの行そのものも必ず削除します。消し忘れると、CSS なら構文エラー、プログラムなら実行時エラーの原因になります。

コンフリクトを解消する手順

1. ファイルを開いて正しい形に直す

マーカーで囲まれた部分を「最終的にこうあってほしい」という形に書き換えます。選択肢は片方を残すことだけではなく、両方の変更が必要なら混ぜても構いませんし、どちらでもない第三の書き方にしても問題ありません。判断の基準は「どちらのブランチが正しいか」ではなく、マージ後のコードとして正しく動くかです。

背景色は feature/header 側を採用しつつ、main で追加したボーダーも残したい場合は、次のように書き直します。

style.css
.site-header {
  padding: 24px 16px;
  background-color: #f5f5f5;
  border-bottom: 1px solid #dddddd;
}

マーカーの3行はすべて消え、ふつうの CSS に戻りました。1つのファイルに複数の衝突箇所があることもあるので、ファイル内を <<<<<<< で検索して残っていないか確認しておくと安心です。

2. git add で「解決済み」を伝える

ふだんの git add は「コミットに含める」という意味ですが、コンフリクト中はそれに加えて「このファイルは解決しました」と Git に宣言する役割を持ちます。これをしないと、中身を直しても Git は未解決のままだと判断します。

ターミナル
# 解消したファイルを指定する
git add style.css

# 状態を確認する(Unmerged paths が消えていれば OK)
git status

すべて解消すると git status から Unmerged paths: が消え、All conflicts fixed but you are still merging. という表示に変わります。これが「あとはコミットするだけ」の合図です。

3. git commit でマージを完了させる

ターミナル
git commit

このとき -m でメッセージを書く必要はありません。マージ中のコミットでは Merge branch 'feature/header' のようなメッセージが自動で用意され、エディタが開いてその内容が表示されます。そのまま保存して閉じればコミットが作られます。何をどう解消したかを追記しておくと、あとから履歴を読む人の助けになります。

コミットのあとは、コードが本当に動くかを必ず確認してください。文法としては正しくても、意図しない組み合わせになっていることがあります。

やっぱりやめたいときは git merge –abort

開いてみたら想像以上に量が多かった、自分が判断できる範囲を超えていた。そんなときは、無理に進めずいったんマージを取り消せます。

ターミナル
# マージを中止して、マージを始める前の状態に戻す
git merge --abort

マーカーが書き込まれたファイルも元に戻り、git merge を打つ前の状態になります。相手のブランチの担当者に確認してから出直す、という判断ができるので、行き詰まったら遠慮なく使ってください。何度でもやり直せるので、状態を複雑にしてしまう前に戻るほうが結果的に速く終わります。

なお、コンフリクトは git merge だけでなく git rebase や git cherry-pick、git pull(内部でマージが走るため)でも発生します。rebase の途中で起きた場合は、使うコマンドが変わります。

ターミナル
# rebase をまるごと中止して、始める前に戻す
git rebase --abort

# コンフリクトを解消して git add したあと、次のコミットへ進む
git rebase --continue

rebase はコミットを1つずつ適用し直す操作なので、コミットの数だけコンフリクトが起きる可能性があります。解消して git add したら git rebase --continue、また止まったら同じことを繰り返します。マージと違い、自分で git commit する必要はありません。

解消を助けてくれるコマンド

手で書き直すのが基本ですが、状況によっては次のコマンドが役に立ちます。

コマンド用途
git diffコンフリクト中に打つと、ぶつかっている箇所を両側の内容付きで表示する
git checkout --ours -- <ファイル>そのファイルを、自分側の内容で丸ごと置き換える
git checkout --theirs -- <ファイル>そのファイルを、相手側の内容で丸ごと置き換える
git mergetool設定した差分ツールを起動し、画面上で見比べながら解消する
git merge --abortマージを中止して、始める前の状態に戻す

--ours と --theirs は、ビルド生成物やロックファイルのように「中身を混ぜる意味がなく、どちらかを丸ごと採用すればよい」ファイルで便利です。指定したあとも git add は必要です。

ここで必ず知っておきたい落とし穴があります。rebase の途中では、--ours と --theirs の意味が普段の感覚と入れ替わります。rebase はコミットを土台のブランチの上に載せ直す操作なので、Git から見た「自分側(ours)」は土台になっているブランチ、「相手側(theirs)」はいま適用し直している自分のコミットになります。「自分の書いた変更を残したい」つもりで --ours を指定すると、逆に土台側の内容になってしまうのです。

同じことはマーカーにも当てはまり、rebase 中は HEAD 側(上)が土台のブランチ、下側が自分のコミットです。思い込みで打たずに git diff で中身を確認してから決めるのが安全です。

git mergetool は、あらかじめ差分ツールを設定しておくと使えるコマンドです。VS Code などのエディタにもコンフリクト解消の画面があり、「現在の変更を取り込む」=HEAD 側、「入力側の変更を取り込む」=取り込もうとしている側として選べます。行数が多いときは、こうした画面のほうが見落としが減ります。

コンフリクトを減らすための習慣

コンフリクトはゼロにはできませんが、起きにくくすることはできます。もっとも効果が大きいのは、こまめに main(共有ブランチ)を取り込むことです。作業ブランチを切ったまま何日も放置すると、その間に main は進み続け、最後にまとめて合流させたときに大量の衝突が発生します。1日1回でも main を取り込んでおけば、ぶつかる量は小さく保たれ、記憶も新しいので判断が簡単です。

次に、1つのプルリクエストを小さく保つことです。変更するファイルが少なく、レビューから取り込みまでが短いほど、他の人の変更と重なる確率は下がります。関係ない修正を混ぜないだけでも衝突の面積は減ります。

意外と見落とされがちなのがフォーマッタや改行コードの設定を揃えることです。インデントがタブかスペースか、保存時に自動整形が走るかどうかがメンバー間で違うと、本来関係のない行まで書き換わり、中身は同じなのに衝突する事態が起こります。Prettier や EditorConfig の設定ファイルをリポジトリで共有し、改行コードも .gitattributes で統一しておきましょう。

解消したのにコミットできないとき

マーカーを消し忘れている

コミットはできたのにコードが動かない、という場合はまずこれを疑ってください。======= や >>>>>>> の行が1つでも残っていると構文として壊れます。コミット前なら git diff --cached でステージした内容を眺めて、記号が混じっていないか確認できます。

git add を忘れている(Unmerged paths が消えない)

ファイルを保存しただけでは、Git は解決したと認識しません。この状態で git commit を打つと拒否されます。

ターミナル
$ git commit
U	style.css
error: Committing is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: Exiting because of an unresolved conflict.

先頭の U は unmerged、つまり未解決という印です。git add <ファイル名> を実行してから、もう一度 git commit してください。エディタの解消 UI で直した場合も、保存しただけでステージされていないことがあります。

片方が削除、片方が変更しているファイル

「一方のブランチでファイルを削除し、もう一方で同じファイルを編集していた」というパターンもあります。この場合、ファイルの中にマーカーは書き込まれず、git status に次のような表示が出ます。

ターミナル
Unmerged paths:
  (use "git add/rm <file>..." as appropriate to mark resolution)
	deleted by them: old-header.php

deleted by them は「相手側のブランチで削除された」、deleted by us は「自分側で削除された」という意味です。決めるのはファイルを残すか消すかの二択で、残すなら git add old-header.php、削除に合わせるなら git rm old-header.php を実行すれば解決済みとして扱われます。

まとめ

コンフリクトは、同じファイルの同じ行を2つのブランチで別々に変更したときに、Git が自動で選べず人間に判断を委ねている状態です。発生したらまず git status を見て、Unmerged paths: に並ぶ both modified のファイルを確認します。ファイルを開くと <<<<<<< HEAD から ======= までが取り込み先の内容、そこから >>>>>>> ブランチ名 までが取り込もうとしている内容です。正しい形に書き直してマーカーの3行を消し、git add で解決済みにして git commit すれば、メッセージは自動で用意されます。行き詰まったら git merge --abort、rebase 中なら --abort と --continue を使い分けてください。--ours と --theirs が rebase 中は入れ替わる点にだけ注意しましょう。

参考ページ