コンフリクトの解決
コンフリクトとは、Gitが同じ箇所に対する2つの変更を検出し、推測を拒否した状態です。解決とは意図に関する判断であり、安全なデフォルトは両方の意図を保持することです。片方を削除して解決すると、マーカーは消えますが問題は悪化します。ビルドは依然として成功し、同僚の変更が失われます。
1. 現在の操作を把握する - ours と theirs は操作に依存する
git status # "rebase in progress" / "You have unmerged paths" / "cherry-pick in progress"
| 操作 | --ours / HEAD 側 | --theirs 側 |
|---|---|---|
merge | 自分がいるブランチ | マージされるブランチ |
rebase | リベース先のブランチ (上流) | リプレイ中の自分のコミット |
cherry-pick、revert、stash pop | 自分がいるブランチ | 適用されるコミット |
リベースの行が落とし穴です。git rebase main 中は、「ours」が main で、「theirs」がユーザー自身の作業であり、マージとは逆になります。git checkout --theirs file をリベース中に実行すると、自分の変更が保持され、上流の変更が破棄されます。いずれかのフラグを使用する前に、この表をもう一度読んでください。
2. マーカーだけでなくコンフリクト全体を確認する
git diff --name-only --diff-filter=U # マージされていないファイル
git status # タイプ: both modified / deleted by us / both added
git config merge.conflictStyle zdiff3 # 共通祖先も表示する (git 2.35+; 古いバージョンでは diff3 を使用)
git checkout --conflict=zdiff3 -- <file> # ベースを表示して1つのファイルを再レンダリングする (そのファイルに既に行った編集は破棄される)
ベースが見えると、マーカーは3つの部分(あなたの側、オリジナル、相手の側)で構成されます。ほとんどの一見したコンフリクトは、各側が何から変更したかがわかれば明白になります。
各側がなぜ変更したかを理解するには:
git log --merge -p -- <file> # 両側からのファイルに触れるコミット (マージまたはリベース進行中)
git show REBASE_HEAD # リベース中: リプレイ中のコミット
git show :1:<file> :2:<file> :3:<file> # ベース、"ours"、"theirs" をファイル全体として
コミットメッセージを読んでください。側面ごとに一文で説明できない解決策は、推測です。
3. 組み合わせて解決する
- 両側が同じ行を異なる理由で編集した場合 - 両方を行うバージョンを書きます。例えば、一方が
tax_rateパラメータを追加し、他方がdiscountパラメータを追加した場合、解決後の関数は両方を受け取り、両方を適用します。 - 一方が移動または名前変更し、他方が編集した場合 - 新しい場所に編集を適用します。古いファイルと新しいファイルの両方を維持しないでください。
- 一方が削除し、他方が変更した場合 - これは機械的な判断ではなく、真の決断です。なぜ削除されたのかを調べてください。変更がまだ重要であれば、それを引き継ぐ必要があります。そうでなければ、削除が優先されます。不明な場合はユーザーに確認してください。
- 生成ファイルとロックファイル (
package-lock.json、poetry.lock、go.sum、スナップショット、コンパイル出力) - 手動でマージしないでください。いずれかの側を採用した後、ツールで再生成します (npm install、poetry lock、go mod tidy、スナップショットコマンド)。 - バイナリファイル - マージは不可能です。意図的に一方を選択し(上の表に注意)、その旨を伝えてください。
テキストはきれいにマージできても、間違っていることがあります。一方が関数名を変更し、他方が古い名前への呼び出しを追加した場合、Gitはコンフリクトを検出しません。だからこそ、ステップ4でビルドを行うのです。
一括フラグは判断を隠します。 git checkout --ours .、-X ours / -X theirs (コンフリクトのあるハンクをすべて一方に解決する)、および -s ours (もう一方のブランチを完全に破棄する) は、それぞれ内容を確認せずに作業を捨てます。ユーザーがまさにそれを求め、何が破棄されるかを理解している場合にのみ使用してください。
4. 検証してから続行する
git diff --check # 残存するコンフリクトマーカーと空白エラーがあると失敗する
grep -rnE '^(<<<<<<<|=======|>>>>>>>)' . # 念のため、git diff-check がチェックしないファイルも対象
# ビルドして最速の関連テストを実行
git add <resolved-files>
該当するコマンドで続行します。エディタが開く場合は GIT_EDITOR=true を追加します。非対話型シェルではエディタがハングするため:
git merge --continue # または: git commit --no-edit
GIT_EDITOR=true git rebase --continue
GIT_EDITOR=true git cherry-pick --continue
リベースはコミットを1つずつリプレイするため、次のコミットで再び停止する可能性があります。このループを繰り返します。コンフリクトが現れた最初のコミットで解決してください。後のコミットは同じコンフリクトの繰り返しである可能性があります。
解決後にテストが失敗した場合、その失敗は解決に関する情報です。テストを編集して「修正」したり、そのまま続行したりせず、どの側の動作がテストで保護されているかを把握するまでは行わないでください。
5. いつ止めて質問するか
以下の場合は、推測するのではなく止めてください:
- 両側が互換性のない方法でビジネスロジックを変更した場合(異なる税金ルール、異なるリトライポリシーなど);
- 各側がなぜ変更を行ったかを一文で説明できない場合;
- 解決によってテストが失敗し、原因が不明な場合。
両方の意図を述べ、何を選びその理由を説明し、ユーザーに判断を委ねてください。--continue より前のすべての操作は元に戻せます: git merge --abort、git rebase --abort、git cherry-pick --abort はツリーを開始時の状態に戻します。
繰り返し発生するコンフリクト
長いリベースで同じコンフリクトが繰り返す場合、またはブランチが繰り返しマージされて放棄される場合:
git config rerere.enabled true # 各解決を記録し、次回に再適用する
git rerere diff # 記録された内容
git rerere forget-path <file> # 誤った記録された解決を破棄する
rerere は解決を確認なしで再適用するため、誤った解決が伝播します。rerere で解決されたコンフリクトの後も、結果を確認してください。
ブランチが長期間存続し、大きく遅れている場合、トランクを1回マージすると通常1回のコンフリクトラウンドで済みますが、リベースはコミットごとにコンフリクトを再生します。その選択はユーザーに委ねられます。
ガードレール
- コンフリクトをなくすために、片方を全面的に採用して解決しないでください。 反対側は誰かの作業です。
- ユーザーが完了を依頼したリベースを、面倒だからといって中止しないでください。 問題が発生している場合にのみ中止し、その旨を伝えてください。
- 依頼がない限り、解決後にプッシュしないでください。 リベースには強制プッシュが必要です。
git-safe-history-rewriteに従ってください。 - コンフリクトした各ファイルについて報告してください: 両側が何をしたか、何を保持したか、検証できなかったこと。