12k
All articles

GitHubのスタック型プルリクエストの仕組み

GitHubのスタック型プルリクエストを解説。gh-stackでの作成、各層のレビュー、変更のリベース、依存するPRのマージ方法を紹介します。

OpenReplay Team
OpenReplay Team
GitHubのスタック型プルリクエストの仕組み

GitHubのスタック型プルリクエスト(stacked pull requests)は、1つの大きな変更を、順序付けられた小さなプルリクエストのチェーンに分割する機能です。各プルリクエストは自分の1つ下にあるプルリクエストのブランチをターゲットにし、GitHubはこのチェーンを1つの単位としてレビュー、リベース、マージします。

従来のやり方は多くのチームがご存じでしょう。互いに依存する3つのパートで機能を実装し、PRを3つ作成すると、2つ目のPRには自身の変更に加えて1つ目のPRの変更まで表示されます。結果としてレビュアーがレビューを諦めるか、すべてを1つの巨大なPRにまとめてしまい、誰もまともに読まなくなります。

本記事では、GitHubネイティブのスタックの仕組みを一通り解説します。スタックの作成方法、レビューの進め方、スタックの途中にあるPRが変更されたときの挙動、そして各マージオプションの動作を取り上げます。通常のPRについて復習したい場合は、先にターミナルからプルリクエストを作成する方法をご覧ください。

重要なポイント

  • GitHubのスタックでは、各プルリクエストのベースが1つ下のPRのブランチになるため、各diffにはそのレイヤーの変更だけが含まれます。
  • GitHubは2026年7月30日にスタック型プルリクエストをパブリックプレビューとして発表しました。この機能は今後変更される可能性があります。
  • GitHubのクイックスタートによると、gh-stack CLI拡張機能にはGitHub CLI 2.90.0以降とGit 2.20以降が必要です。
  • 中間のレイヤーが変更されると、その上にあるすべてのブランチは古いコミットに取り残されます。これらのブランチは下から順にリベースし、force-pushする必要があります。
  • マージ可能な状態にある最上位のPRをマージすると、そのPRと、その下にある未マージのPRすべてが1回のマージで取り込まれます。マージが途中で失敗した場合、すでに取り込まれたPRはマージされたままとなり、残りはオープンのままになります。すべてのPRは、スタックのベースブランチのルールに照らしてチェックされます。

依存関係のあるプルリクエストの問題点

依存関係のあるプルリクエストがレビューでうまくいかないのは、GitHubが各PRをそのベースブランチと比較するためです。たとえば、auth-middleware、api-routes、ui-loginの順に、すべてmainをターゲットとする3つのPRを作成したとします。2つ目のPRのdiffにはミドルウェアのコードもすべて含まれ、3つ目のPRにはすべての変更が含まれてしまいます。あるPRで実際にどの行が追加されたのか、誰にも判別できません。

ネイティブのスタック機能がない場合、開発者は通常2つの回避策のいずれかを取ります。1つは、すべてを1つの大きなPRにまとめる方法です。これではレビューが遅くなり、ミスも見落とされやすくなります。もう1つは、各PRのベースを手作業で1つ下のブランチに設定し、何か変更があるたびにチェーン全体を自分でリベースし、ターゲットを付け替える方法です。

GitHubのスタック型PRとは

GitHubのスタック型プルリクエストとは、順序付けられたチェーンを構成する1つ1つのリンクのことです。各PRは1つ下のPRのブランチをベースとして使用するため、そのdiffにはそのレイヤーで加えられた変更だけが表示されます。GitHubのスタック型プルリクエストの概要では、最下位のPRはスタックのトランクをターゲットにすると説明されています。トランクは通常mainですが、リリースブランチなど任意のブランチにすることもできます。スタック内のすべてのブランチは同じリポジトリに存在する必要があるため、フォークをまたいだスタックは作成できません。

先ほどの3レイヤーの例をスタックにすると、次のようになります。

main
 └─ auth-middleware   PR → main             (diff: middleware only)
     └─ api-routes     PR → auth-middleware  (diff: routes only)
         └─ ui-login    PR → api-routes       (diff: login UI only)

モデル自体は、Gitがすでに理解しているブランチとベースの仕組みのままです。GitHubはその上に追跡機能を追加しています。PRの順序を把握し、PR同士をリンクさせ、グループとしてまとめて操作します。

gh CLIでスタックを作成する

gh-stack拡張機能を使うと、ターミナルからスタックを作成・管理できます。GitHubのクイックスタートでは、前提条件としてGitHub CLI 2.90.0以降とGit 2.20以降が挙げられています。CLIコマンドリファレンスと拡張機能のREADMEでは最小要件がgh 2.0と低めに記載されているため、クイックスタートの値に従うほうが安全です。また、認証済みであること、そしてリポジトリへのプッシュ権限があることも必要です。ghを初めて使う場合は、ターミナルからプルリクエストを作成するから始めてください。

