「まとめて1本の PR」が生んだコンフリクト
チーム開発をしていると、必ずこの生き物に出会います。
CONFLICT (content): Merge conflict in src/...
原因を探ってみると、実は「隣の人」のせいではなく、自分の PR が大きすぎて、レビューに時間がかかりすぎたことが原因、というケースは意外とよくあります。
ありがちなパターンはこうです。
- 検索 API を作った(機能 A)
- ついでに、A の上に乗るフィルタ・ソート機能(機能 B)も同じ PR に詰め込んだ
- 「A と B はセットだから」と 1 本の巨大 PR として出す
- レビューに1週間かかっている間に main が先へ進む
- いざマージしようとしたらコンフリクトの山
「機能 B は機能 A の上に乗るんだから、A がマージされるまで待つか、まとめて 1 本の PR にするしかない」——多くの人がこの二択で悩み、結局まとめてしまいます。
実はここに第三の道があります。それが Stacked PR です。
先に断っておくと、この記事が扱うのは 「自分(または自分のチーム)が把握している依存関係のある変更」をどう分割するか という問題です。
見ず知らずの誰かが、示し合わせもなく同じファイルを同時に触っていた、という類の事故は Stacked PR では防げません。この違いは後の章で改めて整理します。
なぜ「依存関係のある機能追加」は巨大 PR になりがちか
シンプルな悪循環です。
- 機能 B が機能 A に依存しているので、A と B をまとめて 1 本の PR にする(変更ファイル数、うん十個)
- レビュアーが「うっ」となって後回しにする
- レビュー待ちの間に main が先へ進む
- 差分がどんどん本流から乖離していく
- いざレビューが終わって取り込む頃には、あちこちでコンフリクト
- 疲弊したレビュアーは、次から大きい PR を見るたびにさらに後回しにする(1 に戻る)
PR を小さくすればレビューは速くなる、というのは誰でも知っています。問題は「小さくすると、後続の変更が前の変更に依存してしまう」ことです。機能 A の上に機能 B を作りたいのに、A がまだ main に入っていない。しかたなく A と B をまとめて 1 本の巨大 PR にする——多くのチームがこのジレンマで巨大 PR に逆戻りします。
この「未マージの PR の上に、次の PR を積む」を正面から扱う技術が Stacked PR です。
Stacked PR という考え方
考え方自体はシンプルです。
main
└─ PR#1 (feat/A) ← まず A だけレビューしてもらう
└─ PR#2 (feat/A-B) ← A の上に B を積む。B の diff は B の分だけ
PR#2 の base ブランチを main ではなく feat/A にするだけです。レビュアーは PR#2 を開いても、A の変更には煩わされず、B の差分だけ を見られます。
1 本 1000 行の PR を出すのではなく、200 行の PR を 5 本、順番にレビューしてもらう。1 本あたりのレビューコストが激減するので、待ち時間も短くなり、結果としてコンフリクトの発生確率そのものが下がります。
「それ、機能ブランチを枝分かれさせてるだけじゃない?」というのはその通りです。
Stacked PR は魔法ではなく、GitHub の base ブランチ機能を、mainだけでなく他の feature ブランチに対しても律儀に使う、ただそれだけの話です。ただ、これが地味に強い。
Stacked PR が解決すること、しないこと
ここではっきりさせておきます。Stacked PR が効くのは、依存関係がある変更を、自分(またはチーム内で調整済みのメンバー)があらかじめ把握している場合だけです。
- ✅ 解決できること: 「機能 B は機能 A の上に乗る」と最初からわかっている場合に、1本の巨大 PR を作らず、A・B を別々の小さい PR として順番にレビューしてもらえる。レビューが速く終わる分、main との乖離、つまり後で起きるコンフリクトの芽も小さく抑えられる
- ❌ 解決できないこと: 自分がスタックを組んでいるかどうかに関係なく、見ず知らずの誰かが、示し合わせもなく同じファイルを同時に触っていたというパターン。相手のブランチの存在を知らなければ、そもそも「その上に積む」という選択肢自体が生まれません。これは trunk-based development で変更を細かく速くマージする、変更のスコープをファイル単位まで絞る、コードオーナーシップを分ける、着手前に一声かける、といった別の施策で対処すべき領域です
- ❌ 解決できないこと(その2): PR#1(土台)がレビューで指摘を食らって差分が育つと、PR#2(積み上げ側)はそのたびに
rebaseで追従する必要があり、そこでコンフリクトが起きることもあります。Stacked PR は「スタック内部の依存関係に起因するコンフリクト」まで消してくれるわけではありません
つまり Stacked PR は「コンフリクト全般の特効薬」ではなく、「自分で作ってしまいがちな、依存関係のある巨大 PR」というかなり具体的な原因ひとつに効く薬、というのが正確な理解です。逆に言えば、この原因に心当たりがあるなら、今日からでも効きます。
実際にやってみた
理屈だけだと味気ないので、よくある EC サイトの機能追加を例にシミュレーションしてみます。
「検索機能」を強化するタスクが2つありました。
- PR #1: 検索 API そのものを新設(
feat/search-api)。商品名でキーワード検索できるようにする、土台となる変更 - PR #2: 検索結果に「価格順」「新着順」などのフィルタ・ソートを追加(
feat/search-filters)。当然、PR #1 の API がないと実装できない
PR #2 は PR #1 のコードに乗っかる形だったので、PR #1 がまだ未マージの段階で、そのブランチの上に feat/search-filters を積んで作業を始めました。
# 前提: feat/search-api は push 済み、PR#1 として既にレビュー依頼中(未マージ)
git checkout feat/search-api # ローカルに feat/search-api を持ってくる
git checkout -b feat/search-filters
# ここで実装・コミット
git push -u origin feat/search-filters
gh pr create --base feat/search-api # base を main ではなく PR#1 のブランチに
これで PR 画面には「フィルタ・ソート機能の差分だけ」が表示されます。PR #1 の検索 API 本体(1000 行近い変更)に埋もれることなく、レビュアーは新規追加分だけを見ればいい状態。
しばらくして PR #1 がマージされました。ここで面白いことが起きます。
自分では何もしていないのに、PR#2 の base 表示が feat/search-api から自動的に main へ切り替わっていました。
これは GitHub の Pull Request Retargeting という機能です。
base ブランチ(feat/search-api)がマージされて削除されると、それを base にしていた PR の base を自動的に付け替えてくれます。
地味ですが 2020 年からある機能で、これのおかげで「PR が、もう存在しないブランチを指したまま迷子になる」事故を避けられます。
squash マージや rebase マージの場合は、履歴が変わるため rebase --onto が必要です(merge commit なら不要です)。
体感として、1000 行級の PR を 1 本レビューしてもらうのを待つより、200 行の PR を「これ単体でレビューお願いします」と出す方が圧倒的にレビューが早く返ってきますし、待っている間に事故る確率もぐっと下がります。
GitHub 側もこの問題に本気を出し始めた
面白いのは、ここ数年で GitHub 自身がこの「PR を積む」ワークフローに正面から投資し始めていることです。
- 2020 年 5 月: Pull Request Retargeting — base ブランチがマージ・削除されたとき、依存する PR の base を自動的に付け替える機能
-
2026 年 4 月: GitHub Stacked PRs —
gh stackという専用 CLI と、PR 画面にスタック全体を見せる UI(スタックマップ、レビュー時の階層ナビゲーション)がプライベートプレビューとして登場。公式ドキュメントによれば、スタック内の各 PR は直接の base ではなくスタックの最終着地点(通常 main)を対象に branch protection や required status checks が評価される。スタック上位の PR をマージするには、下位の PR も含めてすべて要件を満たしている必要があるとのこと
まとめ
- 「機能 B は機能 A に依存する」というよくある状況が、まとめて 1 本の巨大 PR を生み、それがレビュー遅延とコンフリクトの元になる
- Stacked PR は、未マージの PR の上に次の PR を積むことで、依存関係を保ったままレビュー単位を小さく保てる
- ただし効くのは「自分が把握している依存関係」に対してだけ。見ず知らずの誰かとの偶発的な衝突や、土台 PR がレビューで育ってしまうケースまでは解決しない
- 専用ツールを待たなくても、
gh pr create --base <未マージのブランチ>を使うだけで今日から始められる
「機能追加のたびに気づいたら巨大 PR になっている」という心当たりがあるなら、次はひとつだけ、土台の PR の上にもうひとつ PR を積んでみてください。