Skip to content

필사 모드: GitHub Stacked PR完全ガイド — ネイティブスタックPRが解決したこととそのまま残したこと

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

はじめに — 1,400行のPRをレビューしてほしいという依頼

レビュー依頼が来たが変更ファイルが47個、追加行数が1,400行だとします。マイグレーション・リポジトリ層・APIハンドラー・フロントエンドフォームが一枚に全部入っています。レビュアーが選べる選択肢は事実上三つです。丸一日空けるか、適当に承認するか、「分けてください」と答えるか。三番目を選ぶと今度は作成者が困ります — git上で依存関係のある変更を複数PRに切り、下の断片にレビューフィードバックが来るたびに上全体をリベースしなければならないからです。

2026年7月30日、GitHubがStacked Pull Requestsをパブリックプレビューとして公開しました。数日かけて全リポジトリに順次ロールアウトされ、Web・モバイル・CLIすべてで動作します。Hacker Newsのスレッドは数百件のコメントがつくほど反応が大きく、流れは「ついに」と「Phabricator・Gerritが10年前にやっていたこと」に分かれました。正確な投票数は時点によって変わるので、ここでは数字を引用しません。

この記事は発表の要約ではなく、スタックワークフローそのものについての案内書です。スタックが実際に解決する問題、GitHub実装がすることとしないこと、そしてスタックの本当のコストであるリベースカスケードを順に見ます。PRを分割する一般論はマージされるPRとコミットの書き方編で扱ったので、ここではスタックという構造に集中します。

スタックが解決する問題 — レビュー単位を依存順序で切る

大きな変更を小さく分けろという助言は古くからありますが、実務ではよく失敗します。理由は単純です。依存関係のある変更はそのまま分けられません。 スキーマのマイグレーションなしにはリポジトリ層がコンパイルできず、リポジトリなしにはAPIハンドラーが動きません。ブランチを三つに分けると、二番目のブランチはmainではなく一番目のブランチをベースにしなければならず、その瞬間PR画面には一番目のブランチの変更まで全部混ざって出てきます。

スタックはこの問題を「ベースをトランクではなく下の層にする」という一つのルールで解きます。

main
 └── feat/schema        PR #1   base: main
      └── feat/repo     PR #2   base: feat/schema
           └── feat/api PR #3   base: feat/repo
                └── feat/ui PR #4  base: feat/api

この構造が与えるものは三つです。

  • レビューdiffの縮小。 PR #3を開くとAPIハンドラーの変更だけが見えます。下の二層はすでにベースに含まれているのでdiffから外れます。
  • レビューの並列化。 DBAが#1を、バックエンドレビュアーが#2と#3を、フロントエンドが#4を同時に見ます。順次待ちが消えます。
  • 部分着陸。 #1と#2が承認されれば、上のレビューが終わる前に先にマージできます。レビュー遅延が全体を止めません。

逆にスタックが作り出すものも一つあります。下の層が変わるたびに上全体を並べ直さなければならないということ — 後で改めて見ます。

GitHubネイティブスタックが実際にすること

チェンジログとgh-stackリポジトリ基準で、今回のプレビューがサーバー側で提供するのは大きく四つです。

第一に、スタックマップです。 各PRの上部に、このPRがスタックのどの層にあり、上下に何があるかを示す地図が付きます。レビュアーは「この変更が全体のどのあたりか」をPR説明の手書きチェックリストではなくUIで確認します。これまでスタックツールがPR本文にマークダウンのリストを自動挿入して真似していた部分がサーバー機能になりました。

第二に、層別diffです。 レビュアーがPRを開くとその層の変更だけが表示されます。これは実はベースブランチが下の層に設定されていればもともとそう動作しますが、下の層がマージまたはリベースされた後もその状態が維持されるようサーバーが管理してくれる点が違います。

第三に、二種類のマージ方式です。

  • 準備の整った最上段のPRをマージすると、その下の未マージ層が一回の作業でまとめて着陸します。
  • 下の層だけを先にマージすると、上のPR群が自動でリベースされベースが再調整されます。これまで手作業かCLIツールに任せていた部分です。

第四に、既存の保護ルールとの互換性です。 ブランチ保護、必須チェック、レビュールールがスタックの各PRにそのまま適用されます。マージキューのサポートは「数週間かけて段階的にロールアウト」とだけ書かれており、この記事の時点ですべてのリポジトリで動作すると断定はできません。

ここではっきりさせておくことがあります。GitHubのスタックはブランチ単位です。コミット単位のレビューではありません。HNスレッドで最も繰り返された批判が正確にこの点でした — PhabricatorやGerritのようにコミット一つがそのままレビュー単位でインターディフ(interdiff)を見られるモデルを期待していたなら、今回のプレビューはそれではありません。層ごとにブランチを作る必要があり、強制プッシュでdiffが変わるとレビューコメントが消えるというGitHubの既存動作もそのままです。

