Main Branch

Fundamentals first, always

文章

让 stack 里的每个 pull request 只承担一个审查决策

我在一个混乱的仓库里试了 GitHub 原生的 stacked pull requests——10 个陈旧 draft,一套坏掉的测试。这篇文章讲 stack 修复了什么、代价是什么,以及如何判断什么时候值得多开几条分支。

Andrea Griffiths 2 分钟阅读 🌐 Read in English
Pull Requests 代码审查 GitHub Stacked PRs 开发者工作流
Listen to article

Read in English → · Leer en español →

我想在一个够乱的地方试 stacked pull requests,才能知道它到底有没有用。

这个仓库有 10 个陈旧的 draft PR。两个是 dark mode 的两次不同尝试。另外两个是关于航班延误邮件的重复实现。npm test 直接跑不通,因为一个 Playwright 测试放在了 Jest 的目录里。

这就是这个仓库的开发历史。工作会暂停。上下文会漂移。你回来面对的是一个先需要形状、再需要更多代码的队列。

我关掉了那些 draft,但没删分支,然后把一个真实需求重建成三个 pull request:先修测试运行环境,再改 locale 行为,最后加浏览器覆盖率和 CI。

  • 拆分 Jest 和 Playwright,让 CI 给出有意义的信号。
  • 用当前 locale 设置文档语言。
  • 加浏览器覆盖率,并在 CI 里跑。

GitHub 把这份工作渲染成一个 3/3 的 stack。每一层都跑了 CI 和 CodeQL。有用的结果是一个我能解释的审查顺序:修好信号,改行为,再验证行为。

GitHub 的 merge 面板显示一个 3/3 的 stacked pull request 准备合并

每一层只给审查者一个问题:测试基础设施、locale 行为、浏览器 CI。GitHub 把三个 PR 显示成一个 3/3 stack。

这段体验改变了我对超大 PR 的看法。

审查者要扛的东西太多

一个 pull request 可以是正确的、有测试的,同时还是很难审查的。

你先做一个合理的改动。然后要更新 schema。然后是共享类型。然后是一个 API 接口。然后是一个 UI 组件。然后是测试。然后是一次重构,因为现有代码让新行为显得别扭。

几天后你有了一个 800 行的 PR,里面塞着六类不同的工作。

审查者得在脑子里把整个功能拼出来,才能说出任何有用的话。要理解模型、跟着后端行为走、检查 UI、验证测试,还要判断这次重构该不该跟改动一起进来。

SmartBear 的代码审查指南建议一次审 200 到 400 行代码。超过 400 行,缺陷检出率就会下降。

400 这个数字本身不是最关键的。真正的关键是:当审查者必须同时抓住一堆彼此无关的决策时,这就成了负担。

在这期间分支还在老化。main 一直在推进。反馈来得晚,而且经常一次一堆。作者接着要把审查改动和分支漂移拆开。大 PR 先制造了审查问题,才制造 Git 问题。

Stack 得配得上多开的分支

一个有用的 stack 会有一个可以解释清楚的依赖顺序。

一个大 diff 和一个三层 stack 的对比,stack 让每一层只承担一个审查决策

一个大 diff 让审查者一次做六个决策。一个 stack 让每一层只回答一个问题。

模型给了 API 一个真实的东西可以搭建。API 给了 UI 一个契约。审查者可以一次做一个决策,同时还能看清整个工作要往哪儿走。

一次 600 行的重命名仍然可以是一个连贯的 pull request。一个 120 行的改动如果同时动了 auth、持久化和 UI,就可能包含三个独立的决策。行数帮你注意到问题,决策告诉你在哪儿切分。

我在开 stack 之前会问自己四个问题:

  • 下层能不能独立安全落地,或者放在合适的 flag 后面?
  • 对下层的反馈会不会重塑上层的工作?
  • 审查者不用重建整个功能,也能评估这一层吗?
  • 分支顺序是否匹配真实的实现依赖?

如果四个问题都是 yes,多开的分支就配得上它们的位置。

看整个 stack,不是只看每个切口。三个各自能通过测试的层,加起来可能比一个成型的 PR 更耗审查成本。如果你到了四五层,回头看看这份工作。它要么真的有那么多独立决策,要么开分支本身成了目的。

下层也可能在上层继续堆积的时候卡住,让风险和协调压力都集中在 stack 的最底部。

当反馈动到了地基,重看它上面那一层。如果审查问题变了,就重整或重建那一层。

下层的改动仍然会以 rebase 和冲突解决的形式,向上面每一条分支传导。

有些工作必须放在一起。如果审查者需要同时看 UI 和 API 才能判断行为,那就让它们留在同一个 diff 里。目标是有用的审查,不是漂亮的分支图。

智能体会让超大 diff 来得更快

代码智能体让 feature 大小的 diff 变得很便宜。你的智能体很快。你的审查者是一个手里端着咖啡的人。

我见过智能体生成的 2000 行 pull request。里面可能有好东西。它同时也要求某人在同一时间信任一个模型、一个 API、一个 UI、一组测试覆盖率,以及一大堆生成出来的粘合代码。

在智能体开工之前先给它一个审查形状。让它先确认依赖顺序,先建地基,让每一层只承担一个审查者能回答的问题。

这会改变输出:从一条巨大的分支变成团队真正能讨论的工作。

原生 stack 让审查和检查保持在原位

依赖分支是 Git 的老玩意。真正难的一直是围绕它们的那些工作。

GitHub 原生的 stack 把策略连到整条链上。每个 PR 都是对着 stack 的最终底 base(通常是 main)来评估的,所以 required reviews、CODEOWNERS 和 checks 在整个工作往上叠的时候还继续生效。

合并 stack 最顶部的 PR,就能把整个 stack 一起落地。如果你只合并了一部分,GitHub 会自动 rebase 剩下的层。

有一个副作用:既然每个 PR 都拿 stack 的 base 来跑 checks,CI 的用量就随 stack 大小成倍增加。GitHub 在 workflow 表达式里暴露了 github.event.pull_request.stack.position.size,你可以让快速 check 在每一层都跑,把完整套件留给最顶层的 PR,或者留给还没合并的最底层 PR。API 负责接线,怎么用是你的 workflow 决策。

我在这个 demo 仓库里就想验证这件事。三层都跑了自己的 checks。stack 视图把依赖说清楚了。系统理解了这份工作的形状,而不是逼审查者从分支名和 commit 历史里去猜。

从决策分得开的地方开始

从两层开始。挑一个功能,它的地基和行为已经像是两个独立的审查话题了。先把地基开出来。让反馈在上层还没定型之前先到。

短工作流是这样的:

gh extension install github/gh-stack
gh stack init
gh stack add <branch>
gh stack submit

GitHub 的官方文档在 gh.io/stacks 讲了命令和智能体配置。等你准备开工再去看。

下一次做功能的时候,找到第一个可以独立审查的决策,早点把那个 PR 开出来。


本文由 Andrea Griffiths 撰写,AI 辅助翻译为中文。如有翻译问题,欢迎在 mainbranch-zh 仓库提交 PR。

关于作者: Andrea Griffiths 是 GitHub 的高级开发者倡导者,帮助工程团队采用和扩展开发者技术。她热衷于让技术概念对人类和 AI 代理都可访问。在 LinkedInGitHubTwitter/X 上与她联系。 · Read in English