GitHub 堆叠式 Pull Request 的工作原理
了解 GitHub 堆叠式拉取请求的运作方式:使用 gh-stack 创建堆栈、逐层审查、变基更新并合并依赖的 PR。
GitHub 堆叠式 pull request(stacked pull requests)将一个大型变更拆分为一条有序的小型 pull request 链:每个 PR 都以其下方 PR 的分支为目标,GitHub 会将整条链作为一个整体进行评审、变基(rebase)和合并。
大多数团队都熟悉另一种做法。你分三个相互依赖的部分开发一个功能,提交三个 PR,结果第二个 PR 不仅显示自身的改动,还连带显示了第一个 PR 的改动。评审者看不下去,或者你干脆把所有内容合并成一个巨型 PR,而没有人会认真读完它。
本文将完整介绍 GitHub 原生堆叠功能的工作方式:如何创建堆叠、如何进行评审、当堆叠中间的某个 PR 发生变更时会怎样,以及各种合并选项的具体效果。如果你需要先复习一下普通 PR 的用法,请参阅如何在终端中创建 pull request。
核心要点
- 在 GitHub 堆叠中,每个 pull request 的 base 都是其下方 PR 的分支,因此每个 diff 只包含该层的改动。
- GitHub 于 2026 年 7 月 30 日宣布堆叠式 pull request 进入公开预览阶段。该功能可能会有变动。
- 根据 GitHub 的快速入门指南,gh-stack CLI 扩展需要 GitHub CLI 2.90.0 或更高版本,以及 Git 2.20 或更高版本。
- 如果中间某一层发生变更,其上方的所有分支都会停留在过时的提交上。这些分支必须自下而上依次变基并强制推送。
- 合并最顶层的就绪 PR 时,它会连同其下方所有未合并的 PR 一次性合并。如果合并中途失败,已经合入的 PR 保持已合并状态,其余 PR 保持打开状态。每个 PR 都会按照堆叠 base 分支的规则进行检查。
依赖型 Pull Request 的问题
依赖型 pull request 之所以难以评审,是因为 GitHub 会将每个 PR 与其 base 分支进行比较。假设你依次提交了三个都以 main 为目标的 PR:auth-middleware、api-routes、ui-login。第二个 PR 的 diff 中同样包含所有中间件代码,第三个则包含全部内容。没有人能分辨出某个 PR 实际引入了哪些代码行。
在没有原生堆叠支持的情况下,开发者通常采用以下两种变通方式之一:要么把所有内容合并成一个大 PR,这会拖慢评审并掩盖错误;要么手动将每个 PR 指向其下方的分支,然后在每次发生变更时亲自对整条链进行变基和重新指定目标分支。
什么是 GitHub 堆叠式 PR?
GitHub 上的堆叠式 pull request 是一条有序链中的一环。每个 PR 以其下方 PR 的分支作为 base,因此其 diff 只显示该层所做的改动。GitHub 关于堆叠式 pull request 的概述中说明,最底层的 PR 以堆叠的主干(trunk)为目标,主干通常是 main,但也可以是任意分支,例如某个发布分支。堆叠中的所有分支必须位于同一个仓库中,因此堆叠无法跨 fork 使用。
上述三层示例在堆叠后如下所示:
main
└─ auth-middleware PR → main (diff: middleware only)
└─ api-routes PR → auth-middleware (diff: routes only)
└─ ui-login PR → api-routes (diff: login UI only)
其底层模型仍然是 Git 本身就能理解的分支与 base。GitHub 在此之上增加了追踪能力:它知道各 PR 的顺序,将它们关联起来,并作为一个整体进行操作。
使用 gh CLI 创建堆叠
gh-stack 扩展可以在终端中创建和管理堆叠。GitHub 的快速入门指南将 GitHub CLI 2.90.0 或更高版本以及 Git 2.20 或更高版本列为前置条件。CLI 命令参考和该扩展的 README 给出的最低要求较低,为 gh 2.0,因此以快速入门指南中的版本要求为准更为稳妥。此外,你还需要完成身份认证,并拥有该仓库的推送权限。如果你刚接触 gh,可以先阅读在终端中创建 pull request。
gh --version # 2.90.0 or later
git --version # 2.20 or later
gh auth login
gh extension install github/gh-stack
接下来,逐层构建堆叠:
gh stack init # prompts for the first branch name, e.g. auth-middleware
git add -A
git commit -m "Auth middleware"
gh stack add api-routes
git add -A
git commit -m "API routes"
gh stack add ui-login
git add -A
git commit -m "Login UI"
gh stack push # pushes branches only
gh stack submit # opens PRs with correct bases and links them
gh stack view # branches, PR links, statuses, latest commits
gh stack init 会为堆叠建立本地追踪并创建第一个分支。除非传入 --base,否则主干默认为仓库的默认分支。CLI 命令参考特别指出,push 只上传分支,从不涉及 pull request。submit 才是负责创建 PR、为每个 PR 设置正确的 base 分支并将它们组合成堆叠的命令。
你也可以使用 Web 界面。在 github.com 上,你可以通过创建一个以先前某个 PR 的分支为 base 的 PR,并选择 Create stack 来创建堆叠。要扩展现有堆叠,打开其中任意一个 PR,然后在页头中选择 Add to stack。如果你已打开的 PR 本身已经构成一条链,GitHub 会显示一个横幅,提示将它们转换为堆叠。堆叠可以在 GitHub 移动应用中使用,也可以由 Copilot 等编码智能体通过 gh-stack skill 使用。GitHub Desktop 暂不支持该功能。
逐层评审堆叠
评审者可以分别批准堆叠中的每一层。不同的评审者可以同时评审不同的层,而作者可以继续在上层进行开发。堆叠式 pull request 的主要优势在于评审针对的是小规模 diff,而且无需等到整个功能完成才开始评审。
打开堆叠中的任意 PR 时,你只会看到该层的 diff。合并框中包含一个堆叠图,列出链中的每个 PR 及其状态,页面顶部堆叠图标上的数字则表示当前打开的是哪一层。
小型分层还能在 main 上留下更有价值的历史记录。每个合入的提交都聚焦于单一改动,因此 git bisect 能够精确定位到具体的变更,而不是一次上千行的合并。
更新堆叠中间的 PR
当堆叠中间的某个分支有新提交合入时,其上方的所有分支仍然指向旧的提交。这些上层分支必须变基到更新后的层上并强制推送。在本例中,对 api-routes 的一次评审修复会使 ui-login 落后,而 auth-middleware 不受影响。
对于手动管理的分支链,你需要用原生 Git 亲自完成这些操作(变基的基础知识请参阅 10 个 Git 命令):
# plain Git, no gh-stack
git checkout ui-login
git rebase api-routes # resolve any conflicts
git push --force-with-lease
# repeat for every branch above, bottom to top
而使用 gh-stack 时,根据堆叠代码变更教程,这一过程可以简化为两条命令:
gh stack checkout api-routes
git add -A
git commit -m "Fix API routes"
gh stack rebase --upstack # rebases only the branches above api-routes
gh stack push # per-branch --force-with-lease
如果变基因冲突而中止,请修复冲突文件,执行 git add,然后运行 gh stack rebase --continue。如果想放弃,gh stack rebase --abort 会将所有分支恢复到变基之前的状态。
在 github.com 上,一旦堆叠不再是线性的,合并框中就会出现 Rebase stack 按钮。根据堆叠式 pull request 管理指南,点击该按钮会从底层开始将各分支重放到最新的主干上,并逐一强制推送。以这种方式在 GitHub 服务器上生成的提交不带签名,因此要求签名提交的仓库应改为在本地使用 gh stack rebase 进行变基。
变基会重写上层分支。如果某位团队成员检出了其中某个分支,他应当先停下来与远程仓库同步,然后再提交新的内容,否则他的下一次推送将与被重写的历史发生冲突。
合并堆叠
合并最顶层的就绪 pull request 时,它会连同其下方所有未合并的 PR 一次性合并。如果中途出现失败(例如意外的冲突),合并会在该 PR 处停止:其下方已合并的 PR 保留在 base 分支上,失败的 PR 及其上方所有 PR 保持打开状态。如果只合并较低的层,其上方的 PR 会保持打开状态,同时 GitHub 会对它们进行变基,并将其 base 改为堆叠的 base 分支。合并指南要求堆叠必须具有线性历史,才能合并其中的任何部分。
| 你合并的 PR | 合入 main 的内容 | 其余 PR 的情况 |
|---|---|---|
最顶层就绪 PR(ui-login) | 全部三个 PR | 不再有打开的 PR |
最底层 PR(auth-middleware) | 仅该 PR | 上层 PR 变基并将目标改为 main |
中间 PR(api-routes) | 该 PR 及其下方所有 PR | ui-login 保持打开并将目标改为 main |
GitHub 堆叠中间的 PR 无法单独合并,它总是会与其下方的 PR 一起合入。根据 GitHub 堆叠式 pull request 参考文档,堆叠支持合并提交(merge commit)、压缩合并(squash)和变基合并(rebase)。使用 squash 时,每个 PR 会各自压缩为一个提交,因此合并三个 PR 会在 main 上产生三个提交。
合并规则来自堆叠的 base 分支,而不是每个 PR 直接指向的分支。因此,分支保护规则和必需检查仍然决定着哪些内容能进入 main。堆叠同样支持合并队列(merge queue),队列会按正确顺序接收各 PR。2026 年 7 月 30 日的更新日志称合并队列支持将逐步推出,而 GitHub 的合并指南现已将合并队列描述为完全支持。
原生支持之前的堆叠方案
在 GitHub 原生支持堆叠之前,你需要借助独立工具来管理分支链。Graphite、spr 和 git-branchless 都是在 GitHub 自身的 pull request 模型之外处理这些繁琐的管理工作。
GitHub 原生堆叠式 pull request 能够与你现有的评审流程、必需检查和分支保护规则协同工作,因此团队无需再引入其他工具。对于那些仅仅为了解决依赖型 PR 难以阅读的问题而转向堆叠工具的团队来说,这消除了使用这些工具的主要理由。
总结
GitHub 堆叠是一条 pull request 链,其中每个 PR 都以其下方的分支为 base,由 GitHub 负责变基、重新指定目标分支和合并。由于每一层都很小,评审者能够认真阅读;而堆叠中间的变更也只需对其上方的分支进行变基。想要亲自尝试,请安装 github/gh-stack,并将你的下一个多部分功能拆分为三层。
常见问题
GitHub 堆叠式 pull request 支持自动合并(auto-merge)吗?
不支持。GitHub 的合并文档明确指出,堆叠式 pull request 不支持自动合并。通过 API 进行合并的工具需要针对堆叠使用异步合并 API,因为旧版 pull request 合并端点无法合并堆叠。如果希望在检查通过后自动合入堆叠,请改用合并队列:堆叠支持合并队列,且队列会按堆叠顺序接收各 PR。
当某个堆叠式 pull request 被移出合并队列时会发生什么?
堆叠中位于其上方的所有 pull request 也会一并离开队列。为了保持堆叠的完整性,合并队列允许合并组(merge group)超出其配置的最大规模,最多超出 50%。如果堆叠过大,超出了这一额外空间,GitHub 会将其拆分到多个连续的合并组中。
最底层的 pull request 合并后,如何更新本地堆叠?
运行 gh stack sync。它会一步完成以下操作:拉取最新变更、将主干前移、将剩余分支变基到主干之上、推送这些分支,并刷新每个 pull request 的状态。添加 --prune 可删除已合并 pull request 对应的本地分支。如果 sync 遇到冲突,它会将所有分支恢复原状,此时请运行 gh stack rebase 来解决冲突,然后运行 gh stack push。
如何对现有堆叠中的层进行重新排序、删除或重命名?
使用 gh stack modify,这是一个交互式终端界面,可以删除(drop)、折叠(fold)、插入(insert)、重新排序(reorder)和重命名(rename)分支。它要求工作区干净、历史呈线性、没有正在进行的变基,且没有排队等待合并的 pull request。在同一会话中,你不能既重新排序分支,又进行删除、折叠、插入或重命名操作。你的修改只有在保存后才会生效,而 gh stack modify --abort 可以将堆叠恢复到开始修改之前的状态。完成后,运行 gh stack submit 将更新同步到 GitHub。