Skip to content

필사 모드: GitHub 堆叠 PR 完全指南 —— 原生堆叠 PR 解决了什么,又原样留下了什么

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

引言 —— 一份要求评审 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分支自有服务 + 本地 CLIgt sync、restack 自动完成自有 Web 评审界面、合并队列付费 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 产生冲突的情况下叠加上去的方案。但作为交换,它也没能获得提交级别评审的那些优点 —— 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.enabledrebase.updateRefs。不管用哪种工具,这两项都能把级联成本降到最低。
  • 不要期待:提交级别的评审和 interdiff。这次预览是分支级别的模型,并没有在此基础上重现 Phabricator、Gerrit 的评审体验。

堆叠不会把一个大改动变小,它只是减少了拆分一个本来就能被干净拆开的改动时所产生的摩擦。找到该在哪里切开,依然是设计要做的事。

参考资料

현재 단락 (1/89)

评审请求发过来,改动了 47 个文件,新增了 1,400 行代码。数据库迁移、仓储层、API 处理器、前端表单全都塞在同一个 PR 里。评审者实际上只有三个选择:整整腾出一天时间、随便点个批准,或者回...

작성 글자: 0원문 글자: 7,027작성 단락: 0/89