持续交付2.0

想要一次修改十万个文件,该怎么做?答案就在本周三

Image

关注我,每天收获一个新技能!

本周三,我们来聊聊如何领导你的技术团队,走上快速发展的路。
点击上面的“预约”,和乔帮主面对对。

0

引子
本文作者是 Laurent Le Brun,是构建工具 Bazel(Google 内部称为 Blaze)的开发者。他讲述了一个代码格式化工具开发与推广过程中遇到的难题。
如果你正在从事 DevOps 基础设施相关工作,可能会遇到下面类似的问题:
1. 格式化工具本身 如何处理注释?如何分割行?如何保留现有的行分割?
2. 格式化工具如何确保不会破坏代码?
3. 如何对大量文件修改后,提交到代码库?

1

大牛召唤

2012 年 9 月,我当时是 Google 的一名初级工程师,负责Bazel(Google 的构建工具,内部称为 Blaze)的开发。

有一天,我的收件箱里收到了一封神秘的日历邀请。这是由美国的两名工程师发送的,我和我的团队负责人都收到了邀请。他们是 Rob Pike 和 Russ Cox。虽然我没有和他们合作过,但我通过他们的名声知道他们。

罗伯特·派克(Robert Pike,1956 年出生),是 Go 语言创始人,在贝尔实验室工作期间开发了 Plan 9 操作系统,他是当时的Unix团队的成员。

Russ Cox,也是 Go 语言的开发者,也参与了 Plan 9 操作系统的开发。

在会议期间,Rob 和 Russ 分享了他们雄心勃勃的计划:重新格式化 Google 代码库中的每个 Bazel BUILD 文件,并使用预提交脚本强制执行此格式。

2

代码格式化
这个工作本身还存在争议

当时,代码格式化程序还不那么常见。

Python 的 Black、Clang Format 或 Prettier 等流行工具还不存在。随处可见的格式化程序的主要示例是gofmt。

当 Russ 和 Rob 在 Go 团队工作时,他们希望在 Bazel 的 BUILD 文件中复制这一成功。

BUILD 文件的格式不一致,混合了各种缩进样式,没有标准指南。

复制粘贴的代码块通常很容易被发现,因为它们的格式很突出。

过去关于样式指南的讨论表明,工程师之间存在很多分歧。

之前编写格式化工具的尝试被采纳的程度很有限。它存在一些问题,很难被采用。

Russ 已经使用 Go 开发了该工具的新版本,名为 Buildifier。在某些情况下,编写格式化程序可能非常困难,但复杂性取决于以下几点:

  • 如何处理注释?Buildifier 将注释附加到语法树中的附近节点(数据结构不是超细粒度的,因此在某些情况下,Buildifier 会移动注释)。

  • 如何分割行?Buildifier 在一些硬编码情况下会分割行,但它并不关心行的长度。这是一个重大的简化。

  • 如何保留现有的行分割?Buildifier 在语法树中的几个特定位置保留了其中一些。

  • 我们需要部分格式化吗?Buildifier 总是重新格式化整个文件,而不仅仅是被修改的行。

由于设计选择,Buildifier 相当简单。实施看起来很有希望,但我们必须考虑如何推广它。Russ 在整个代码库上对其进行了测试,并能够在几分钟内重新格式化所有文件。他请求我们允许推广该工具并将其作为提交前检查来执行。

3

重新格式化每个 BUILD 文件
这个工作本身似乎很疯狂

重新格式化每个 BUILD 文件的想法听起来很疯狂,而且非常具有破坏性。每天大约有 10,000 名工程师在代码库中更改 BUILD 文件,强制执行严格的格式规则似乎很可怕。

1. 我们能否拒绝每个与输出字节不匹配的提交?

2. 我们能否放宽提交前规则或让工具选择加入?

Russ 坚持说:是的,必须对每个人都严格执行。没有例外,工具中不允许个人设置,没有个人偏好。

几天后,该计划获得正式批准。

4

推广

我在几个地方帮助了推广。