gh --version        # 2.90.0 or later
git --version       # 2.20 or later
gh auth login
gh extension install github/gh-stack

次に、1レイヤーずつスタックを構築します。

gh stack init                      # prompts for the first branch name, e.g. auth-middleware
git add -A
git commit -m "Auth middleware"

gh stack add api-routes
git add -A
git commit -m "API routes"

gh stack add ui-login
git add -A
git commit -m "Login UI"

gh stack push                      # pushes branches only
gh stack submit                    # opens PRs with correct bases and links them
gh stack view                      # branches, PR links, statuses, latest commits

gh stack initは、スタックのローカル追跡を設定し、最初のブランチを作成します。--baseを指定しない場合、トランクはリポジトリのデフォルトブランチになります。CLIコマンドリファレンスが指摘しているように、pushはブランチをアップロードするだけで、プルリクエストには一切手を加えません。PRを作成し、それぞれに正しいベースブランチを設定し、1つのスタックとして結び付けるのはsubmitコマンドです。

Web UIを使うこともできます。github.comでは、以前のPRのブランチをベースとするPRを作成し、Create stackを選択することでスタックを作成できます。既存のスタックを拡張するには、スタック内の任意のPRを開き、ヘッダーからAdd to stackを選択します。オープン中のPRがすでにチェーン状に並んでいる場合は、それらをスタックに変換するよう提案するバナーがGitHubに表示されます。スタックはGitHubモバイルアプリからも利用でき、gh-stackスキルを通じてCopilotなどのコーディングエージェントからも利用できます。GitHub Desktopは対応していません。

レイヤーごとにスタックをレビューする

レビュアーは、スタックの各レイヤーを個別に承認できます。作成者がさらに上にレイヤーを積み重ねている間にも、複数のレビュアーがそれぞれ異なるレイヤーを同時にレビューできます。スタック型プルリクエストの最大のメリットは、小さなdiffに対してレビューが行われること、そして機能全体の完成を待たずにレビューを始められることです。

スタック内のどのPRを開いても、表示されるのはそのレイヤーのdiffだけです。マージボックスにはスタックマップが表示され、チェーン内のすべてのPRとそのステータスが一覧できます。また、ページ上部のスタックアイコンに表示される番号で、現在どのレイヤーを開いているかがわかります。

レイヤーが小さいと、mainの履歴もより有用なものになります。取り込まれる各コミットの内容が絞り込まれているため、git bisectを実行した際に、1,000行に及ぶマージではなく特定の変更を問題箇所として特定できます。

スタックの途中にあるPRを更新する

スタックの途中にあるブランチに新しいコミットが追加されると、その上にあるすべてのブランチは古いコミットを指したままになります。上位のブランチは、更新されたレイヤーの上にリベースし、force-pushする必要があります。この例では、api-routesにレビュー指摘の修正を加えると、ui-loginが取り残されます。auth-middlewareには影響しません。

手作業で管理しているチェーンの場合は、素のGitを使ってこれを自分で行います(リベースの基本については10個のGitコマンドを参照してください)。

# plain Git, no gh-stack
git checkout ui-login
git rebase api-routes              # resolve any conflicts
git push --force-with-lease
# repeat for every branch above, bottom to top

gh-stackを使えば、コード変更をスタックするチュートリアルにあるように、この作業は2つのコマンドで済みます。

gh stack checkout api-routes
git add -A
git commit -m "Fix API routes"
gh stack rebase --upstack          # rebases only the branches above api-routes
gh stack push                      # per-branch --force-with-lease

リベースがコンフリクトで停止した場合は、コンフリクトしたファイルを修正してgit addし、gh stack rebase --continueを実行します。中止したい場合は、gh stack rebase --abortを実行すると、すべてのブランチがリベース前の状態に戻ります。

github.comでは、スタックが線形でなくなると、マージボックスにRebase stackボタンが表示されます。スタック型プルリクエストの管理ガイドによると、このボタンをクリックすると、最下位から順にブランチが最新のトランク上にリプレイされ、それぞれがforce-pushされます。この方法でGitHubのサーバー上で作成されたコミットには署名が付かないため、署名付きコミットを必須としているリポジトリでは、代わりにgh stack rebaseを使ってローカルでリベースしてください。

リベースを行うと、上位のブランチが書き換えられます。チームメイトがそれらのブランチのいずれかをチェックアウトしている場合は、新たにコミットする前に作業を止めてリモートと同期する必要があります。そうしないと、次回のプッシュ時に書き換えられた履歴とコンフリクトが発生します。

スタックをマージする

マージ可能な状態にある最上位のプルリクエストをマージすると、そのPRと、その下にある未マージのPRすべてが1回のマージで取り込まれます。予期しないコンフリクトなどで途中で失敗した場合、マージはそのPRで停止します。それより下にあってすでにマージされたPRはベースブランチに残り、失敗したPRとその上にあるPRはすべてオープンのままになります。下位のレイヤーだけをマージした場合、その上にあるPRはオープンのまま残り、GitHubがそれらをリベースしたうえで、ベースをスタックのベースブランチに付け替えます。マージガイドでは、スタックの一部でもマージするには、スタックが線形の履歴を持っている必要があるとされています。

