2491 字
12 分钟
更新于 2026-08-05
2 次修订
GitHub Stacked PRs 使用初体验

文章首图

今天注意到 GitHub 把 Stacked PRs 开放成 Public Preview 了,不再是之前需要排 waitlist 才能体验的状态。

恰好我有一个很早很早之前的 PyQT 番茄钟项目最近想要重构为 Tauri + Rust,于是乎我就先来体验一下。

gh stack 是什么?

它有点像 gh 的一个 cli extension,可以让 PR 们可以用一种和谐的状态关联到一起,而不再各自独立,只能 mention 彼此相互称兄道弟。

它让所有的 PR 不再只指向 main branch,而是可以在 feature branch 上进行堆叠。

Stacked PRs 关系概览

它带来什么?

这是我之前写的:

大 PR 很难审查,流程很长,而且冲突阻塞关系很难明确。rebase 很频繁
小 PR 审查起来快,但是关系只能靠 mention 连接,很碎片,关系不明。我习惯提原子 PR,经常就有这类问题,理不清做了什么。
gh stack 可以兼顾两者的优点,而且协作也会更原子化。

不废话直接开始。

它怎么创建?

具体怎么创建可以看 : Creating stacked pull requests

但是其实如果依赖 claude code,codex 等工具的话,github 官方也有提供对应 Skill installation。这点倒是和 memU 最近做的 Skill 注入有点像。

直接跟你的 codex 或者 claude 说,请帮我全局安装 github 新推出的 gh stack 以及它的 Skill 就可以了。

也就是说我们可以完全不用关心命令怎么敲命令,我们可以直接进入主视角。

因为虽然不关心它具体操作,但我们得理解它的具体形态。以及有哪些 benifits。

在 maintainer 视角里的 Stacked PRs

是否支持先后合入?

单独合并 bottom PR

一次合并多层 Stack

支持并且也只能按照先后顺序合入。

这里可以看到, bottom 的 PR 支持一个一个合入。

但是在上方的 PR 每次合入的时候都会把下方几个一起合入,当我的指针在第三层的时候,一次会合入三个 PR。

增量式的 review

以上图为例, #23 具有 #23 所有的代码。

而 #24 具有 #23 所有的代码与 #24 的增量代码,但 review 的时候只会看到 #24 diff #23

所有的 review 都是增量式的,不会重叠。

在 Contributer 视角里的 Stacked PRs

开幕雷击

当前所有的 Stack PRs 的分支必须位于同一个仓库并且和被 merges 的分支位于同一个仓库。

比如:

Terminal window
xnne-bot wants to merge 4 commits into NevaMind-AI:main

我从 fork 仓库仓库向 upstream PR,那么我的所有 PR 不能形成 stack PRs,只有都位于 nevamind-AI/memU 的仓库才能被合并到一起。

至少目前是这样的。

是否支持树状堆叠?

官方给的示例大概是这样的。

Terminal window
L0 main
L1 └── auth-layer
L2 └── api-endpoints
L3 └── frontend

它是一个线性 layer。

我看到的第一个念头是,能不能是一个 Tree 而不是一个 Stack?

从目前来看是不支持的,所有 PR 都必须严格遵循类似依赖的关系。上方 PR 合入的前提是下方的 PR 一起跟着合入。

如果两个 PR 位于同一层会发生什么?

Opus

两个 PR 不能作为同一个 Stack 中的“同一层”存在。

假设 PR B 和 PR C 都以 PR A 的 branch 为 base,那么从 Git 和普通 PR 的角度看完全合法,但它们会成为两个并列的 sibling PR,而不再属于同一条线性 Stack。合并其中一个,也不会把另一个一起合入。

如果 B 和 C 存在先后依赖,就需要把它们排列成 A → B → C;如果它们彼此独立,只是共同依赖 A,更合适的做法是先合并公共基础 A,再让 B 和 C 分别从新的 trunk 开始。

换句话说,Stack 要求每层都有唯一的上一层和下一层。一旦出现同层分叉,它就不再是一个 Stack,而是一张普通的 branch dependency graph。

也就是说, Stack 不应该包含这种并列的关系。它只为了处理线性的包含依赖的那种关系。如果存在这种并列关系,说明 stack 该在这里断一下。

渐进式地、递增式地拆解一个 PR。

上层的 PR 是否默认具有了下层的所有代码且实时变动?

如果只是简单拼图的话,和我们的 mention 实际上就能做到。

但是 mention 最烦的一点是,如果 PR B 具有 PR A 的所有代码,PR B 依赖于 PR A,但是这个时候 PR A 变了,我们必须手动对齐 PR B 和 PR A。

这也是我们很多时候不愿意把一个 PR 拆开最后变得很大的原因。

而 Stack PR 解决了这个难题。如果下层的 PR 所有内容都被上层所包含,并且下层的 PR 有任何变动都会自动同步上层。整体上就像一个 PR,但是 review 起来是不同的 part。

Opus

只说对了一半:上层包含下层代码是真的,“下层变化会实时自动同步到上层”则不是真的。

假设我们把一次重构拆成五层:

main
└── PR A:数据模型
└── PR B:核心服务
└── PR C:API
└── PR D:前端界面
└── PR E:集成测试

这些 branch 在 Git 历史上是一条连续的链:

main ← A0 ← B0 ← C0 ← D0 ← E0

