shibomb

Gitコンフリクトの衝突を安全に解消!rebaseとmergeの使い分けと復元

チーム開発でブランチを統合したら、突然 CONFLICT (content): Merge conflict in ... と表示されて手が止まった。どちらのコードを残せばよいか分からず、壊してしまいそうで怖い。そんな経験はありませんか。この記事では、コンフリクトが起きる仕組み、マーカーの読み方と解消の3ステップ、merge と rebase の違いと使い分け、失敗したときのやり直し方、そして衝突を減らすチーム運用までを順に解説します。読み終えるころには、落ち着いて対処できる手順が手元に残ります。

なぜコンフリクトは起きるのか?衝突が発生するメカニズムを理解する

コンフリクトは、Git が「どちらを採用すべきか自分では決められない」ときに、人間へ判断を求めるための仕組みです。エラーではなく確認依頼だと捉えると、気持ちが少し楽になります。

Git はブランチを統合するとき、2つのブランチの共通の祖先と、それぞれの先端を比べる3-way マージを行います。次の図で、A が共通の祖先です。

      C---D   main
     /
A---B
     \
      E---F   feature

B から先で、main 側と feature 側が同じファイルの同じ行付近を別々に書き換えていると、Git は自動で統合できません。ここで初めてコンフリクトが発生します。

逆にいえば、別のファイルを触った場合や、同じファイルでも離れた場所を触った場合は自動で統合されます。コンフリクトは特別な失敗ではなく、チームで開発すれば普通に起きる現象です。

注意点があります。自動でマージできたからといって、動作が正しいとは限りません。たとえば一方が関数名を変え、もう一方が旧名の関数を呼ぶコードを追加していた場合、Git は何も言わずに統合しますが、実行時にエラーになります。統合後にテストを走らせる習慣が大切です。

慌てずに差分を読む!コンフリクトマーカーの見方と解消の3ステップ

コンフリクトが起きたファイルには、Git が次のようなマーカーを書き込みます。

<<<<<<< HEAD
const price = basePrice * 1.10;
=======
const price = calcPrice(basePrice, taxRate);
>>>>>>> feature/tax-rate

<<<<<<< から ======= までが現在のブランチ (HEAD) の内容で、======= から >>>>>>> までが取り込もうとしているブランチの内容です。「どちらが正しいか」ではなく、「それぞれの変更は何を目的にしているか」を読み取ってください。上の例なら、一方は税率を直書きし、もう一方は税率を引数で渡す関数に置き換えています。後者の意図を活かす形が最終形になりそうです。

解消の手順は次の3ステップです。

  1. git status で、衝突しているファイル (both modified と表示されるもの) を確認します。
  2. ファイルを開き、両方の意図を踏まえた最終形に書き換えます。<<<<<<< ======= >>>>>>> の3種類の行は必ずすべて削除してください。
  3. git add で解消済みと伝え、merge なら git commit、rebase なら git rebase --continue で進めます。

最後に、ビルドとテストを実行して動作を確認しましょう。判断に迷うときは、相手の変更を書いた人に意図を聞くのが最も確実です。自己判断で片方を消すと、他の人の修正を黙って消してしまう事故につながります。

差分が読みにくいときは、git config --global merge.conflictstyle zdiff3 を設定しておくと便利です (Git 2.35 以降で使えます)。マーカーに共通祖先の内容も表示されるため、「元はどうだったか」を見ながら判断できます。VS Code などのエディタが備える、Accept Current や Accept Both といったボタンも便利ですが、最終的な中身は自分の目で確認してください。

「merge」と「rebase」は何が違う?コミット履歴と現場での使い分け基準

Git の rebase と merge の違いは、一言でいえば履歴の残し方です。上の図の状態で feature を統合する場合、結果は次のように変わります。

merge の結果:
A---B---C---D-----M   main (Mは統合コミット)
     \         /
      E-------F       feature

rebase の結果 (feature を main の先端に載せ替え):
A---B---C---D---E'---F'   feature

merge は両方の履歴をそのまま残し、統合コミットを1つ作ります。いつ何を統合したかが分かる反面、統合コミットが増えると履歴が枝分かれして読みにくくなります。rebase は自分のコミットを最新の main の先に付け替えるため、履歴が一直線になって読みやすくなります。ただし、コミットは新しく作り直され、ハッシュ値が変わります。

