はじめに
feature ブランチを最新の main に追従させようと git rebase origin/main を実行したら、こんな出力が出て止まった経験はないでしょうか。
Auto-merging src/pages/Settings.tsx
CONFLICT (content): Merge conflict in src/pages/Settings.tsx
error: could not apply 1a2b3c4... feature: プロフィール編集機能を追加
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
私も先日これに遭遇し、error: could not apply という文言から「rebase が失敗した」と思い込みました。しかし実際には、これは失敗ではありません。この記事では、rebase の衝突がなぜ起きるかと、止まった状態から完了(または撤退)までのコマンドベースの手順を整理します。
結論
- 衝突による停止は「自動マージできない箇所を人間に選ばせるための一時停止」であり、rebase の正常な動作
- 対処は 「
git statusで確認 → マーカーを解消 →git add→git rebase --continue」の繰り返し - 迷ったら
git rebase --abortでいつでも rebase 前の状態に無傷で戻れる
なぜ止まるのか
rebase は、feature ブランチの各コミットを main の先端の上に1つずつ適用し直す操作です。main 側の別の変更(たとえば他の人の PR)と自分のコミットが同じファイルの同じ行域を変更していると、git はどちらを採るべきか機械的に判断できず、そのコミットの適用を保留して停止します。
merge が衝突を1回でまとめて解消するのに対し、rebase はコミット単位で適用するため、衝突もコミットごとに複数回発生しうる点が特徴です。
対処フロー
Step 1: 状態を確認する
git status
interactive rebase in progress; onto 9f8e7d6
You are currently rebasing branch 'feature/xxx' on '9f8e7d6'.
(fix conflicts and then run "git rebase --continue")
Unmerged paths:
both modified: src/pages/Settings.tsx
both modified が衝突しているファイルです。エラーメッセージの見た目に惑わされず、まず git status で「rebase 進行中・解消待ち」という事実を確認するのが出発点です。
どのコミット同士がぶつかったのかを知りたければ、衝突に関与した両側の変更を表示できます。
git log --merge --oneline -- src/pages/Settings.tsx
Step 2: 衝突マーカーを解消する
衝突ファイルを開くと、次のマーカーが入っています。
<<<<<<< HEAD
(main 側=載せ替え先の内容)
=======
(自分のコミットの内容)
>>>>>>> 1a2b3c4 (feature: プロフィール編集機能を追加)
両方の変更が必要なら手で統合し、マーカー行(<<<<<<< ======= >>>>>>>)を残さず削除します。
片側をまるごと採用する場合はコマンドでも解消できますが、ここに rebase 最大の罠があります。
git checkout --ours src/pages/Settings.tsx # main 側(載せ替え先)を採用
git checkout --theirs src/pages/Settings.tsx # 自分のコミットの変更を採用
rebase 中は HEAD が main 側に付け替わっているため、--ours が main 側、--theirs が自分の変更を指します。merge のときと逆なので、機械的に打つと意図と反対の側を採用してしまいます。
Step 3: 解消を宣言して続行する
git add src/pages/Settings.tsx
git rebase --continue
git add が「このファイルの衝突は解消済み」という宣言になります。--continue するとコミットメッセージの確認を経て次のコミットの適用に進み、残りがすべて衝突なく適用されれば rebase 完了です。途中でまた衝突すれば、Step 1 から繰り返します。
3つの出口を使い分ける
| コマンド | 動作 | 使いどころ |
|---|---|---|
git rebase --continue |
解消済みとして次へ進む | 基本はこれ |
git rebase --skip |
今のコミットを捨てて次へ進む | 同じ変更が main に取り込み済みのとき |
git rebase --abort |
rebase 前の状態へ完全に巻き戻す | 解消が手に負えないとき・やり直したいとき |
--abort は作業ツリーもブランチも rebase 開始前に戻すので、失うものはありません。「いつでも撤退できる」と知っておくだけで、rebase の心理的ハードルは大きく下がります。
なお --skip はコミットそのものを履歴から落とすため、「同じ内容が main に取り込み済み」と確認できたときに限って使うのが安全です。
衝突対応を楽にする設定
zdiff3: 共通祖先も表示する
git config --global merge.conflictStyle zdiff3
衝突マーカーに「分岐前はどうだったか」(共通祖先)のブロックが追加され、どちらが何を変えたのかを読み取れるようになります。
<<<<<<< HEAD
(main 側の内容)
||||||| parent of 1a2b3c4
(分岐前の元の内容)
=======
(自分のコミットの内容)
>>>>>>> 1a2b3c4
rerere: 同じ衝突の解消を再利用する
git config --global rerere.enabled true
一度解消した衝突のパターンを記録し、同じ衝突に再び出会ったとき自動で再適用します。長生きする feature ブランチを何度も rebase する運用では特に効きます。ただし過去の解消が誤っていた場合もそのまま再適用されるので、自動解消された箇所は git diff --cached で確認してから続行するのが無難です。
まとめ
-
CONFLICTとerror: could not applyは失敗ではなく解消待ちの一時停止 - 基本フローは
git status→ マーカー解消 →git add→git rebase --continueの繰り返し - rebase 中の
--oursは main 側、--theirsは自分の変更(merge と逆) - 手に負えなければ
git rebase --abortで無傷で撤退できる
rebase と merge の使い分けそのものは、前回の記事「git rebase と git merge の違いを整理する」で扱っています。