gh-stack — コマンド単位のワークフロー

CLI拡張がローカル側を担当します。スタックのメタデータは.git/gh-stackにJSONとして保存され、コミットされません。つまりスタックの順序情報はローカル状態で、サーバーはPRのベース関係でスタックを認識します。

# インストール (gh 2.0以上)
gh extension install github/gh-stack

# 1) トランクからスタックを開始
git switch main && git pull --ff-only
gh stack init feat/schema
git commit -am "add nullable columns for new pricing model"

# 2) 層を積む — 現在のブランチの上に新しいブランチを作る
gh stack add feat/repo
git commit -am "repository reads new columns behind a flag"
gh stack add feat/api
git commit -am "expose pricing endpoint"

# 3) 現在のスタックを確認
gh stack view

# 4) 三つの層をPRにしてお互いを連結する
gh stack submit

レビューフィードバックが下の層に来たときがスタックの本題です。

# 下へ二段下がって修正
gh stack down
gh stack down
git commit -am "address review: keep columns nullable for one release"

# フェッチ -> カスケードリベース -> プッシュ -> PR状態同期を一度に
gh stack sync

# リベースだけ別に回したいなら
gh stack rebase

# 層の順序を変えたり合わせたりする(線形履歴が必要)
gh stack modify

# 準備の整った地点まで原子的にマージ
gh stack merge

移動コマンドが別途あるという点が実際の使用で思ったより大きいです。gh stack up / down / top / bottom / trunkで層の間を行き来し、gh stack checkoutで特定のPR番号やブランチへ直接行きます。複数のスタックに同時に属するブランチを指定すると、コマンドが終了コード6であいまいさを知らせるので、スクリプトでこのコードを分岐条件として使えます。

文書に書かれた制約も事前に知っておく価値があります。gh stack push原子的ではありません。ブランチごとに--force-with-leaseをかけて順次プッシュするので、途中で一つのブランチが拒否されるとそのブランチだけ失敗状態で残り再実行が必要です。ローカルのスタックとリモートが大きく乖離した場合、syncは対話モードでしか解決を尋ねないので、CIで無人実行する対象ではありません。

リベースカスケード — スタックの本当のコスト

スタックのコストはブランチを複数作る手間ではありません。下が動けば上が全部揺れるという構造そのものです。

高さnのスタックで底の層を一度修正すると、その上のn-1個のブランチを順に リベースしなければなりません。この際、同じ衝突が層ごとに繰り返し現れることがよくあります。底で関数シグネチャを変えたなら、その関数を使う全上層で同じ形の衝突を再び解かねばならないからです。ツールが自動化してくれるのはリベースの実行であって衝突の判断ではありません。

このコストを減らす手段はツールと無関係に三つあります。

# 1) 同じ衝突を二度解かない — rerereは解決結果を記録して再利用する
git config --global rerere.enabled true

# 2) リベースが中間ブランチの参照も一緒に動かすようにする (Git 2.38+)
git config --global rebase.updateRefs true

# 3) ベースが変わった場合の正確な移植 — 移す区間を明示する
git rebase --onto origin/main <前のベースコミット> feat/api

rerereはスタックワークフローで体感差が最も大きい設定です。同じ衝突解決を層ごとに繰り返し入力していた作業がなくなります。rebase.updateRefsはGit 2.38で入ったオプションで、一回のリベースでスタック全体のブランチポインタを一緒に前進させます — 事実上純粋なgitだけでする最小限のスタック対応です。

そしてカスケードが発生するたびに上のPRのCIが全部再実行されます。 高さ5のスタックで底を三回直すとCI実行は15回です。ここにマージキューが付くとキュー進入時にもう一回回る可能性があります。スタックがレビュー時間を減らす一方でCIコストを増やすという事実は、導入前に計算しておくべき項目です。

最後に協業面での落とし穴が一つ。スタックのブランチは強制プッシュが日常です。他の人が同じブランチにコミットを重ねていたら、そのコミットが消える可能性があります。--force-with-leaseが基本の防御線ですが万能ではないので、スタックブランチは単一作成者所有というルールをチームで明示しておくほうが安全です。

既存ツールと何が違うか

スタックはGitHubが最初に作った概念ではありません。むしろこの領域はツールが過剰供給されていたほうに近いです。

ツールレビュー単位状態をどこに置くかカスケード自動化サーバー側統合コスト
GitHubネイティブ + gh-stackブランチローカル.git/gh-stack + サーバーのPRベース関係sync・rebaseコマンド、下位マージ時にサーバーが自動再調整スタックマップ、層別diff、原子的マージプレビュー期間無料
Graphiteブランチ自社サービス + ローカルCLIgt sync・restackで自動自社Webレビュー UI、マージキュー有料SaaS(無料枠あり)
ghstackコミットコミットメッセージに埋め込んだ識別子コミットスタックをまるごと押し戻す方式なし(PR作成のみ)オープンソース
git-branchlessコミットローカルイベントログgit movegit syncで部分木を移動なしオープンソース
Saplingコミット自社VCS状態スタック編集が標準動作GitHub PR作成に対応オープンソース
純粋gitブランチなしrebase.updateRefsのみなし無料