rebase ではコンフリクトがコミットごとに起きる点にも注意してください。3つのコミットを載せ替えると、最大3回解消を求められる可能性があります。また、rebase 中の ours と theirs は、merge のときと意味が逆になります。公式ドキュメントにも記載がある仕様で、混乱しやすいので、慣れないうちはエディタで内容を読んで直接編集するほうが安全です。

現場での使い分けは、チームの方針によって異なります。よくある基準は次のとおりです。

  1. まだ自分しか使っていないブランチを最新の main に追従させるなら、rebase が選択肢になります。
  2. すでに push して他の人も触っているブランチでは、rebase を避け、merge を使います。履歴を書き換えると、他の人の手元とずれて新たなコンフリクトを生むためです。これは Pro Git でも「共有したコミットは rebase しない」という指針として紹介されています。
  3. main への取り込みは、プルリクエストの merge 方式 (merge commit や squash など) をチームで統一します。

rebase 後に push するときは、git push --force-with-lease を使います。単純な --force と違い、リモートに他の人の新しいコミットがあれば拒否してくれるため、上書き事故を防ぎやすくなります。

作業を途中でやり直したいときの安全策:abortとreflogの活用法

コンフリクト解消の途中で「訳が分からなくなった」と感じたら、いったん始める前の状態に戻せます。

git merge --abort
git rebase --abort

実行中の操作に合わせて、どちらか一方を使います。コンフリクト解消の途中で編集した内容は破棄されますが、操作を始める前の状態にきれいに戻るため、何度でもやり直せます。これを知っているだけで、試行錯誤への恐怖はかなり減ります。

すでにコミットまで進めてしまい、「やはり元に戻したい」場合は git reflog が役立ちます。HEAD がどこを指してきたかの履歴をローカルに記録しており、rebase などで見えなくなったコミットも辿れます。

git reflog
git reset --hard HEAD@{2}

reflog で戻りたい時点を探し、reset --hard で移動します。ただし --hard はコミットしていない変更を消すので、実行前に git status で確認してください。reflog はあくまで自分のローカルの記録で、期限が来ると古い項目は整理されます。

もっと手軽な保険は、危ない操作の前にバックアップ用のブランチを作っておくことです。git branch backup/feature-before-rebase と実行するだけで、元の状態への目印が残ります。初心者のトラブルの多くは、この一手間で防げます。

コンフリクトを最小限に抑えるチーム運用のベストプラクティス

コンフリクトは解消が上手になるより、そもそも起きにくくするほうが効果的です。ブランチ運用の工夫で、発生率は大きく下げられます。

最も効くのは、ブランチを短命にして、変更を小さく保つことです。数週間も main から離れたブランチは、ずれが蓄積して大きな衝突になりがちです。1つのブランチで1つの目的だけを扱い、数日以内に取り込むのを目安にしましょう。作業中も、こまめに main の最新を取り込んでおくと、衝突が小さいうちに片付きます。

次に、コードの整形ルールを統一します。フォーマッタ (Prettier や Black など) とリンターを共有し、保存時やコミット前に自動実行すれば、インデントや改行の違いによる無意味な衝突が減ります。

3つ目は、担当範囲の共有です。同じファイルを複数人が同時に大きく変更しそうなときは、事前に声をかけます。ファイルの責務を小さく分割しておくことも、長期的な保守性の面で有効です。

4つ目は、生成ファイルの扱いです。ロックファイル (package-lock.json など) は手で直さず、衝突したら片方を採用して、パッケージマネージャーのコマンドで再生成するのが一般的です。

同じ衝突を繰り返し解消する場面が多いなら、git config --global rerere.enabled true で rerere 機能を有効にできます。一度行った解消内容を Git が記録し、同じ衝突に再度遭遇したときに自動で適用してくれます。ただし自動適用された結果も、内容は必ず確認してください。

チームで決めておきたいのは、merge と rebase のどちらを使うか、プルリクエストの取り込み方式、ブランチの命名規則です。ルールを文書にまとめておけば、新しいメンバーも迷わず参加できます。

関連記事