@GergelyOrosz:实际上,关于堆叠差异(又称堆叠式拉取请求)这个概念,你需要了解的是,这篇深度文章写于……
摘要
Gergely Orosz 分享了 Pragmatic Engineer 关于堆叠差异的深度解析,解释了这一由 Meta 和 Google 推广的概念,以及 GitHub 新的公开预览如何使其更容易使用。
查看缓存全文
缓存时间: 2026/07/31 04:47
实际上,这就是你需要了解的关于堆叠式 diff(也就是堆叠式拉取请求)的概念。这篇深入分析是 3 年前在 @Pragmatic_Eng 上发布的。现在 GitHub 已经内置了公开预览功能,我们移除了付费墙。终于!! https://newsletter.pragmaticengineer.com/p/stacked-diffs
堆叠式 Diff(以及你为什么要了解它们)
来源:https://newsletter.pragmaticengineer.com/p/stacked-diffs 👋 你好,我是 Gergely,这是《Pragmatic Engineer Newsletter》中一篇之前仅限订阅者阅读的文章。在每一期中,我都会从工程经理和资深工程师的视角,探讨大型科技公司和初创公司面临的挑战。
这一期的付费墙在发布近 3 年后被移除——恰逢 GitHub 在公开评审中推出了堆叠式 diff 功能。(https://x.com/msdev/status/2082910135639474675?s=20) 如果你想保持行业领先,欢迎订阅这份每周通讯:
我在 Uber 担任开发人员期间,最大的个人“惊叹”时刻之一,就是开始使用堆叠式 diff 的时候。堆叠(Stacking)是指将一个功能对应的拉取请求(PR)拆分成多个更小、且相互依赖的 PR——因此得名“堆叠”。这听起来可能有些反直觉,但这种工作流程极其高效,因为它让 PR 的评审和修改变得更简单——也正如我们在 Uber 所称的“diff”。
堆叠的概念在谷歌已经存在了很久,但 Meta 是第一家将这种工作流程真正推广到公司之外的公司,它开源了内部开发者工具 Phabricator,该工具支持堆叠式 diff。直到今天,Meta 的工程师每天都离不开堆叠式 diff。
在今天的文章中,我联系了五位前 Meta 软件工程师,请他们分享对堆叠的见解。他们现在都是开发者工具初创公司 Graphite (https://graphite.dev/) 的联合创始人或早期员工,这家公司为所有使用 GitHub 的公司构建这种堆叠工作流程。我是 Graphite 的小投资者 (https://blog.pragmaticengineer.com/investing/)——因为在 Uber 亲身体验了堆叠对生产效率的巨大影响,并且很好奇为什么这种工具没有更广泛地普及。
今天的内容包括:
- 堆叠式 diff:概念
- Meta 如何使用堆叠式 diff
- 谷歌的堆叠方式:链式(chaining)
- Meta 和谷歌之外的堆叠实践
- Meta 的其他优秀开发者工具——包括我们一年前在《Inside Meta’s Engineering Culture》 (https://newsletter.pragmaticengineer.com/p/facebook-2) 中未涉及的一些
- Meta 开发者工具对行业的影响
- 在 Meta 工作与在 Graphite 这样的初创公司工作
如果你正在寻找支持这种工作流程的工具,以下是一些可以参考的:
- Graphite (https://graphite.dev/)(本篇文章的深入分析由该公司的工程师贡献)
- Gerrit (https://www.gerritcodereview.com/)(将这种方式称为基于补丁(patch-based)的方法)
- Sapling (https://sapling-scm.com/)
- ReviewStack (https://reviewstack.dev/)
堆叠是一个棘手的概念,实际操作比理解更容易。为了帮助大家理解,我们会分别解释和可视化这个过程。
Tomas Reimers (https://www.linkedin.com/in/tomasreimers/) 在 Meta 工作了 2.5 年,之后在三年前共同创立了 Graphite。以下是他对堆叠的解释:
“堆叠让你可以轻松创建许多小型 PR,并且无需等待评审。传统的 Git 工作流程将每个功能等同于一个独立的 PR。一个 PR 有自己的分支,由多个提交组成。传统的、非堆叠的工作流程是这样的:
- 完成一个功能
- 提交一个 PR
- 等待 PR 批准
- 批准后合并回主分支
“一旦 PR 被合并,你就想开始下一个功能。于是你再次从主分支分出分支,开始构建。即使是最顺畅的情况下,这个过程也会花费很长时间!最大的时间浪费是 PR 需要等待数小时或数天的评审。我们分析了 1500 万个拉取请求的历史数据,有些 PR 甚至等了几年才被合并!
“堆叠让变更的单位变成单个提交,而不是由多个提交组成的拉取请求。 通过堆叠,你可以将较大的更改拆分成许多较小的更改。这个更改——一个提交!——可以单独测试、评审、落地(landed)和回滚。
“这些较小的更改集合,也就是“堆栈”,可以一个接一个地持续构建,让工程师保持不被阻塞,持续推进。举一个实际例子:当你完成一个功能后,你通常希望利用这个已完成的来构建下一个功能。使用堆叠,你不需要等待第一个功能被批准并合并;你只需要继续工作!
“作为方法论,堆叠让开发者能够绕过主分支依赖的延迟,实现持续的并行开发。”
对于堆叠,可视化解释也很有帮助,以下是我尝试绘制的一张图。传统的拉取请求流程如下:
经典的 PR 流程:检出分支、进行更改、获得评审,然后合并回主分支。 高效的工程师往往倾向于创建足够小的 PR,原子化且易于评审。工程师如何创建一系列小型 PR?有两种选择:
- 创建一个 PR,等待评审通过,然后再创建下一个 PR。这很耗时!
- 创建一个 PR,然后在等待评审期间,从新分支创建第二个 PR,继续工作。
高效工程师最常用的方法是#2。当然,这会带来多一些上下文切换,但总比等着什么都不做要好,对吧?因此,当处理多个拉取请求时,典型流程是:
处理多个拉取请求时,更常见的工作流程是并行处理几个 PR,同时等待评论返回。这需要更多上下文切换,但总比等着什么都不做好! 但是,当 PR 之间相互依赖时,这种“传统”的拉取请求流程就行不通了。 只要 PR 完全独立,经典流程就足够好用,你只需要在本地切换不同的分支即可。
但是,如果你希望在 PR_2 中做出依赖于 PR_1 已合并到主分支的更改,你就会被阻塞。此时别无选择,只能等待 PR_1 获得批准,并催促评审者加快进度。
实际上,开发人员通常会往 PR_1 中添加更多提交,结果把它变成了一个巨大的代码更改。这并不理想,但没有其他办法,因为没有什么好方法能将一个 PR“拆分”成几个独立部分。堆叠式 diff 正是为解决这个问题而生的。
堆叠式 diff 的理念是,你可以继续在主分支上工作,评审的事情稍后再说。 你检出主分支并开始工作,进行一个小更改,并提交为diff——一个非常小的拉取请求。然后你继续工作,创建第二个 diff,第三个,以此类推。
你仍然需要评审才能将这些 diff 合并,但这可以稍后再做。因此,现在你的工作流程变成了这样:
使用堆叠式 diff 时的工作流程:创建 diff 后继续下一个代码更改 继续在当前分支上工作而不等待评审,可能感觉像是一个小改变。但在处理小型 diff 和小型 PR 时,这会带来巨大的效率提升。我们来看下两者的差异:
使用拉取请求与使用堆叠式 diff 的工作流程差异 堆叠式 diff 的真正好处在于,它能保护你的“编码连续性”,确保你在完成一个代码更改并转向下一个时,不会中断:
使用堆叠式 diff 能让开发者保持“心流”,专注于创建下一个 diff。评审可以稍后再说。 使用堆叠式 diff,每个提交(diff)都成为一次代码评审。 这就是堆叠式 diff 的核心思想。其他一切都只是工具层面的问题。
大多数开发人员处理的工作都很庞大且复杂,由于这种复杂性,要求工程师将拉取请求限制在最多 200 行变更是不合理的。有时候,一个完整的工作可能长达数百行,甚至数千行!
Diff——本质上就是提交——使得确保所有提交都保持较小的规模变得合理。一个改变 1000 行代码的大型工作可以拆分为三个独立的 diff,如下所示:
- 脚手架(800 行):“无聊”但冗长的部分。也许是自动生成的代码。
- 核心业务逻辑(150 行):对代码的关键更改。
- 边界情况(50 行):处理非核心代码之外的情况。
这样一来,评审这段代码并发现问题就变得更容易了。它还能帮助你作为开发者更好地组织工作!
理论上,堆叠式 diff 看起来是一个令人愉快的工作流程。然而,当主分支或前面的 diff 发生变化时,事情会变得稍微复杂一些。我们来看后一种情况:当评审者要求对第一个 diff 进行重大更改时。
在这个例子中,我们有三个相互依赖的堆叠 diff:
三个相互依赖的代码更改(diff) 当我们完成最后一个代码更改(diff)后,针对 Diff 1 的代码评审完成了。评审者发现问题,需要显著修改 Diff 1。那么:
Diff 1 需要修改,但我们已经完成了 Diff 2 和 Diff 3。现在该怎么办? 怎么办?如果没有包含更新后的 Diff 1 中的更改,Diff 2 和 Diff 3 就无法合并到主分支。这正是我们使用交互式变基 (https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History) 的原因——本质上就是重写本地git 历史。我们执行命令 git rebase -i。如果对 Diff 1 的修改导致与 Diff 2 发生冲突,我们就手动选择希望更新后的 Diff 2 包含的内容。对于 Diff 3,我们也同样操作:
我们使用交互式变基来变基 Diff 2 和 Diff 3。这可能很简单,也可能很复杂,具体取决于 Diff 1 所做的更改! 使用堆叠式 diff 通常意味着更频繁地对小 diff 进行变基。 当我在 Uber 开始使用基于 Phabricator 的堆叠式 diff 时,变基几乎成了每天的习惯。这不仅是因为链条中较早的 diff 常常会收到有意义的反馈并要求重大代码修改,还因为主分支经常变化!
不过,变基并不是堆叠式 diff 独有的:任何时候当很多工程师在同一个代码库区域工作时,变基都是一个痛点。堆叠式 diff 实际上让变基变得稍微容易一些,因为你需要合并的冲突往往更小,毕竟 diff 本身就是更小的变更。最危险的变基往往是合并一个改变数百或数千行代码、包含几十个冲突的分支。
熟练进行变基对任何软件工程师来说都是一项有用的技能。 处理堆叠式 diff 会迫使你掌握这个实践!我的补充建议是,当使用堆叠工作流程时,专门的堆叠工具——比如 Phabricator (http://phabricator.org/)(现在作为开源项目已经停止维护)、Graphite (https://graphite.dev/)、Gerrit (https://www.gerritcodereview.com/)(这种方式称为基于补丁的方法)、Sapling (https://sapling-scm.com/) 或 ReviewStack (https://reviewstack.dev/)——可以让变基比原生 git 流程稍微轻松一些。
Nick Yan (https://www.linkedin.com/in/nicholasyyan/) 在 Meta 工作了 4 年,之后加入 Graphite 担任工程师。2017 年他加入 Meta(当时还叫 Facebook)时,他惊讶地发现在入职培训(onboarding)的早期就讲到了堆叠式 diff:
“我们第一天就学习了堆叠式 diff。在加入 Facebook 之前,我在之前的实习和学校项目中用过 Git。显然,Meta 的代码库比我 COMP11 课程的作业要大得多!对于 Meta 的工程师来说,堆叠是唯一的编写和发布代码的方式,因此这也是 Bootcamp 最先讲的工程主题之一。堆叠在文化中如此核心,以至于我的经理经常告诉我,如果我不把代码拆分成更小的块,他们就不会接受。拆分较大的更改是很容易的。从第一天开始使用堆叠,我就不需要学习 git 的‘困难部分’。 我不需要担心依赖图、提交树或 cherry-pick 更改。当有很多工程师都在各自开发功能时,堆叠感觉是一种更简单、更适合快速变复杂的代码数据模型。我记得我的经理真的说过‘除非你拆分开,否则我不接受’。小的、堆叠的 PR 是文化规范,我从那以后一直以这种方式开发。”
Aryaman Naik (https://www.linkedin.com/in/aryamannaik/) 在 Meta 工作了近 5 年后加入 Graphite。他分享说,堆叠式 diff 是公司里“唯一的”工作方式:
“我说几乎 100% 的工程师一直在使用堆叠,一点也不夸张。这是一种文化规范,是你应该遵循的预期工作流程。堆叠如此普遍,以至于当我离开 Meta 时,我很震惊地发现堆叠是 Meta 和其他少数公司独有的概念。在 Meta 工作过的工程师不知道不堆叠是怎么做的。”
为什么在 Meta,堆叠被认为是更好的工作方式? Tomas Reimers (https://www.linkedin.com/in/tomasreimers/)——Graphite 联合创始人——在 Meta 做了两年半的移动产品基础设施。他说:
“从核心来看,堆叠让开发者能够保持快速开发,通过帮助他们避免因为等待代码评审而被阻塞。堆叠式 PR、堆叠式更改和堆叠式 diff 比拥有几个独立 PR 更好,因为得益于‘依赖变更’的概念,你需要等待的时间更少。假设你有一个‘子’功能依赖于尚未合并的工作;使用堆叠,你不需要等待‘父’功能落地,你可以在它评审的同时继续开发。而且,你可以将这种方式扩展到 5 个、10 个甚至 20 个拉取请求的栈中——我就做过!一旦你的工作准备就绪,你可以一次合并所有(待续)”
相似文章
堆叠式拉取请求现已公开预览
GitHub 现已推出堆叠式拉取请求的公开预览,允许开发者将大型变更拆分为更小、更易审查的 PR,这些 PR 可独立审查并一键合并。
@cognition: Devin 现在原生支持 @Github Stacked PRs ▪︎ 将大型变更拆分为更小、更易审查的差异 ▪︎ 处理…
Devin 现在原生支持 GitHub Stacked PRs,使开发者能够将大型变更拆分为更小的差异,跨栈修复评论,并自动变基下游变更。
接受混乱的 git 历史
一篇博客文章,讨论两种 git 工作流理念——频繁提交与谨慎变基——并解释为何在大型团队中,鉴于团队的韧性,混乱的历史是可以接受的,文中还提到了 GitHub 的新堆叠 PR 功能。
spr:GitHub上的堆叠拉取请求
spr 是一个 CLI 工具,可将 Git 分支上的每个提交转换为 GitHub 上的独立拉取请求,无需手动管理分支即可实现堆叠式 PR。
用这一个简单技巧让代码审查重新可行
文章提出使用堆叠分支(小型、顺序的拉取请求)来使审查AI生成的代码更易管理和高效,解决常见的大型、难以审查的差异问题。