例如,我修改了语法以使构建语言形式化,并编写了样式指南——因此我一劳永逸地决定了 BUILD 文件应该是什么样子。

我还需要记录哪些列表可以安全地重新排序,因为 Buildifier 的一个显着功能是它能够在某些情况下对列表进行排序(例如,在构建中,源文件列表有时与顺序无关)。

当然,我们必须将 Buildifier 集成到每个主要的代码编辑器中。如果代码在保存时就被格式化,那么在尝试提交代码时就不会出现意外。

在代码库中,一些工具会生成 BUILD 文件。工具不应尝试自行生成格式化的代码;它们应该将其委托给格式化工具,格式化工具充当事实来源,并且可能会随时间而变化。

5

如何确保不会破坏代码

格式化程序如何确保不会破坏代码?你如何测试?


第一个想法是比较语法树,但当我们重新排序列表项时,这种方法不起作用。Bazel 有一个非常方便的功能,称为 Bazel Query,它可以输出有关包的信息。我说过 Bazel 哪些列表与顺序无关,因此我们可以使用 Bazel Query 来检查重新格式化是否不会影响 Bazel 对文件的理解。

最重要的是,Google 拥有良好的测试基础设施。当然,在这种更改之后运行测试是可能的(但速度很慢,并且计算成本很高)。

6

如何提交对十多万个文件的更改?

在这种规模下,许多工具都无法正常工作。而Google 有一个工具可以将大更改拆分为可以独立提交的小更改。

如果发生冲突,该工具会指示还原任何有冲突的文件。为了绕过不同目录有不同审批者,这些更改会被发送给“全局审批者”,该审批者能够在 Google 代码库的任何地方批准更改。谷歌大仓的根目录有一个作用于全局的 Owner 文件。类似于下面两篇引用的内容:

代码OWNERS 机制在代码仓库中的规则设置

Code Owner Rules

7

结果比较好

与我的预期相反,这件事相对平淡无奇。几乎没有人抱怨新的要求。几乎没有人抱怨新的格式风格。

我记得之前关于缩进、括号位置等的讨论非常冗长,没有达成任何共识。然而,当 Buildifier 推出时,人们实际上并不关心样式决定。他们只是喜欢统一性。

“格式化问题本不应该存在,我们非常关心这个问题,愿意花费自己的工程时间来消除它。我明白,自动格式化似乎不会带来重大改变。直到我们从 Go 代码审查和代码编辑流程中消除了它,我才真正理解它。希望在六个月或一年后,当一切都转换完毕,我们回顾这一点时,回想起来,它的好处会更加明显。”—— Russ Cox

那么,这值得吗?是的。这次经历让我认识到了统一性的价值以及自动化工具提高生产力的威力。

重新格式化的好处很快就显现出来了。

Buildifier 不仅重新格式化了我们的 BUILD 文件。它从根本上改变了我们的代码维护和大规模更改方法。


在使用 Buildifier 之前,对 BUILD 文件进行大规模更改非常困难。重构工具试图检测并匹配现有的格式样式,但它们无法很好地工作(特别是在存在代码注释和多行表达式的情况下),并且在代码审查中遇到了很多阻力。

使用 Buildifier,这种更改变得很常见。这使得 Bazel 中许多以前被认为不可能实现的增强功能成为可能。这使我们能够修复遗留的设计问题。

特别是,这使得我以后可以将 Bazel 构建语言从 Python 替换为Starlark。

那么,为什么不能不管旧文件,而只将其应用于新文件呢?因为这样一来,每当有人修改文件时,就会产生很长的差异,使得更改难以审查。这种成本将分摊给公司的所有工程师。

在迁移开始时文件数量实际上是 193k,迁移结束时文件数量是 216k。此类迁移必须同时处理代码库的增长。

8

更有趣的是
大家对这个故事的讨论

Image

Image

Image

地

原文链接:https://laurent.le-brun.eu/blog/the-story-of-reformatting-100k-files-at-google-in-2011