GitHub 已将 Stacked Pull Requests 推向公开预览,为一组存在依赖关系的 Pull Request 提供原生支持。它解决的不是“如何少写代码”,而是如何把大型变更拆成多个边界清楚、能够分别审查和合并的小改动,同时保留它们之间的依赖关系。
堆叠式 PR 解决什么问题
传统的大型 PR 往往混合数据库迁移、领域模型、服务接口、业务逻辑和前端适配。审查者需要一次理解全部上下文,作者也容易在持续修改中制造噪声。即使最终实现正确,审查成本仍然很高。
堆叠式 PR 会把一个大目标拆成依次建立在前序改动之上的多个分支。例如:
main
└── feature/schema
└── feature/repository
└── feature/api
这里可以对应三个 PR:
feature/schema相对main,只增加数据库结构。feature/repository相对feature/schema,增加数据访问代码。feature/api相对feature/repository,开放 HTTP 接口。
审查者可以先验证迁移脚本,再检查存储层,最后关注 API 行为。每个 PR 的讨论范围更窄,问题也更容易定位。GitHub 提供原生能力的价值,在于让这种依赖关系成为平台能识别和呈现的信息,而不再完全依赖 PR 描述、分支命名和团队口头约定。
“可以独立审查”不等于“可以随意合并”
堆叠中的 PR 可以分别接受审查,但它们仍有明确的先后关系。如果 API 分支依赖尚未进入主干的存储层代码,直接把 API PR 合入 main 可能导致冲突、缺失依赖或 CI 失败。
实际使用时需要区分三个概念:
- 审查独立性:每个 PR 只呈现当前层新增的改动。
- 逻辑依赖性:后续 PR 需要前序 PR 提供的类型、接口或行为。
- 合并顺序:通常从栈底向栈顶合并,并在前序 PR 合并后更新后续分支。
拆分边界也很关键。一个好的栈中,每一层应当有单一目的,并尽量保持可测试状态。不要为了压低 diff 行数,把一个不可运行的函数机械切成三个 PR;也不要建立十几层长栈,否则重定基、冲突处理和审查状态同步会迅速变复杂。
可以这样实践:用 Git 和 GitHub CLI 建立三层 PR 栈
下面的命令展示了一个不依赖特定预览界面的基础流程。运行前需要安装并登录 GitHub CLI,同时把仓库中的文件名和测试命令替换为项目实际内容。
# 从最新主干创建第一层:数据库结构
git switch main
git pull --ff-only
git switch -c feature/schema
mkdir -p db/migrations
printf '%s\n' 'CREATE TABLE widgets (id INTEGER PRIMARY KEY, name TEXT NOT NULL);' \
> db/migrations/001_create_widgets.sql
git add db/migrations/001_create_widgets.sql
git commit -m "Add widgets schema"
git push -u origin feature/schema
gh pr create \
--base main \
--head feature/schema \
--title "Add widgets schema" \
--body "Stack 1/3: introduces the database schema."
# 第二层建立在 schema 分支之上
git switch -c feature/repository
mkdir -p src
printf '%s\n' 'export const findWidget = (id) => ({ id, name: "demo" });' \
> src/widget-repository.js
git add src/widget-repository.js
git commit -m "Add widget repository"
git push -u origin feature/repository
gh pr create \
--base feature/schema \
--head feature/repository \
--title "Add widget repository" \
--body "Stack 2/3: depends on the widgets schema."
# 第三层建立在 repository 分支之上
git switch -c feature/api
printf '%s\n' \
'import { findWidget } from "./widget-repository.js";' \
'export const getWidget = (id) => findWidget(id);' \
> src/widget-api.js
git add src/widget-api.js
git commit -m "Add widget API"
git push -u origin feature/api
gh pr create \
--base feature/repository \
--head feature/api \
--title "Add widget API" \
--body "Stack 3/3: exposes repository behavior through the API layer."
这个示例的关键不是文件内容,而是每个 PR 的 --base:第二层以第一层分支为基线,第三层以第二层为基线。因此,审查第二个 PR 时不会重复展示第一层已经引入的全部差异。
第一层合入 main 后,可以这样更新剩余分支:
git fetch origin
git switch feature/repository
git rebase origin/main
git push --force-with-lease
git switch feature/api
git rebase feature/repository
git push --force-with-lease
这里应使用 --force-with-lease,避免无条件强推覆盖远端的新提交。团队还需要确认分支保护规则、CI 触发条件和平台预览功能的实际行为,再决定是否使用 rebase、更新 PR 基线或由原生界面完成栈的推进。
团队落地时应先约定规则
公开预览适合在低风险仓库或单个团队中试用,而不宜立即替换所有既有流程。预览功能的界面、限制和行为仍可能调整,自动化脚本也不应过早依赖尚未稳定的交互细节。
开始试用前,建议确认以下事项:
- 单个 PR 是否仍能独立构建和运行测试。
- 栈的推荐层数以及每层允许的变更规模。
- 谁负责在底层 PR 合并后更新后续分支。
- 审查批准在 rebase 后是否失效,是否需要重新审批。
- CI 是否会对不同基线正确计算差异并执行完整检查。
- 紧急回滚时,应回滚单层还是整个功能栈。
堆叠式 PR 最适合依赖顺序天然清晰的大型改动,例如数据库迁移到服务实现、底层重构到调用方迁移,或协议定义到客户端适配。它不能替代合理的模块边界,也不会自动消除跨分支冲突。真正的收益来自更小的审查单元、明确的依赖关系,以及团队对合并顺序的一致执行。