因此 E 的 branch 确实包含 A、B、C、D 的代码。不过每张 PR 的 base 都是它正下方的 branch,所以 GitHub 查看 PR E 时,正常情况下只会显示 E 这一层的测试改动,而不会把前面四层的 diff 全部重新展示一遍。

现在假设最底层的 PR A 又新增了一个提交 A1。A 的 branch 向前移动了,但 B 到 E 不会实时跟着移动,而是暂时分叉成这样:

main
└── A0
├── A1 ← PR A 的新位置
└── B0 ← C0 ← D0 ← E0 ← 上面四层仍在旧链上

如果没有 Stacked PRs,我们需要自己依次完成四次对齐:

把 B rebase 到 A1
把 C rebase 到新的 B
把 D rebase 到新的 C
把 E rebase 到新的 D

gh stack 知道整条依赖链,因此站在 A 这一层执行:

Terminal window
gh stack rebase --upstack

它会执行级联 rebase,最终得到:

main ← A0 ← A1 ← B1 ← C1 ← D1 ← E1

这里的 B1E1 是 rebase 后的新提交,虽然某一层的代码可能没有发生实质变化,但 commit SHA 会被重写。之后还需要把更新后的 branches push 到远端。也可以直接执行:

Terminal window
gh stack sync

sync 会完成 fetch、整条 stack 的级联 rebase、push,以及 GitHub 上 PR 和 stack 状态的同步。正常情况下,无论上面还有两层还是十层,开发者执行的都只是同一条命令,层数不会线性增加操作步骤

如果修改发生在中间的 PR C,那么 A 和 B 不需要变化,只需从 C 开始重新对齐 D 和 E。在 C 的 branch 上运行 gh stack rebase --upstack 即可:

main ← A ← B ← C1 ← D1 ← E1

真正可能变麻烦的是冲突,而不是 sync 命令本身。级联 rebase 时,每一层都要在它的新下层之上重放提交:A 的改动如果同时影响了 B、C、D、E 修改的代码,那么这些层都有可能依次产生冲突。gh stack sync 遇到 rebase 冲突时不会留下同步了一半的 stack,而是恢复各 branch 原来的状态,并提示改用 gh stack rebase 处理;解决当前冲突、暂存文件后执行 gh stack rebase --continue,如果后面的层再次冲突,就要继续处理。

此外,rebase 会重写所有受影响的上层 branch,因此远端 PR 需要更新,CI 也可能重新运行;仓库如果启用了“有新提交时撤销 approval”之类的保护规则,已有 review 状态也可能受到影响。Stack 越深、各层修改的代码越重叠,这些成本越明显。

所以 Stacked PRs 确实解决了手动逐层对齐的问题,但不是让同步消失了,而是把它收敛成一个知道整条依赖链的级联操作:没有冲突时,多级 sync 并不麻烦;发生冲突时,仍要按照层级逐个解决。实践中应该尽量让每层改动聚焦、减少跨层修改同一片代码,而不是无限增加 Stack 的深度。

非常清晰了,也就是说 sync 依然是开发者或者 Agent 手动执行的,但是比起以前复杂的 rebase diff fetch 的操作,目前非常地简单,只需要运行一条命令即可。但是对于冲突的部分,依然需要手动解决。

本地分支在 sync 操作下是什么样的?

上一节提到了上层 PR 包含下层所有 PR,下层变动用 sync 同步。

如果从本地分支的视角来看,所有分支都会一起变动吗?另外分支是否包含所有代码?不同层之间的 branch 之间的 diff 最终是如何转换到 PR 里被我们 review 的增量式 diff 的?对于有 stack-link 的分支之间,它们分支有什么特殊标记或者关联吗?

Opus

结论:上层 branch 包含下层代码;sync 只移动需要对齐的 branch;Stack 关系不是 Git 的特殊能力。

假设本地有四层:

main ← A ← B ← C ← D
  • checkout A:包含 main + A
  • checkout C:包含 main + A + B + C
  • checkout D:包含整条链的代码

如果 A 发生变化,执行 gh stack sync 后,B、C、D 会被级联 rebase,branch 指针和 commit SHA 随之更新;如果只修改 C,则通常只有 C、D 需要更新。它改变的是 branch 指针,不是同时修改多份工作目录。

PR 之所以只显示自己这一层,是因为每张 PR 都和正下方的 branch 比较:

PR A:main → A
PR B:A → B
PR C:B → C
PR D:C → D

所以 D 虽然包含 A、B、C 的代码,PR D 展示的仍然只有 C..D 的增量 diff。

这些 branch 对 Git 来说仍然只是普通 branch,没有特殊的 Stack 标记。关系主要记录在两处:

  1. 每张 PR 的 base/head 关系;
  2. GitHub 上用于展示和管理的 Stack 元数据。

gh stack init 还会维护本地层级信息;gh stack link 则可以跳过这份本地 tracking state,直接把已有 PR 按顺序链接成 GitHub Stack。

ok 了解,也就是说 sync 的时候其实不会因为有 N 层把同一份文件改 N 次,恰恰每个文件应该只改了一次,然后其他的 branch 的 head 指向了新文件。

这么看来 github stack 实现起来也相当简单。只需要知道每个 PR branch 各自的 base 是谁就好了。

GitHub Stacked PRs 使用初体验
https://xnnehang.top/posts/github-stacked-prs/
作者
XnneHang
发布于
2026-08-05
许可协议
CC BY-NC-SA 4.0