- Published on
GitHub 堆叠 PR 完全指南 —— 原生堆叠 PR 解决了什么,又原样留下了什么
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 —— 一份要求评审 1,400 行代码的 PR
- 堆叠解决的问题 —— 按依赖顺序切分评审单元
- GitHub 原生堆叠实际做了什么
- gh-stack —— 命令级别的工作流
- 变基级联 —— 堆叠真正的代价
- 与现有工具相比有何不同
- 堆叠反而得不偿失的场景
- 结语 —— 工具自动化的是变基的执行,不是设计本身
- 参考资料
引言 —— 一份要求评审 1,400 行代码的 PR
评审请求发过来,改动了 47 个文件,新增了 1,400 行代码。数据库迁移、仓储层、API 处理器、前端表单全都塞在同一个 PR 里。评审者实际上只有三个选择:整整腾出一天时间、随便点个批准,或者回复"请拆分一下"。选第三个的话,麻烦就轮到作者头上了 —— 在 git 上把一组相互依赖的改动拆成多个 PR,意味着每次评审意见落在下面某个分片上时,都得把上面的全部改动重新变基(rebase)一遍。
2026 年 7 月 30 日,GitHub 把 Stacked Pull Requests 推进了公开预览阶段。这项功能会在几天内陆续推送到所有代码仓库,Web 端、移动端、CLI 都能用。Hacker News 的讨论帖吸引了几百条评论,反应分成两派 ——"总算来了"和"这不就是 Phabricator、Gerrit 十年前就在做的事吗"。具体的点赞数会随时间变化,这里就不引用具体数字了。
本文不是发布公告的摘要,而是一份关于堆叠这套工作流本身的指南。会依次讲清楚堆叠真正解决的问题、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 描述里手写的清单。以前堆叠工具靠往 PR 正文里自动插入一份 markdown 列表来模拟的这个功能,现在变成了服务端的原生能力。
第二,分层 diff。 评审者打开 PR 时,只会看到那一层的改动。这其实是当基础分支被设为下层分支时本来就有的行为,区别在于,下层一旦合并或被变基之后,服务端会负责维持这个状态的一致性。
第三,两种合并方式。
- 合并已就绪的最顶层 PR,会把它下面所有尚未合并的层作为一次操作一起落地。
- 只先合并下面的某一层,上面的那些 PR 就会自动变基、重新对准新的目标。这原本是靠手动操作或者依赖 CLI 工具来完成的。
第四,与现有保护规则的兼容。 分支保护、必需检查、评审规则依然原样适用于堆叠中的每一个 PR。合并队列(merge queue)支持只写着"将在未来几周内逐步推出",所以截至本文写作时,不能断定它已经在所有代码仓库里都能用。
这里有一点需要说清楚。GitHub 的堆叠是分支级别的,不是提交级别的评审。HN 讨论帖里被反复提及最多的批评,正是这一点 —— 如果你期待的是 Phabricator 或 Gerrit 那种一个提交即一个评审单元、还能看 interdiff(增量对比)的模式,那这次预览提供的并不是那个东西。你依然得为每一层单独建分支,而且 GitHub 原有的"强制推送改变 diff 后评审意见会消失"这个行为也没有变。
gh-stack —— 命令级别的工作流
CLI 扩展负责本地这一侧。堆叠的元数据以 JSON 格式存放在 .git/gh-stack 里,不会被提交进版本库。也就是说,堆叠的顺序信息是本地状态,服务端是靠 PR 之间的 base 关系来识别堆叠的。
# 安装 (需要 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 base 关系 | sync、rebase 命令,下层合并时服务端自动重新对齐 | 堆叠地图、分层 diff、原子化合并 | 预览期免费 |
| Graphite | 分支 | 自有服务 + 本地 CLI | gt sync、restack 自动完成 | 自有 Web 评审界面、合并队列 | 付费 SaaS(有免费额度) |
| ghstack | 提交 | 嵌在提交信息里的标识符 | 把整个提交堆叠原样重新推送进去 | 无(只创建 PR) | 开源 |
| git-branchless | 提交 | 本地事件日志 | 用 git move、git sync 移动子树 | 无 | 开源 |
| Sapling | 提交 | 自有 VCS 状态 | 堆叠编辑是默认行为 | 支持创建 GitHub PR | 开源 |
| 纯 git | 分支 | 无 | 仅 rebase.updateRefs | 无 | 免费 |
核心差异可以归结为两句话。提交级别的工具(ghstack、git-branchless、Sapling,以及它们精神上的继承者 jj)采用"一个提交即一次评审"的模型,把分支这个概念藏在用户看不到的地方。分支级别的工具(Graphite、GitHub 原生)则原样保留 GitHub 的 PR 模型,在其上叠加堆叠的概念。
GitHub 选择分支级别,看起来是唯一一种能在不与现有的分支保护、必需检查、CODEOWNERS 产生冲突的情况下叠加上去的方案。但作为交换,它也没能获得提交级别评审的那些优点 —— interdiff、保留完整的提交历史、重写后依然存活的评审意见。HN 讨论帖里的大部分批评,说的都是这笔交换。
如果已经在用 Graphite,现在没有太大理由要换过来。自动变基和堆叠可视化两边都有,而且 Graphite 在合并队列和评审界面上更成熟一些。反过来,如果此前一直没用任何工具、靠纯 git 硬扛,原生堆叠是引入摩擦最小的选择 —— 不需要新开账号,不需要新的 Web 界面,也不需要新的权限审批。
堆叠反而得不偿失的场景
堆叠不是免费的,也不是对所有团队都是净收益。在下列情况下,最好不要用它。
改动实际上并不相互依赖时。 如果是三个彼此独立的修改,直接各自基于 main 建三个 PR 就行。一旦把它们捆成一个堆叠,就凭空制造出了原本不存在的顺序依赖 —— 底层的 PR 一旦卡在评审阶段,其余的也会跟着被拖住。
堆叠的高度远超 3 层时。 级联的成本随高度增长,CI 成本也一样。一个高度为 8 的堆叠,通常不是"拆分得很好",而是"本该在设计阶段就切开的东西,被拖到评审阶段才切"。这种情况下,往往放到 feature flag 后面、直接落地到 main 会更好。
只有一个评审者、而且时间充裕时。 堆叠收益中很大一部分来自并行评审。如果反正都是一个人依次看完,拆成多层并不会缩短总评审时间,只会给作者增加变基负担。
分支由多人共享时。 前面提到过,堆叠的前提是频繁强制推送。如果团队习惯结对开发,或者多人在同一个分支上提交,用长期存在的功能分支加定期合并,会比堆叠更安全。
只用squash合并、但项目又很看重提交历史时。 GitHub 的堆叠是把每一层作为独立 PR 分别合并,所以层级别的历史会保留下来,但层内部的提交会在 squash 策略下消失。如果需要提交级别的存档,最好去看看 Sapling 或 jj 这类工具。
总结一下,堆叠真正好用的条件其实很窄 ——依赖顺序清晰、堆叠高度在 2 到 4 层之间、有多个评审者、每一层本身也能独立部署。越是偏离这些条件,变基的成本就越容易超过收益。
结语 —— 工具自动化的是变基的执行,不是设计本身
GitHub 原生堆叠把以前第三方工具一直在模仿的东西 —— 堆叠地图、分层 diff、下层合并后的自动重新对齐、原子化合并 —— 全部变成了服务端原生功能。光是不需要另外注册一个服务这一点,就大幅降低了引入门槛。
- 现在就可以确认:用
gh extension install github/gh-stack安装,在下一次大改动上试一次高度为 3 的堆叠。合并队列的集成还在逐步推出中,所以要先在团队的代码仓库里确认实际行为。 - 一定要打开:
rerere.enabled和rebase.updateRefs。不管用哪种工具,这两项都能把级联成本降到最低。 - 不要期待:提交级别的评审和 interdiff。这次预览是分支级别的模型,并没有在此基础上重现 Phabricator、Gerrit 的评审体验。
堆叠不会把一个大改动变小,它只是减少了拆分一个本来就能被干净拆开的改动时所产生的摩擦。找到该在哪里切开,依然是设计要做的事。
参考资料
- Stacked pull requests are now in public preview — GitHub Changelog (2026-07-30)
- github/gh-stack —— CLI 扩展代码仓库与命令参考
- GitHub Community Discussion #201439 —— 堆叠 PR 预览发布讨论帖
- Hacker News — GitHub Stacked PRs 讨论
- Graphite — Stacked diffs 指南
- ezyang/ghstack —— 提交级别的堆叠工具
- arxanas/git-branchless —— 基于本地事件日志的堆叠编辑
- Sapling SCM —— 把堆叠作为默认模式的版本控制系统
- git-rebase 文档 —— update-refs 与 onto 选项
- git-rerere 文档 —— 复用冲突解决方案
- 写出会被合并的 PR 和提交(相关文章)
- 代码评审沟通(相关文章)