GitHub PR が「コンフリクトあり」と言うのにローカルではマージできる
TL;DR
- GitHub の Pull Request が “This branch has conflicts” と表示するのに、ローカルで
git mergeすると何事もなく成功する、ということがあります。 - 原因は criss-cross merge(交差マージ)で
merge-baseが複数存在する状態です。 - ローカルの git(
ort/recursiveストラテジー)は複数の merge-base を仮想ベースに統合して賢く解決しますが、GitHub のマージエンジンは同じ状況をうまく処理できないことがあります。 - 解決策は
git rebaseで履歴を線形化することです。
状況
スタックド PR(PR-A の上に PR-B を積む運用)をしていて、次のようなことが起きました。
feature-aブランチからfeature-bブランチを切って作業- 途中で別ブランチ
feature-cの変更もfeature-bにマージ(feature-cの修正がfeature-bの作業に必要だった) feature-aとfeature-cがそれぞれ main にマージされたfeature-bの PR の base が main に切り替わった
この時点で GitHub が “This branch has conflicts that must be resolved” と表示します。ところが手元で試すと:
|
|
問題なくマージできてしまいます。
再現手順
以下のスクリプトでこの状態を手元で再現できます。
|
|
ここまで実行すると、次のようなコミットグラフになります。
|
|
feature-b と main の merge-base を調べると、2つ返ってきます。
|
|
この2つは互いに祖先関係にありません。これが criss-cross merge です。
|
|
そしてローカルでのマージは成功します。
|
|
この状態で GitHub に push して PR を作ると、GitHub 側では “This branch has conflicts” と表示されうるわけです。
原因: criss-cross merge とは
通常の 3-way merge では、2つのブランチの共通祖先(merge-base)が 1つに定まります。しかし上のケースでは、feature-b に feature-a と feature-c が それぞれ独立に取り込まれ、main にも それぞれ独立にマージされている ため、最近共通祖先が2つ存在します。
main
│
┌────┴────┐
│ │
feature-a feature-c ← 両方とも main と feature-b の両方に入っている
│ │
└────┬────┘
│
feature-b
このように、DAG 上で同等な merge-base が複数存在する状態を criss-cross merge と呼びます。
ローカル git と GitHub で結果が異なる理由
ローカルの git(ort / recursive ストラテジー)
git のデフォルトマージストラテジーは、merge-base が複数ある場合に 仮想的なマージベースを生成 します。複数の merge-base 同士を再帰的にマージして1つの仮想コミットを作り、それを base として 3-way merge を行います。この「再帰マージ」の賢さによって、criss-cross でも多くのケースで自動解決できます。
GitHub のマージエンジン
GitHub がサーバー側でどのマージアルゴリズムを使っているかは公開されていません。しかし実際の挙動として、criss-cross merge の状況で「ローカルでは解決できるのに GitHub では conflict と判定される」ケースが存在します。
解決策: rebase で線形化する
feature-b を main の上に rebase すると、criss-cross 構造が解消されて merge-base が一意になります。
|
|
rebase 中にコンフリクトが出る場合がありますが、これは git が正直に示してくれるコンフリクトなので、通常どおり解決して git rebase --continue すれば OK です。
rebase 後は merge-base が1つになっていることを確認できます。
|
|
rebase 後に force push すれば、GitHub のコンフリクト表示も解消されます。
まとめ
- スタックド PR + 複数ブランチの交差取り込み で criss-cross merge が発生しやすいです。
- ローカルの git は merge-base が複数あっても仮想ベースで解決できますが、GitHub のマージ判定は同じ状況に対応できないことがあります。
- 「ローカルでマージできるのに GitHub が conflict と言う」場合は、
git merge-base --allを確認してみてください。2つ以上返ってきたら criss-cross merge です。 git rebaseで履歴を線形化すれば解決します。