Stacked PRs:把大改动拆成一条可合并的依赖链
Stacked PRs:把大改动拆成一条可合并的依赖链
最近 GitHub 文档里出现了一个很值得注意的概念:Stacked PRs,中文可以叫“堆叠式拉取请求”。
它解决的是一个很常见的开发问题:一个功能很大,最好拆成多个小 PR
来评审;但这些小 PR
又彼此依赖,后面的工作必须建立在前面的工作之上。如果每个 PR
都必须等前一个合并到 main,开发节奏就会被卡住。
Stacked PRs 的做法是:让后一个 PR 直接以前一个 PR 的分支作为 base,形成一条依赖链。
它不是把一个巨大 PR 按文件随便切开,而是把一项工作按依赖关系切成多个、每个都能独立理解的变更层。
一、先看它长什么样
假设我们要做一个“用户登录”功能,可以按下面的顺序拆分:
main |
这三个 PR 不是三个互不相关的分支,而是一条有方向的依赖链:
feat/auth-layer添加认证模型、共享类型或数据库结构。feat/api-endpoints在认证层之上添加 API 路由。feat/frontend再调用这些 API,添加页面和交互。
因此,PR #2 的改动里不会重复展示 PR #1 已经完成的内容;PR #3 也只展示相对于 PR #2 的新增内容。
这就是“stack”的含义:每一层都站在下面一层之上。
二、它和普通 PR 有什么不同?
普通的 PR 通常是这样:
feature ───────────────→ main |
Stacked PRs 则是这样:
feature-1 ─────────────→ main |
最重要的变化不是分支多了,而是 PR 的 base 不再全部指向
main。
如果三个分支都直接向 main 提 PR,那么后面的 PR
会把前面分支的代码也全部带进
diff,审阅者看到的可能是一个越来越大的混合变更。Stacked PRs 让每个 PR
只显示自己这一层的增量,因此评审者可以按层理解:
PR #1:底层能力是否正确? |
三、它真正解决了什么问题?
1. 不必等前一个 PR 合并
最直接的好处是,前一个 PR 还在 review,你就可以继续开发后面的部分。
比如后端同事正在等待认证层的
review,前端同事不必把工作复制到另一个临时分支,也不必等认证层合并后再开始,而是直接基于
feat/auth-layer 创建 feat/api-endpoints 或
feat/frontend。
这样既保留了依赖关系,又不会让团队停下来排队。
2. 每个 PR 都更容易审查
一个 2000 行的 PR 很难认真 review。即使代码本身没有问题,审阅者也很容易因为范围太大而只做粗略检查。
拆成多个有明确主题的层之后,每个 PR 可以只关注一件事:
| 层 | 关注点 | 适合的评审问题 |
|---|---|---|
| 底层 | 数据结构、共享类型、核心逻辑 | 设计是否稳定?边界是否正确? |
| 中层 | API、服务、业务流程 | 是否正确调用底层能力?错误处理是否完整? |
| 上层 | UI、测试、文档、集成 | 用户行为是否正确?是否覆盖关键路径? |
评审的粒度变小之后,反馈也会更具体:到底是数据模型的问题、API 的问题,还是页面集成的问题,不会全部混在同一个 diff 里。
3. 更适合持续交付和 AI 代理开发
大型功能通常天然存在先后顺序:先改 schema,再改后端,再改前端,最后补测试和文档。
这种顺序本身就很像一个 stack。尤其是使用 AI coding agent 时,每完成一个清晰的子任务就创建一个分支和 PR,可以把“代理做了哪些变更、这些变更之间是什么关系”记录下来,而不是把几十个不同目的的修改堆进一个分支。
四、实际开发流程
方式一:手动理解 stack
先用普通 Git 分支建立关系:
git switch main |
创建 PR 时,base 要逐层指定:
PR #1: feat/auth-layer → main |
这样做能帮助你理解原理,但随着层数增加,手动维护会变得麻烦:底层 PR 合并后,上面的分支都要重新变基,相关 PR 的 base 也要同步更新。
方式二:使用 GitHub CLI 的
gh stack
GitHub 目前提供了 gh stack
扩展来管理本地的堆叠分支。先安装:
gh extension install github/gh-stack |
常用命令大致可以这样记:
# 初始化一个 stack |
其中最关键的是 submit、sync 和
rebase:它们把推送分支、创建或更新
PR、重新建立上下游关系这些容易出错的操作集中处理了。
五、底层 PR 合并后会发生什么?
假设现在的 stack 是:
main ← PR #1 ← PR #2 ← PR #3 |
当 PR #1 合并到 main 后,PR #2 不应该继续以已经合并的
feat/auth-layer 作为长期 base。系统需要把它重新定位成:
main ← PR #2 ← PR #3 |
这就是级联变基(cascading rebase)。PR #2
的独有提交会被重新应用到最新的 main 上,PR #3 再以 PR #2
的新位置为基础。
如果底层改动和上层改动没有冲突,这个过程通常很顺滑;如果底层 PR 的实现方式发生了变化,上层分支就可能出现冲突,需要人工解决。也就是说,Stacked PRs 减少了等待,但没有消除依赖关系带来的协调成本。
六、合并时必须遵守什么顺序?
通常应该从底部向上合并:
PR #1 → PR #2 → PR #3 |
因为上层 PR 依赖下层 PR。底层合并后,上层 PR 会自动重新定位到新的 base 分支。
GitHub 的堆叠 PR 也支持一次合并整个 stack,或者合并其中一段。但理解上仍然可以记住一句话:依赖先合并,被依赖的代码后合并。
另外,堆叠中的每一层仍然要满足仓库的规则和 CI
要求。分支保护、CODEOWNER 审批和 GitHub Actions 不应该因为某个 PR
不是直接面向 main 就被绕过。
七、什么时候适合用,什么时候不适合用?
适合使用的情况:
- 一个大功能可以自然拆成多个有先后关系的主题。
- 后面的开发确实依赖前面的代码,但不想等待前一个 PR 合并。
- 团队希望小步 review,而不是等所有工作完成后一次性提交。
- 代码库有较好的自动化测试、分支保护和 PR 管理习惯。
不适合使用的情况:
- 各个改动其实互不依赖,只是为了“看起来像一个 stack”而强行串起来。
- 每一层都很小,拆分后反而增加沟通和维护成本。
- 团队不熟悉 rebase,或者没有明确谁负责维护整个 stack。
- 分支会被很多人同时随意修改,导致 stack 经常发生分叉。
一个很实用的判断标准是:如果删除下面一层,上面一层仍然可以独立存在,那它们可能应该是普通的并行 PR;如果上面一层必须建立在下面一层之上,stack 才有意义。
八、最容易踩的坑
1. 把 stack 当成“超长分支”
Stack 的目的不是让一条分支永远叠很多东西,而是让每一层都保持可理解、可评审、可合并。如果某一层已经变成“顺手把所有剩余工作都放进去”,就失去了拆分的意义。
2. 上层 PR 混入下层未完成的内容
创建上层分支前,应该先把当前层的提交边界整理清楚。每一层最好代表一个稳定的逻辑单元,而不是半个功能加上一堆临时修改。
3. 变基后忘记同步远程
rebase 会改写提交历史。stack 重新变基后,通常需要使用安全的强制推送方式更新远程分支;不要在不了解影响的情况下直接覆盖其他人的分支。
4. 评审顺序混乱
最下面的 PR 发生重大变化时,上面的 diff 也可能跟着变化。审阅者最好从底层开始看,开发者也应该在底层设计变化后及时通知上层 PR 的审阅者。
5. 忘记它仍是预览功能
GitHub 文档目前将 Stacked PRs 标记为 Public Preview,功能和命令未来可能发生变化;同时,所有 stack 分支必须位于同一个仓库中,GitHub Desktop 目前也不支持这种工作流。
九、我的理解:Stacked PRs 是“依赖关系可视化”
Stacked PRs 最有价值的地方,不只是帮我们少敲几条 Git 命令,而是把代码之间的依赖关系显式化了。
普通的大 PR 往往把这些关系都压扁在一个 diff 里:数据库结构、后端服务、前端页面和测试同时出现,审阅者只能自己在脑子里重建顺序。
Stack 则把这个顺序写在分支和 PR 的 base 关系里:
基础能力 → 业务能力 → 用户界面 → 测试与收尾 |
因此,它更像一种协作结构,而不只是 Git 技巧。它要求我们在提交代码之前先回答一个问题:这项改动依赖什么,谁依赖它?
如果这个问题能回答清楚,Stacked PRs 就能让大型改动变得更容易并行、更容易评审,也更容易在中途停下来合并一部分成果。
参考资料
- GitHub:关于堆叠式拉取请求
- GitHub:堆叠式拉取请求 CLI 命令
- GitHub:堆叠式拉取请求快速入门