核心の違いは二行に要約できます。コミット単位のツール(ghstack、git-branchless、Sapling、そして精神的後継のjj)は「コミット一つがそのままレビュー一つ」というモデルを取り、ブランチをユーザーから隠します。ブランチ単位のツール(Graphite、GitHubネイティブ)はGitHubのPRモデルをそのまま残し、その上にスタックの概念を載せます。

GitHubがブランチ単位を選んだのは、既存の保護ルール・必須チェック・CODEOWNERSと衝突なしに載せられる唯一の選択だったからだと見えます。その代わりコミット単位レビューの利点 — インターディフ、コミット履歴の保存、書き換え後も生き残るレビューコメント — は得られませんでした。HNスレッドの批判の大半はこの交換についてのものでした。

すでにGraphiteを使っているなら今すぐ移る理由は大きくありません。自動リベースとスタック可視化は両方ともでき、Graphite側がマージキューとレビューUIでより成熟しています。逆にまだ何のツールもなく純粋gitで耐えていたなら、ネイティブスタックは導入摩擦が最も低い選択肢です — 新しいアカウントも新しいWeb UIも新しい権限承認も要りません。

スタックが損になる場合

スタックはタダではなく、すべてのチームにとって得でもありません。次のような場合は使わないほうがよいです。

変更が実際には依存的でないとき。 お互いに独立した三つの修正なら、そのままPR三枚をそれぞれmainの上に作ればよいです。スタックとしてまとめた瞬間、存在しなかった順序依存が生まれ、底のPRがレビューで詰まると残りも一緒に足止めされます。

層の高さが3をはるかに超えるとき。 カスケードコストは高さに比例し、CIコストもそうです。高さ8のスタックは大抵「うまく分割できた」ではなく「設計段階で切るべきだったものをレビュー段階で切っている」状態です。フィーチャーフラグの裏に入れてmainに直接着陸させたほうがよい場合が多いです。

レビュアーが一人で余裕があるとき。 スタックの利益の大きな部分は並列レビューです。どのみち一人が順番に見るなら、層を分けても総レビュー時間は減らず、作成者のリベース負担だけ増えます。

ブランチを複数人で共有するとき。 先に書いた通りスタックは強制プッシュを前提とします。ペア作業や複数人が一つのブランチにコミットする文化なら、スタックの代わりに長生きするフィーチャーブランチと定期的なマージのほうが安全です。

スカッシュマージのみ使うがコミット履歴が重要なプロジェクトのとき。 GitHubのスタックは各層を個別PRとしてマージするので層単位の履歴は残りますが、層内のコミットはスカッシュ方針に従って消えます。コミット単位のアーカイブが必要ならSapleyやjj系を見るほうがよいでしょう。

まとめるとスタックがうまく効く条件は狭いです — 依存順序が明確で、層が2から4層で、レビュアーが複数いて、各層が単独でもデプロイ可能な変更であること。 この条件から外れるほど利益よりリベースコストが大きくなります。

おわりに — ツールが自動化したのはリベースの実行であって設計ではない

GitHubネイティブスタックはこれまでサードパーティツールが真似していたもの — スタックマップ、層別diff、下位マージ後の自動再調整、原子的マージ — をサーバー機能に引き上げました。別サービスへの加入なしに使えるという点だけでも導入障壁が大きく下がりました。

  • 今確認すること: gh extension install github/gh-stackでインストールし、次の大きな変更一件だけ高さ3のスタックを試してみてください。マージキュー連携はまだロールアウト中なので、チームのリポジトリで実際の動作を先に確認する必要があります。
  • 必ず有効にすること: rerere.enabledrebase.updateRefs。どんなツールを使うにせよ、カスケードコストを最も大きく減らしてくれます。
  • 期待しないこと: コミット単位のレビューとインターディフ。今回のプレビューはブランチ単位モデルで、その上でPhabricator・Gerritのレビュー体験を再現してはいません。

スタックは大きな変更を小さくしてはくれません。すでに小さく切れる変更を切るときの摩擦を減らすだけです。切るべき地点を見つける作業は依然として設計の仕事です。

参考資料

현재 단락 (1/89)

レビュー依頼が来たが変更ファイルが47個、追加行数が1,400行だとします。マイグレーション・リポジトリ層・APIハンドラー・フロントエンドフォームが一枚に全部入っています。レビュアーが選べる選択肢は...

작성 글자: 0원문 글자: 8,287작성 단락: 0/89