マージするPRmainに取り込まれるもの残りのPRの扱い
マージ可能な最上位のPR(ui-login)3つのPRすべてオープンのまま残るPRはなし
最下位のPR(auth-middleware)そのPRのみ上位のPRはリベースされ、ターゲットがmainに付け替えられる
中間のPR(api-routes)そのPRと、その下にあるすべてのPRui-loginはオープンのまま残り、ターゲットがmainに付け替えられる

GitHubのスタックの途中にあるPRを単独でマージすることはできません。必ずその下にあるPRと一緒に取り込まれます。GitHubのスタック型プルリクエストのリファレンスによると、スタックはマージコミット、スカッシュ、リベースのいずれにも対応しています。スカッシュの場合は各PRがそれぞれ1つのスカッシュコミットになるため、3つのPRをマージするとmainには3つのコミットが追加されます。

マージルールは、各PRが直接ターゲットにしているブランチではなく、スタックのベースブランチから適用されます。したがって、mainに何が取り込まれるかは、引き続きブランチ保護と必須チェックによって決まります。スタックはマージキューにも対応しており、マージキューはPRを正しい順序で受け取ります。2026年7月30日のchangelogでは、マージキューへの対応は段階的にロールアウトされるとされていましたが、現在のGitHubのマージガイドでは、マージキューは完全にサポートされていると記載されています。

ネイティブ対応以前のスタッキング

GitHubがスタックにネイティブ対応する以前は、ブランチのチェーンを管理するために別のツールが必要でした。Graphite、spr、git-branchlessはいずれも、GitHub本来のプルリクエストのモデルの外側でこうした管理作業を担っていました。

GitHubネイティブのスタック型プルリクエストは、既存のレビュー、必須チェック、ブランチ保護とそのまま連携するため、チームが新たなツールを導入する必要はありません。読みにくい依存PRを回避するためだけにスタッキングツールへ移行したチームにとっては、そうしたツールを使う最大の理由がなくなることになります。

まとめ

GitHubのスタックとは、各プルリクエストが1つ下のブランチをベースとして使用するプルリクエストのチェーンであり、リベース、ターゲットの付け替え、マージはGitHubが処理します。各レイヤーが小さいため、レビュアーはしっかりと読み込むことができ、スタックの途中で変更が生じても、リベースが必要なのはその上にあるブランチだけです。試してみるには、github/gh-stackをインストールし、次に取り組む複数パートからなる機能を3つのレイヤーに分割してみてください。

よくある質問

GitHubのスタック型プルリクエストで自動マージは使えますか?

いいえ。GitHubのマージに関するドキュメントには、スタック型プルリクエストでは自動マージはサポートされていないと記載されています。API経由でマージを行うツールでは、スタックに対して非同期マージAPIを使用する必要があります。従来のプルリクエストのマージエンドポイントではスタックをマージできないためです。チェックが通った時点でスタックを自動的に取り込みたい場合は、代わりにマージキューを使用してください。スタックはマージキューに対応しており、キューはPRをスタックの順序どおりに受け取ります。

スタック型プルリクエストがマージキューから削除されるとどうなりますか?

スタック内でその上にあるすべてのプルリクエストもキューから外れます。スタックを分断させないために、マージキューではマージグループが設定された最大サイズを最大50%まで超えることが許容されます。その余裕分にも収まらない大きさのスタックは、GitHubによって連続する複数のマージグループに分割されます。

最下位のプルリクエストがマージされた後、ローカルのスタックを更新するにはどうすればよいですか?

gh stack syncを実行します。このコマンドは1回の操作で、最新の変更の取得、トランクの更新、残っているブランチのトランク上へのリベース、それらのプッシュ、各プルリクエストのステータスの更新を行います。--pruneを付けると、マージ済みのプルリクエストに対応するローカルブランチが削除されます。syncの途中でコンフリクトが発生した場合は、すべてのブランチが元の状態に戻されるので、gh stack rebaseを実行してコンフリクトを解消し、その後gh stack pushを実行してください。

既存のスタック内のレイヤーを並べ替えたり、削除したり、名前を変更したりするにはどうすればよいですか?

gh stack modifyを使用します。これはインタラクティブなターミナルUIで、ブランチの削除(drop)、統合(fold)、挿入(insert)、並べ替え(reorder)、名前の変更(rename)を行えます。実行するには、ワーキングツリーがクリーンであること、履歴が線形であること、進行中のリベースがないこと、マージ待ちのキューに入っているプルリクエストがないことが条件です。1回のセッションで、ブランチの並べ替えと、削除・統合・挿入・名前の変更を同時に行うことはできません。変更は保存した時点で初めて適用され、gh stack modify --abortを実行すると、開始前の状態にスタックが復元されます。作業後は、gh stack submitを実行してGitHubに反映してください。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.