管理源代码分支的模式
现代源代码控制系统提供了强大的工具,可以轻松地在源代码中创建分支。但最终这些分支必须合并回来,许多团队花费了过多的时间来处理他们错综复杂的分支。有几种模式可以让团队有效地使用分支,集中精力整合多个开发人员的工作,并组织通往生产发布的路径。最重要的主题是,应该经常集成分支,并将精力集中在一个健康的主线上,以便以最小的努力将其部署到生产环境中。
2020 年 5 月 28 日
源代码是任何软件开发团队的重要资产,几十年来,已经开发出一套源代码管理工具来保持代码的良好状态。这些工具允许跟踪更改,因此我们可以重新创建以前版本的软件,并查看它如何随着时间的推移而发展。这些工具也是多个程序员团队协作的核心,所有程序员都在一个共同的代码库上工作。通过记录每个开发人员所做的更改,这些系统可以同时跟踪多条工作线,并帮助开发人员找出如何将这些工作线合并在一起。
将开发划分为分割和合并的工作线是软件开发团队工作流程的核心,并且已经发展出几种模式来帮助我们掌握所有这些活动。像大多数软件模式一样,其中很少有所有团队都应该遵循的黄金标准。软件开发工作流程非常依赖于上下文,特别是团队的社会结构和团队遵循的其他实践。
我在本文中的任务是讨论这些模式,我是在一篇文章的上下文中这样做的,在这篇文章中,我描述了这些模式,但穿插了模式解释和叙述部分,这些部分更好地解释了上下文和它们之间的相互关系。为了更容易区分它们,我用“✣”符号标识了模式部分。
基本模式
在思考这些模式时,我认为开发两个主要类别很有用。一组着眼于集成,即多个开发人员如何将他们的工作组合成一个连贯的整体。另一个着眼于通往生产的路径,使用分支来帮助管理从集成代码库到在生产中运行的产品的路径。有些模式是这两者的基础,我将把这些模式作为基本模式来处理。剩下几种模式既不基本,也不适合这两个主要组——所以我将把它们留到最后。
源代码分支
创建一个副本并记录对该副本的所有更改。
如果几个人在同一个代码库上工作,他们很快就会无法在同一个文件上工作。如果我想运行编译,而我的同事正在输入表达式,那么编译将失败。我们不得不互相喊叫:“我正在编译,不要改变任何东西”。即使只有两个人,这也难以维持,而对于一个更大的团队来说,这将是不可理解的。
对此的简单答案是,每个开发人员都获取代码库的副本。现在我们可以轻松地处理自己的功能,但出现了一个新问题:当我们完成后,如何将我们的两个副本合并回来?
源代码控制系统使此过程变得更加容易。关键是它将对每个分支所做的每次更改都记录为提交。这不仅可以确保没有人忘记他们对 `utils.java` 所做的小改动,记录更改还可以更容易地执行合并,尤其是在多个人更改了同一个文件时。
这导致了我将在本文中使用的分支定义。我将分支定义为对代码库的特定提交序列。分支的头部或尖端是该序列中的最新提交。
那是名词,但也有动词“分支”。我的意思是创建一个新分支,我们也可以将其视为将原始分支分成两个分支。当一个分支的提交应用于另一个分支时,分支就会合并。
我使用的“分支”定义与我观察到的大多数开发人员谈论它们的方式相对应。但是源代码控制系统倾向于以更特殊的方式使用“分支”。
我可以用一个现代开发团队中的常见情况来说明这一点,该团队将其源代码保存在共享的 git 存储库中。一位名叫 Scarlett 的开发人员需要进行一些更改,因此她克隆了该 git 存储库并检出了 master 分支。她进行了一些更改,并提交回她的 master 分支。同时,另一位开发人员,我们称她为 Violet,将存储库克隆到她的桌面上并检出 master 分支。Scarlett 和 Violet 是在同一个分支上工作还是在不同的分支上工作?他们都在“master”上工作,但他们的提交彼此独立,并且在将更改推回共享存储库时需要合并。如果 Scarlett 决定不确定她所做的更改,因此她标记了最后一次提交并将她的 master 分支重置为 origin/master(她从共享存储库克隆的最后一次提交),会发生什么情况?
根据我之前给出的分支定义,Scarlett 和 Violet 正在不同的分支上工作,彼此独立,也与共享存储库上的 master 分支独立。当 Scarlett 用标签搁置她的工作时,根据我的定义,它仍然是一个分支(她很可能认为它是一个分支),但在 git 的说法中,它是一条带标签的代码行。
使用像 git 这样的分布式版本控制系统,这意味着每当我们进一步克隆存储库时,我们也会获得额外的分支。如果 Scarlett 克隆她的本地存储库以将其放在笔记本电脑上以便回家路上使用,那么她就创建了第三个 master 分支。在 GitHub 中进行 fork 时也会出现同样的效果——每个 fork 的存储库都有自己的一组额外分支。
当我们遇到不同的版本控制系统时,这种术语上的混淆会变得更糟,因为它们对什么是分支都有自己的定义。Mercurial 中的分支与 git 中的分支完全不同,后者更接近于 Mercurial 的书签。Mercurial 还可以使用未命名的头部进行分支,并且 Mercurial 用户经常通过克隆存储库来进行分支。
所有这些术语上的混淆导致一些人避免使用这个术语。这里一个更有用的通用术语是代码线。我将代码线定义为代码库的特定版本序列。它可以以标签结束,可以是分支,也可以丢失在 git 的 reflog 中。您会注意到我的分支和代码线定义之间存在着惊人的相似之处。代码线在很多方面都是更有用的术语,我确实使用它,但在实践中它并没有被广泛使用。因此,在本文中,除非我是在 git(或其他工具)术语的特定上下文中,否则我将交替使用分支和代码线。
这种定义的结果是,无论您使用什么版本控制系统,每个开发人员在对其自己机器上的工作副本进行本地更改后,至少都有一条个人代码线。如果我克隆一个项目的 git 存储库,检出 master 分支,并更新一些文件——即使在我提交任何内容之前,这都是一条新的代码线。类似地,如果我创建 subversion 存储库主干的自己的工作副本,那么该工作副本就是它自己的代码线,即使没有涉及 subversion 分支。
何时使用它
一个古老的笑话说,如果你从高楼上掉下来,掉下来不会伤害你,但着陆会伤害你。源代码也是如此:分支很容易,合并更难。
记录提交时每次更改的源代码控制系统确实使合并过程更容易,但它们并没有使合并变得微不足道。如果 Scarlett 和 Violet 都更改了变量的名称,但更改为不同的名称,那么就会出现源代码管理系统无法在人工干预的情况下解决的冲突。更尴尬的是,这种文本冲突至少是源代码控制系统可以发现并提醒人们注意的东西。但通常情况下,冲突出现在文本合并没有问题的地方,但系统仍然无法工作。想象一下,Scarlett 更改了一个函数的名称,而 Violet 在她的分支中添加了一些代码,这些代码使用旧名称调用了这个函数。这就是我所说的语义冲突。当发生此类冲突时,系统可能会构建失败,或者可能会构建成功但在运行时失败。
任何使用过并发或分布式计算的人都熟悉这个问题。我们有一些共享状态(代码库),开发人员并行地进行更新。我们需要以某种方式将这些更改序列化为一些共识更新,从而将它们组合起来。我们的任务更加复杂,因为要使系统正确执行和运行,就意味着对该共享状态的有效性标准非常复杂。没有办法创建确定性算法来找到共识。人类需要找到共识,而这种共识可能涉及混合不同更新的选择部分。通常,只有通过原始更新来解决冲突才能达成共识。
我从以下问题开始:“如果没有分支会怎样”。每个人都会编辑实时代码,半生不熟的更改会破坏系统,人们会互相踩踏。因此,我们给了个人一种时间冻结的错觉,即他们是唯一改变系统的人,并且这些改变可以等到它们完全成熟后再冒险进行系统更改。但这只是一种错觉,最终要为此付出代价。谁来支付?什么时候支付?支付多少?这就是这些模式正在讨论的内容:为吹笛者付费的替代方案。
—— Kent Beck
因此,本文的其余部分,我将介绍各种模式,这些模式支持令人愉快的隔离和当你坠落时头发在风中飘扬的感觉,但最大限度地减少了与坚硬地面不可避免的接触的后果。
主线
一个单一的、共享的、充当产品当前状态的分支
主线是一个特殊的代码线,我们将其视为团队代码的当前状态。每当我想开始一项新的工作时,我都会从主线中提取代码到我的本地仓库中开始工作。每当我想与团队中的其他人共享我的工作时,我会用我的工作更新主线,最好使用我稍后将讨论的主线集成模式。
不同的团队对这个特殊分支使用不同的名称,这通常受到所使用的版本控制系统的约定的影响。git 用户通常称之为“master”,subversion 用户通常称之为“trunk”。
我必须在这里强调,主线是单一、共享的代码线。当人们在 git 中谈论“master”时,他们可能指的是几个不同的东西,因为每个仓库克隆都有自己的本地 master。通常,这样的团队都有一个中央仓库——一个共享仓库,它充当项目的单一记录点,并且是大多数克隆的来源。从头开始一项新的工作意味着克隆该中央仓库。如果我已经有一个克隆,我会先从中央仓库中拉取 master,以便它与主线保持同步。在这种情况下,主线是中央仓库中的 master 分支。
当我在开发我的功能时,我有我自己的个人开发分支,它可能是我本地的 master,或者我可以创建一个单独的本地分支。如果我在这上面工作了一段时间,我可以通过定期拉取主线的更改并将它们合并到我的个人开发分支中来保持与主线的同步。
类似地,如果我想创建一个新版本的产品来发布,我可以从当前的主线开始。如果我需要修复错误以使产品足够稳定以供发布,我可以使用发布分支。
何时使用它
我记得在 21 世纪初,我去和一位客户的构建工程师交谈。他的工作是组装团队正在开发的产品的构建。他会向团队中的每个成员发送一封电子邮件,然后他们会回复发送他们代码库中准备集成的各种文件。然后,他会将这些文件复制到他的集成树中,并尝试编译代码库。通常,他需要几周的时间才能创建一个可以编译并准备好进行某种形式测试的构建。
相比之下,使用主线,任何人都可以从主线的尖端快速启动产品的最新构建。此外,主线不仅可以更容易地查看代码库的状态,它还是我将在稍后探讨的许多其他模式的基础。
主线的另一种选择是发布列车。
健康分支
在每次提交时,执行自动检查,通常是构建和运行测试,以确保分支上没有缺陷
由于主线具有这种共享的、已批准的状态,因此保持其稳定状态非常重要。同样是在 21 世纪初,我记得我曾与另一个组织的一个团队交谈,该组织以每天构建其每种产品而闻名。这在当时被认为是一种相当先进的做法,该组织也因此受到称赞。在这些文章中没有提到的是,这些每日构建并不总是成功。事实上,发现团队的每日构建已经几个月没有编译成功的情况并不少见。
为了解决这个问题,我们可以努力保持分支的健康——这意味着它可以成功构建,并且软件运行时几乎没有错误。为了确保这一点,我发现编写自测试代码至关重要。这种开发实践意味着,在我们编写生产代码时,我们还会编写一套全面的自动化测试,以便我们可以确信,如果这些测试通过,那么代码中就没有错误。如果我们这样做,那么我们可以通过每次提交都运行一个构建来保持分支的健康,这个构建包括运行这个测试套件。如果系统编译失败,或者测试失败,那么我们的首要任务是在对该分支进行任何其他操作之前修复它们。这通常意味着我们“冻结”分支——除了修复以使其再次健康之外,不允许对其进行任何提交。
在提供足够信心以确保健康的测试程度上存在着一种张力。许多更彻底的测试需要很长时间才能运行,从而延迟了对提交是否健康的反馈。团队通过在部署管道上将测试分成多个阶段来处理这个问题。这些测试的第一阶段应该运行得很快,通常不超过十分钟,但仍然要相当全面。我将这样的套件称为提交套件(尽管它通常被称为“单元测试”,因为提交套件通常主要是单元测试)。
理想情况下,应该对每次提交都运行所有测试。但是,如果测试很慢,例如需要将服务器浸泡几个小时的性能测试,那就不切实际了。如今,团队通常可以构建一个可以在每次提交时运行的提交套件,并尽可能频繁地运行部署管道的后期阶段。
代码无错误运行不足以说明代码是好的。为了保持稳定的交付速度,我们需要保持代码的内部质量。一种流行的方法是使用集成前审查,尽管正如我们将看到的,还有其他选择。
何时使用它
每个团队都应该对开发工作流程中每个分支的健康状况有明确的标准。保持主线的健康状况具有巨大的价值。如果主线是健康的,那么开发人员可以通过简单地拉取当前的主线来开始一项新的工作,而不会被妨碍他们工作的缺陷所困扰。我们经常听到人们在开始一项新的工作之前,要花费数天的时间来修复或解决他们拉取的代码中的错误。
健康的主线也为生产铺平了道路。可以随时从主线的头部构建新的生产候选版本。最好的团队发现,他们不需要做太多工作来稳定这样的代码库,通常可以直接从主线发布到生产环境。
拥有健康主线的关键是自测试代码,其提交套件可以在几分钟内运行。构建这种能力可能是一项重大投资,但一旦我们能够在几分钟内确保我的提交没有破坏任何东西,这将彻底改变我们的整个开发流程。我们可以更快地进行更改,自信地重构我们的代码以保持其易用性,并大幅缩短从所需功能到在生产环境中运行代码的周期时间。
对于个人开发分支,明智的做法是保持它们的健康,因为这样可以启用差异调试。但是,这种愿望与频繁提交以检查当前状态的愿望背道而驰。如果我即将尝试不同的路径,即使编译失败,我也可以创建一个检查点。我解决这种紧张局势的方法是,一旦我完成了我的直接工作,就压缩掉任何不健康的提交。这样,我的分支上就不会有超过几个小时的不健康提交。
如果我保持我的个人分支的健康,这也使得提交到主线变得容易得多——我知道,主线集成中出现的任何错误都纯粹是由于集成问题,而不是仅仅由于我的代码库中的错误。这将使查找和修复它们变得更快更容易。
集成模式
分支是关于管理隔离和集成的相互作用。让每个人始终在一个共享的代码库上工作是行不通的,因为如果你正在输入变量名,我就无法编译程序。因此,至少在某种程度上,我们需要一个我可以工作一段时间的私有工作区的概念。现代源代码控制工具可以很容易地对分支进行分支和监控更改。然而,在某些时候,我们需要进行集成。考虑分支策略实际上就是决定如何以及何时进行集成。
主线集成
开发人员通过从主线拉取、合并以及(如果健康)推回主线来集成他们的工作
主线清晰地定义了团队软件的当前状态。使用主线最大的好处之一是它简化了集成。如果没有主线,那就是我上面描述的与团队中的每个人协调的复杂任务。然而,使用主线,每个开发人员都可以自己进行集成。
我将通过一个例子来说明这是如何工作的。一位我称之为 Scarlett 的开发人员通过将主线克隆到她自己的仓库中来开始一些工作。使用 git,如果她还没有中央仓库的克隆,她将克隆它并检出 master 分支。如果她已经有了克隆,她将从主线拉取到她的本地 master 中。然后,她可以在本地工作,将提交提交到她的本地 master 中。
当她工作时,她的同事 Violet 将一些更改推送到主线上。由于她在自己的代码线中工作,Scarlett 在处理自己的任务时可能没有注意到这些更改。
在某个时候,她到达了一个她想要集成的点。这的第一部分是将主线的当前状态获取到她的本地 master 分支中,这将拉入 Violet 的更改。当她在本地 master 上工作时,提交将显示在 origin/master 上,作为一个单独的代码线。
现在,她需要将她的更改与 Violet 的更改合并。有些团队喜欢通过合并来做到这一点,而另一些团队则喜欢通过变基来做到这一点。一般来说,人们在谈论将分支合并在一起时,无论他们是实际使用 git 合并还是变基操作,都会使用“合并”一词。我将遵循这种用法,所以除非我实际上在讨论合并和变基之间的区别,否则请将“合并”视为可以使用其中任何一种实现的逻辑任务。
关于是使用普通合并、使用还是避免快进合并,还是使用变基,还有很多其他的讨论。这超出了本文的范围,尽管如果人们给我发送足够的 Tripel Karmeliet,我可能会写一篇关于这个问题的文章。毕竟,如今“有来有往”才是王道。
如果 Scarlett 幸运的话,合并 Violet 的代码将是一个干净的合并,如果不是,她将不得不处理一些冲突。这些可能是文本冲突,其中大多数源代码控制系统可以自动处理。但是,语义冲突要难处理得多,这就是自测试代码非常方便的地方。(由于冲突可能会产生大量的工作,并且总是会带来大量工作的风险,我用一块令人担忧的黄色来标记它们。)
此时,Scarlett 需要验证合并后的代码是否满足主线的健康标准(假设主线是一个健康分支)。这通常意味着构建代码并运行构成主线提交套件的任何测试。即使这是一个干净的合并,她也需要这样做,因为即使是一个干净的合并也可能隐藏语义冲突。提交套件中的任何失败都应该纯粹是由于合并造成的,因为两个合并父级都应该是绿色的。知道这一点应该有助于她追踪问题,因为她可以查看差异以寻找线索。
通过这个构建和测试,她已经成功地将主线拉取到她的代码线中,但是——这一点很重要,而且经常被忽视——她还没有完成与主线的集成。要完成集成,她必须将她的更改推送到主线中。除非她这样做,否则团队中的其他人将与她的更改隔离——本质上是不集成。集成既是拉取又是推送——只有在 Scarlett 推送之后,她的工作才与项目的其余部分集成。
如今,许多团队要求在将提交添加到主线之前进行代码审查步骤——我称之为集成前审查的模式,稍后将讨论。
有时,在 Scarlett 可以推送她的代码之前,其他人会先将代码集成到主线。在这种情况下,她必须再次拉取并合并代码。通常,这只是偶尔出现的问题,无需进一步协调即可解决。我见过一些构建时间较长的团队使用集成令牌,以便只有持有令牌的开发人员才能进行集成。但近年来,随着构建时间的缩短,我很少听到这种做法。
何时使用它
顾名思义,只有当我们的产品也使用主线时,我才可以使用主线集成。
主线集成的另一种替代方法是从主线拉取代码,并将这些更改合并到个人开发分支中。这可能很有用 - 拉取代码至少可以让 Scarlett 了解其他人集成的更改,并检测她的工作与主线之间的冲突。但是,在 Scarlett 推送代码之前,Violet 无法检测到她正在进行的工作与 Scarlett 的更改之间的任何冲突。
当人们使用“集成”一词时,他们经常会忽略这个重点。我们经常听到有人说他们正在将主线集成到他们的分支中,而实际上他们只是在拉取代码。我已经学会了对此保持警惕,并进一步探究以检查他们是指简单的拉取还是真正的主线集成。这两者的后果截然不同,因此不要混淆这些术语非常重要。
另一种情况是,当 Scarlett 正在进行一些尚未准备好与团队其他成员完全集成的工作时,但它与 Violet 的工作重叠,并且她想与 Violet 分享。在这种情况下,他们可以打开一个协作分支。
特性分支
将一项特性的所有工作都放在其自己的分支上,并在特性完成后集成到主线中。
使用特性分支,开发人员在开始一项特性的工作时会打开一个分支,继续处理该特性直到完成,然后将其集成到主线中。
例如,让我们以 Scarlett 为例。她将负责添加本地销售税收集功能到他们的网站。她从当前稳定的产品版本开始,将主线拉取到她的本地存储库中,然后创建一个从当前主线尖端开始的新分支。她在该特性上工作所需的时间,对该本地分支进行一系列提交。
她可能会将该分支推送到项目存储库,以便其他人可以查看她的更改。
在她工作期间,其他提交会进入主线。因此,她可能会不时地从主线拉取代码,以便了解那里的任何更改是否可能会影响她的特性。
请注意,这与我上面描述的集成不同,因为她没有将代码推送到主线。此时,只有她能看到她的工作,其他人看不到。
有些团队希望确保所有代码(无论是否已集成)都保存在中央存储库中。在这种情况下,Scarlett 会将她的特性分支推送到中央存储库。这也将允许其他团队成员查看她的工作内容,即使它尚未集成到其他人的工作中。
当她完成该特性的工作后,她将执行主线集成,将该特性合并到产品中。
如果 Scarlett 同时处理多个特性,她将为每个特性打开一个单独的分支。
何时使用它
特性分支是当今行业中流行的模式。为了讨论何时使用它,我需要介绍它的主要替代方案 - 持续集成。但首先,我需要谈谈集成频率的作用。
集成频率
我们进行集成的频率对团队的运作方式有着非常强大的影响。《DevOps 状态报告》的研究表明,精英开发团队的集成频率明显高于表现不佳的团队 - 这一观察结果与我的经验以及我许多业界同行的经验相符。我将通过考虑 Scarlett 和 Violet 的两个集成频率示例来说明这一点。
低频集成
我将从低频情况开始。在这里,我们的两位主角通过将主线克隆到他们的分支中来开始一个工作单元,然后进行一些他们还不想推送的本地提交。
在他们工作时,其他人将提交推送到主线。(我无法快速想出另一个人的名字,也是一种颜色 - 也许是 Grayham?)
该团队的工作方式是维护一个健康的分支,并在每次提交后从主线拉取代码。Scarlett 在她的前两次提交中没有任何要拉取的内容,因为主线没有更改,但现在需要拉取 M1。
我用黄色框标记了合并。这次合并将提交 S1..3 与 M1 合并。很快,Violet 也需要做同样的事情。
此时,两位开发人员的代码都与主线保持同步,但他们还没有进行集成,因为他们彼此隔离。Scarlett 不知道 Violet 在 V1..3 中所做的任何更改。
Scarlett 又进行了几次本地提交,然后准备进行主线集成。这对她来说很容易推送,因为她之前拉取了 M1。
然而,Violet 的工作要复杂一些。当她进行主线集成时,她现在必须将 S1..5 与 V1..6 集成。
我已经根据涉及的提交数量科学地计算了合并的大小。但即使你忽略我脸颊上舌头的形状,你也会意识到 Violet 的合并最有可能很困难。
高频集成
在前面的示例中,我们的两位彩色开发人员在进行了一些本地提交后进行了集成。让我们看看如果他们在每次本地提交后都进行主线集成会发生什么。
第一个变化在 Violet 的第一次提交中就很明显,因为她立即进行了集成。由于主线没有更改,因此这是一个简单的推送。
Scarlett 的第一次提交也进行了主线集成,但由于 Violet 先完成了,因此她需要进行合并。但由于她只是将 V1 与 S1 合并,因此合并规模很小。
Scarlett 的下一次集成是一个简单的推送,这意味着 Violet 的下一次提交也需要与 Scarlett 的最新两次提交合并。然而,这仍然是一个相当小的合并,一个是 Violet 的,两个是 Scarlett 的。
当外部推送到主线出现时,它会在 Scarlett 和 Violet 的集成节奏中被接收。
虽然这与之前发生的情况类似,但集成规模更小。Scarlett 这次只需要将 S3 与 M1 集成,因为 S1 和 S2 已经在主线上了。这意味着 Grayham 在推送 M1 之前必须先集成主线上已有的内容 (S1..2, V1..2)。
开发人员继续进行剩余的工作,并在每次提交时进行集成。
比较集成频率
让我们再看一下这两个总体情况
低频
高频
这里有两个非常明显的区别。首先,顾名思义,高频集成有更多的集成 - 在这个简单的例子中是两倍。但更重要的是,这些集成比低频情况下的集成要小得多。较小的集成意味着工作量更少,因为可能导致冲突的代码更改更少。但比工作量更重要的是,风险也更低。大型合并的问题不在于它们所涉及的工作量,而在于工作的不确定性。大多数情况下,即使是大型合并也能顺利进行,但偶尔也会出现非常、*非常*糟糕的情况。这种偶尔的痛苦最终比经常性的痛苦更糟糕。如果我比较每次集成多花 10 分钟和 50 次中有一次要花 6 个小时来修复集成 - 我更喜欢哪种?如果我只看工作量,那么 50 次中有一次更好,因为它是 6 个小时而不是 8 小时 20 分钟。但不确定性使得 50 次中有一次的情况感觉更糟,这种不确定性会导致对集成的恐惧。
让我们从另一个角度来看看这些频率之间的差异。如果 Scarlett 和 Violet 在他们的第一次提交中就发生了冲突,会发生什么?他们什么时候会发现冲突的发生?在低频情况下,他们直到 Violet 最后一次合并时才会发现冲突,因为那是 S1 和 V1 第一次被放在一起。但在高频情况下,在 Scarlett 的第一次合并时就会检测到冲突。
低频
高频
频繁集成增加了合并的频率,但降低了合并的复杂性和风险。频繁集成还可以让团队更快地发现冲突。当然,这两件事是相互关联的。糟糕的合并通常是团队工作中潜伏的冲突的结果,只有在进行集成时才会浮出水面。
也许 Violet 正在查看账单计算,发现它包含了估算税,而作者假设了一种特定的税收机制。她的特性需要对税收进行不同的处理,因此直接的方法是从账单计算中删除税收,并在稍后将其作为单独的函数进行处理。账单计算只在几个地方被调用,因此很容易使用
自测试代码是我们的救命稻草。如果我们有一套强大的测试套件,将其作为健康分支的一部分使用将能够发现冲突,因此错误进入生产环境的可能性要小得多。但即使有强大的测试套件作为主线的守门人,大型集成也会让生活变得更加艰难。我们需要集成的代码越多,就越难找到错误。我们还更有可能遇到多个相互干扰的错误,这些错误更加难以理解。提交越小,我们不仅要查看的内容更少,还可以使用差异调试来帮助缩小引入问题的更改范围。
很多人没有意识到的是,源代码控制系统是一种沟通工具。它可以让 Scarlett 看到团队中的其他人正在做什么。通过频繁集成,她不仅可以在发生冲突时立即收到通知,还可以更清楚地了解每个人的工作内容以及代码库的演变过程。我们不再像个人那样各自为政,而是更像一个团队在共同努力。
提高集成频率是减少特性规模的一个重要原因,但还有其他优势。特性越小,构建速度就越快,投入生产的速度就越快,开始交付价值的速度就越快。此外,较小的特性可以缩短反馈时间,使团队能够在更多地了解客户的情况下做出更好的特性决策。
持续集成
开发人员一旦有可以共享的健康提交,就会立即进行主线集成,通常不到一天的工作量
一旦团队体验到高频集成既更高效又更轻松,那么自然要问的问题是“我们可以多频繁地进行集成?”特性分支意味着变更集的大小有一个下限 - 不能小于一个 cohesive 特性。
持续集成应用了不同的集成触发器 - 每当您在功能方面取得一定进展并且您的分支仍然健康时,您就可以进行集成。不要求功能完整,只要求对代码库进行了有价值的更改。经验法则是“每个人每天都提交到主线”,或者更准确地说:*您的本地存储库中永远不应该有超过一天的工作未集成*。在实践中,大多数持续集成实践者每天都会集成多次,他们乐于集成一小时或更短时间的工作量。
使用持续集成的开发人员需要习惯于使用部分构建的功能来达到频繁的集成点。他们需要考虑如何在不公开运行系统中部分构建的功能的情况下做到这一点。通常这很容易:如果我正在实现一个依赖优惠券代码的折扣算法,并且该代码尚未出现在有效列表中,那么即使我的代码已投入生产,它也不会被调用。同样,如果我正在添加一项功能,询问保险索赔人是否是吸烟者,我可以构建和测试代码背后的逻辑,并通过将提出该问题的 UI 留到构建功能的最后一天来确保它不会在生产中使用。通过最后连接关键接口来隐藏部分构建的功能通常是一种有效的技术。
如果无法轻松隐藏部分功能,我们可以使用功能标志。除了隐藏部分构建的功能外,此类标志还允许将功能选择性地公开给一部分用户 - 这对于新功能的缓慢推出通常很方便。
集成部分构建的功能尤其让那些担心主线中存在错误代码的人感到担忧。因此,使用持续集成的人还需要自测代码,以便确信在主线中包含部分构建的功能不会增加出现错误的可能性。使用这种方法,开发人员在编写部分构建的功能代码时为其编写测试,并将功能代码和测试一起提交到主线(可能使用测试驱动开发)。
就本地存储库而言,大多数使用持续集成的人都不会费心使用单独的本地分支来工作。通常可以直接提交到本地主服务器并在完成后执行主线集成。但是,如果开发人员愿意,在功能分支中打开并在那里完成工作,然后定期集成回本地主服务器和主线是完全可以的。功能分支和持续集成之间的区别不在于是否存在功能分支,而在于开发人员何时与主线集成。
何时使用它
持续集成是功能分支的替代方案。两者之间的权衡非常复杂,值得在本文的单独部分中进行讨论,现在是时候解决它了。
比较功能分支和持续集成
功能分支似乎是目前业界最常见的分支策略,但也有一群直言不讳的从业者认为持续集成通常是一种更好的方法。持续集成提供的关键优势在于它支持更高(通常高得多)的集成频率。
集成频率的差异取决于团队能够使功能的规模有多小。如果团队的功能都可以在一天内完成,那么他们可以同时执行功能分支和持续集成。但大多数团队的功能长度都比这更长 - 功能长度越大,两种模式之间的差异就越大。
正如我已经指出的那样,更高的集成频率会导致更少的集成参与和更少的集成恐惧。这通常是一件很难沟通的事情。如果您生活在一个每隔几周或几个月集成一次的世界中,那么集成很可能是一项非常令人担忧的活动。很难相信这是每天可以做很多次的事情。但集成就是这样一件事,频率降低了难度。这是一个违反直觉的概念 - “如果它很痛 - 那就更频繁地做”。但是集成越小,它们就越不可能变成一场史诗般的痛苦和绝望的合并。对于功能分支,这支持更小的功能:几天而不是几周(几个月是正确的)。
持续集成允许团队获得高频集成的优势,同时将功能长度与集成频率分离。如果团队更喜欢一周或两周的功能长度,持续集成允许他们在仍然获得最高集成频率的所有好处的同时做到这一点。合并规模更小,需要更少的工作来处理。更重要的是,正如我上面解释的那样,更频繁地执行合并可以降低出现糟糕合并的风险,这既可以消除由此带来的糟糕意外,又可以减少对合并的总体恐惧。如果代码中出现冲突,高频集成会在它们导致那些令人讨厌的集成问题之前快速发现它们。这些好处非常强大,以至于有些团队的功能只需要几天时间就可以完成持续集成。
持续集成的明显缺点是它缺乏对主线进行高潮集成的封闭性。这不仅是一个失去的庆祝活动,而且如果团队不擅长保持健康的分支,这也是一个风险。将功能的所有提交保存在一起还可以根据即将发布的版本是否包含某个功能做出延迟决定。虽然功能标志允许从用户的角度打开或关闭功能,但功能的代码仍在产品中。对此的担忧通常被夸大了,毕竟代码没有任何重量,但这确实意味着想要进行持续集成的团队必须制定严格的测试方案,以便他们可以确信即使每天进行多次集成,主线也能保持健康。有些团队发现这种技能难以想象,但另一些团队发现它既可行又解放。这个先决条件意味着功能分支更适合那些不强制要求健康分支并且需要发布分支在发布之前稳定代码的团队。
虽然合并的规模和不确定性是功能分支最明显的问题,但它最大的问题可能是它会阻碍重构。重构在定期且几乎没有摩擦的情况下进行时最为有效。重构会引入冲突,如果这些冲突没有被及时发现和解决,合并就会变得令人担忧。因此,重构在高频集成中效果最佳,因此它作为极限编程的一部分而流行起来也就不足为奇了,极限编程也将持续集成作为其原始实践之一。功能分支还 discourage 开发人员进行不被视为正在构建的功能的一部分的更改,这破坏了重构稳步改进代码库的能力。
我们发现,在合并到主干之前,拥有生命周期非常短(不到一天)的分支或分支,以及总共不超过三个活动分支,是持续交付的重要方面,并且都有助于提高性能。每天将代码合并到主干或主服务器中也是如此。
-- 2016 年 DevOps 状态报告
当我遇到软件开发实践的科学研究时,我通常仍然 unconvinced,因为他们的方法存在严重问题。一个例外是DevOps 状态报告,该报告制定了软件交付性能指标,他们将其与更广泛的组织绩效衡量标准相关联,而组织绩效衡量标准又与投资回报率和盈利能力等业务指标相关联。2016 年,他们首次评估了持续集成,发现它有助于提高软件开发性能,这一发现在此后的每次调查中都得到了重复。
使用持续集成并不能消除保持功能较小的其他优势。频繁发布小功能提供了一个快速的反馈周期,这对于改进产品来说非常有效。许多使用持续集成的团队也努力构建产品薄片并尽可能频繁地发布新功能。
特性分支
- 可以将功能中的所有代码作为一个单元进行质量评估
- 仅当功能完成时才将功能代码添加到产品中
- 合并频率降低
持续集成
- 支持比功能长度更高的频率集成
- 减少查找冲突的时间
- 更小的合并
- 鼓励重构
- 需要承诺健康的分支(以及自测代码)
- 科学证据表明它有助于提高软件交付性能
功能分支和开源
许多人将功能分支的流行归因于 GitHub 和起源于开源开发的拉取请求模型。鉴于此,有必要了解开源工作与许多商业软件开发之间存在的截然不同的环境。开源项目的结构多种多样,但常见的结构是由一个人或一小群人充当维护者的角色,负责大部分编程工作。维护者与更大的程序员群体(即贡献者)一起工作。维护者通常不认识贡献者,因此不了解他们贡献的代码的质量。维护者也不确定贡献者实际投入了多少时间,更不用说他们完成工作的效率了。
在这种情况下,功能分支很有意义。如果有人要添加一个功能(无论大小),而我不知道它何时(或是否)完成,那么等到它完成后再进行集成是有意义的。能够审查代码以确保它通过我对代码库的任何质量标准也更加重要。
但许多商业软件团队的工作环境却截然不同。有一个全职人员团队,所有人员都对软件做出了大量(通常是全职)的贡献。项目负责人非常了解这些人(除了他们刚开始工作时),并且可以对代码质量和交付能力抱有可靠的期望。由于他们是受薪员工,因此领导者还可以更好地控制投入项目的时间以及编码标准和团队习惯等方面。
鉴于这种截然不同的环境,很明显,此类商业团队的分支策略不需要与开源世界中使用的策略相同。对于偶尔为开源工作做出贡献的人来说,持续集成几乎是不可能的,但对于商业工作来说,这是一个现实的选择。团队不应该想当然地认为,对开源环境有效的方法在他们不同的环境中也一定是正确的。
集成前审查
在接受提交之前,对主线的每次提交都要进行同行评审。
长期以来,代码审查一直被鼓励作为一种提高代码质量、改进模块化、可读性和消除缺陷的方法。尽管如此,商业组织经常发现很难将其融入软件开发工作流程中。然而,开源世界广泛接受了这样一种理念,即在接受对项目的贡献之前应该对其进行审查,并且这种方法近年来在开发组织中得到了广泛的传播,尤其是在硅谷。像这样的工作流程特别适合 GitHub 的拉取请求机制。
当 Scarlett 完成了她希望集成的一大块工作时,这样的工作流程就开始了。就像她在成功构建后进行主线集成一样(假设她的团队是这样做的),在她推送到主线之前,她会将她的提交发送出去进行审查。然后,团队中的其他成员,比如 Violet,会对提交进行代码审查。如果她对提交有问题,她会提出一些意见,然后双方会来回讨论,直到 Scarlett 和 Violet 都满意为止。只有当他们完成后,提交才会被放到主线上。
集成前审查随着开源的兴起而流行起来,它非常适合于由忠诚的维护者和偶尔的贡献者组成的组织模式。它们允许维护者密切关注任何贡献。它们也与功能分支很好地结合在一起,因为完成的功能标志着进行此类代码审查的明确点。如果您不确定贡献者是否会完成一项功能,为什么要审查他们的部分工作?最好等到功能完成。这种做法也在大型互联网公司中广泛传播,谷歌和 Facebook 都构建了特殊的工具来帮助这项工作顺利进行。
培养及时进行集成前审查的纪律非常重要。如果开发人员完成了一些工作,然后又去做其他事情几天,那么当审查意见回来时,这项工作就不再是他们脑海中的首要任务了。对于已完成的功能来说,这令人沮丧,但对于部分完成的功能来说,情况要糟糕得多,因为在审查得到确认之前,可能很难取得进一步的进展。原则上,可以使用持续集成进行集成前审查,实际上也是可能的——谷歌遵循这种方法。但是,尽管这是可能的,但它很困难,而且相对少见。集成前审查和功能分支是更常见的组合。
何时使用它
将开源软件和私有软件开发团队的需求混为一谈,就像当前软件开发仪式的原罪一样
尽管集成前审查在过去十年中已成为一种流行的做法,但它也存在缺点和替代方案。即使做得很好,集成前审查总是会在集成过程中引入一些延迟,从而鼓励降低集成频率。结对编程提供了一个持续的代码审查过程,其反馈周期比等待代码审查要快。(与持续集成和重构一样,它是极限编程的原始实践之一)。
许多使用集成前审查的团队并没有足够快地完成审查。然后,他们可以提供的宝贵反馈来得太晚,无法发挥作用。在这一点上,有一个尴尬的选择,要么进行大量的返工,要么接受一些可能有效但会损害代码库质量的东西。
代码审查并不局限于代码进入主线之前。许多技术领导者发现,在提交之后审查代码很有用,当他们发现问题时,可以与开发人员进行沟通。重构文化在这里很有价值。如果做得好,这将建立一个社区,团队中的每个人都定期审查代码库并修复他们看到的问题——我称之为精炼代码审查的做法
围绕集成前审查的权衡主要取决于团队的社会结构。正如我已经提到的,开源项目通常具有一些受信任的维护者和许多不受信任的贡献者的结构。商业团队通常都是全职的,但也可能有类似的结构。项目负责人(如维护者)信任一小部分(可能是单一的)维护者,并且对团队其他成员贡献的代码持谨慎态度。团队成员可能会被同时分配到多个项目中,这使得他们更像是开源贡献者。如果存在这样的社会结构,那么集成前审查和功能分支就非常有意义。但是,信任度更高的团队通常会发现其他机制可以在不增加集成过程摩擦的情况下保持代码质量。
因此,虽然集成前审查可能是一种有价值的做法,但它绝不是获得健康代码库的必要途径,特别是如果您希望建立一个不完全依赖其初始领导者的均衡团队。
集成摩擦
拉取请求增加了处理低信任情况的开销,例如,允许您不认识的人为您的项目做出贡献。
对您自己团队中的开发人员强制执行拉取请求,就像让您的家人通过机场安检才能进入您的家一样。
-- Kief Morris
集成前审查的问题之一是,它通常会使集成变得更加麻烦。这是一个集成摩擦的例子——使集成需要时间或需要努力才能完成的活动。集成摩擦越多,开发人员就越倾向于降低集成频率。想象一下,某个(功能失调的)组织坚持要求对主线的每次提交都需要填写一份需要半小时才能填好的表格。这样的制度会 discourage 人们频繁地进行集成。无论您对功能分支和持续集成的态度如何,检查任何增加这种摩擦的因素都是有价值的。除非它能明显增加价值,否则应消除任何此类摩擦。
手动流程是这里摩擦的常见来源,尤其是在涉及与不同组织协调的情况下。这种摩擦通常可以通过使用自动化流程、改进开发人员教育(以消除需求)以及将步骤推迟到部署管道或生产环境中的质量保证的后期步骤来减少。您可以在有关持续集成和持续交付的材料中找到更多消除此类摩擦的想法。这种摩擦也会出现在生产过程中,并遇到相同的困难和处理方法。
让人们不愿考虑持续集成的原因之一是,如果他们只在集成摩擦很大的环境中工作过。如果进行一次集成需要一个小时,那么很明显,每天进行几次集成是荒谬的。加入一个集成不是什么大事的团队,有人可以在几分钟内完成集成,感觉就像一个不同的世界。我怀疑关于功能分支和持续集成优缺点的很多争论都被混淆了,因为人们没有同时经历过这两个世界,因此无法完全理解这两种观点。
文化因素会影响集成摩擦——尤其是团队成员之间的信任。如果我是一名团队负责人,并且我不相信我的同事能够做好工作,那么我可能会想要阻止那些会破坏代码库的提交。自然,这是集成前审查的驱动因素之一。但是,如果我所在的团队中,我相信同事的判断,那么我可能会更愿意接受提交后审查,或者完全取消审查,并依靠定期精炼审查来解决任何问题。我在这种环境中的收获是消除了预提交审查带来的摩擦,从而鼓励更高频率的集成。通常,团队信任是功能分支与持续集成之争中最关键的因素。
Rouan Wilsenach 的Ship/Show/Ask是一种有趣的方法,它在需要时保留了集成前审查,但鼓励采用摩擦较小的路径。这将更改分为 Ship(集成到主线)、Show(集成到主线,但打开拉取请求以传达和讨论更改)或 Ask(打开拉取请求以进行集成前审查)。
模块化的重要性
大多数关心软件架构的人都强调模块化对行为良好的系统的重要性。如果我面临着对模块化程度低的系统进行小的更改,我必须了解几乎所有内容,因为即使是很小的更改也会波及到代码库的许多部分。然而,如果模块化程度高,我只需要了解一两个模块中的代码,以及另外几个模块的接口,而可以忽略其余部分。这种减少我需要理解的工作量的能力就是为什么随着系统的增长,在模块化上投入如此多的精力是值得的。
模块化也会影响集成。如果一个系统具有良好的模块,那么大多数情况下,Scarlett 和 Violet 将在代码库中完全不同的部分工作,并且他们的更改不会导致冲突。良好的模块化还增强了关键接口和抽象分支等技术,以避免对分支提供的隔离的需求。通常,团队被迫使用源代码分支,因为缺乏模块化使他们没有其他选择。
功能分支是一种糟糕的模块化架构,它没有构建能够在运行时/部署时轻松交换功能的系统,而是将自己耦合到源代码控制,通过手动合并提供这种机制。
-- Dan Bodart
支持是双向的。尽管进行了很多尝试,但在我们开始编程之前,构建一个良好的模块化架构仍然极其困难。为了实现模块化,我们需要在系统增长的过程中不断地观察它,并使其朝着更加模块化的方向发展。重构是实现这一目标的关键,而重构需要高频率的集成。因此,模块化和快速集成在健康的代码库中相互支持。
总而言之,模块化虽然难以实现,但值得付出努力。这项工作涉及良好的开发实践、学习设计模式以及从代码库的经验中学习。混乱的合并不应该仅仅因为可以理解的想要忘记它们的愿望而被关闭——相反,要问为什么合并会混乱。这些答案通常是关于如何改进模块化的重要线索,从而改进代码库的健康状况,从而提高团队的生产力。
我对集成模式的个人看法
我作为一名作家的目的不是说服您遵循特定的道路,而是让您了解应该考虑的因素,因为是*您*决定要遵循哪条道路。尽管如此,我还是会在这里加上我对我之前指出的模式中更喜欢的模式的看法。
总的来说,我更喜欢在使用持续集成的团队中工作。我认识到环境是关键,在很多情况下,持续集成并不是最佳选择——但我的反应是努力改变这种环境。我之所以有这种偏好,是因为我希望身处一个每个人都可以轻松地不断重构代码库、改进其模块化、保持其健康的环境中——所有这些都是为了让我们能够快速响应不断变化的业务需求。
如今,我更像是一名作家,而不是一名开发人员,但我仍然选择在Thoughtworks工作,这家公司里充满了喜欢这种工作方式的人。这是因为我相信这种极限编程风格是我们开发软件最有效的方式之一,我希望看到团队进一步发展这种方法,以提高我们行业的效率。
从主线到生产发布的路径
主线是一个活跃的分支,定期会有新的和修改后的代码加入。保持它的健康非常重要,这样当人们开始新的工作时,他们是从一个稳定的基础开始的。如果它足够健康,你也可以直接从主线发布代码到生产环境。
这种将主线保持在始终可发布状态的理念是持续交付的核心原则。为此,必须具备将主线维护为健康分支的决心和技能,通常需要部署流水线来支持所需的密集测试。
以这种方式工作的团队通常可以通过在每个发布版本上使用标签来跟踪他们的发布。但是不使用持续交付的团队需要另一种方法。
发布分支
一个只接受为稳定准备发布的产品版本而提交的分支。
一个典型的发布分支将从当前主线复制,但不允许添加任何新功能。主要开发团队继续向主线添加此类功能,这些功能将在未来的版本中获得。在发布分支上工作的开发人员只专注于消除任何阻止发布到生产环境的缺陷。对这些缺陷的任何修复都在发布分支上创建并合并到主线。一旦没有更多需要处理的错误,该分支就可以进行生产发布了。
尽管发布分支上修复工作的范围(希望)比新功能代码要小,但随着时间的推移,将它们合并回主线变得越来越困难。分支不可避免地会发生分歧,因此随着越来越多的提交修改主线,将发布分支合并到主线变得更加困难。
以这种方式将提交应用于发布分支的一个问题是,很容易忽略将它们复制到主线,特别是当由于分支分歧而变得更加困难时。由此产生的回归是非常尴尬的。因此,有些人倾向于在主线上创建提交,并且只有在它们在那里工作后才将它们挑选到发布分支中。
**挑选**是指将提交从一个分支复制到另一个分支,但分支没有合并。也就是说,只复制了一个提交,而不是自分支点以来的先前提交。在这个例子中,如果我将 F1 合并到发布分支中,那么这将包括 M4 和 M5。但是挑选只获取 F1。挑选可能无法干净地应用于发布分支,因为它可能依赖于在 M4 和 M5 中所做的更改。
在主线上编写发布修复的缺点是,许多团队发现这样做更难,并且在主线上以一种方式修复它并在发布之前必须在发布分支上重新工作是令人沮丧的。当存在发布时间的进度压力时,尤其如此。
一次只在生产环境中使用一个版本的团队只需要一个发布分支,但有些产品会在生产环境中使用多个版本。运行在客户设备上的软件只有在客户希望升级时才会升级。许多客户不愿意升级,除非他们有引人注目的新功能,因为他们已经被失败的升级所困扰。然而,这些客户仍然希望修复错误,特别是如果这些错误涉及安全问题。在这种情况下,开发团队会为仍在使用的每个版本保留发布分支,并根据需要对其应用修复程序。
随着开发的进行,将修复程序应用于旧版本变得越来越困难,但这通常是做生意的成本。只有鼓励客户经常升级到最新版本才能缓解这种情况。保持产品的稳定性对此至关重要,一旦被烧伤,客户将不愿意再次进行不必要的升级。
(我听到的其他发布分支术语包括:“发布准备分支”、“稳定分支”、“候选分支”和“强化分支”。但“发布分支”似乎是最常见的。)
何时使用它
当团队无法保持其主线处于健康状态时,发布分支是一个有价值的工具。它允许团队的一部分人专注于产品生产就绪所需的必要错误修复。测试人员可以从该分支的尖端获取最新的稳定候选版本。每个人都可以看到为稳定产品所做的工作。
尽管发布分支很有价值,但大多数最好的团队不会对单生产环境产品使用这种模式,因为他们不需要这样做。如果主线保持足够健康,那么对主线的任何提交都可以直接发布。在这种情况下,发布版本应该用公开可见的版本号和构建号进行标记。
你可能已经注意到我在上一段中加入了笨拙的形容词“单生产环境”。这是因为当团队需要在生产环境中管理多个版本时,这种模式变得至关重要。
当发布过程中存在重大摩擦时(例如,必须批准所有生产发布的发布委员会),发布分支也可能很方便。正如 Chris Oldwood 所说,“在这些情况下,发布分支更像是一个隔离区,而企业齿轮缓慢地转动”。一般来说,应该尽可能地从发布过程中消除这种摩擦,就像我们需要消除集成摩擦一样。然而,在某些情况下,例如移动应用商店,这可能是不可能的。在许多情况下,大多数情况下,一个标签就足够了,并且只有在源代码需要进行一些必要的更改时才打开分支。
成熟度分支
一个分支,其头部标记着代码库成熟度级别的最新版本。
团队通常想知道源代码的最新版本是什么,这一事实可能会因具有不同成熟度级别的代码库而变得复杂。QA 工程师可能希望查看产品的最新 staging 版本,调试生产故障的人员希望查看最新的生产版本。
成熟度分支提供了一种执行此跟踪的方法。这个想法是,一旦代码库的某个版本达到一定程度的准备就绪,就将其复制到特定的分支中。
考虑一个用于生产环境的成熟度分支。当我们准备发布生产版本时,我们会打开一个发布分支来稳定产品。一旦准备就绪,我们将其复制到一个长期运行的生产分支。我认为这是一种复制而不是合并,因为我们希望生产代码与在上游分支上测试的代码完全相同。
成熟度分支的吸引力之一是,它清楚地显示了发布工作流程中达到该阶段的代码的每个版本。因此,在上面的示例中,我们只希望在生产分支上进行一次提交,该提交结合了提交 M1-3 和 F1-2。有一些 SCM-jiggery-pokery 可以实现这一点,但在任何情况下,这都会丢失与主线上细粒度提交的链接。这些提交应该记录在提交消息中,以帮助人们以后跟踪它们。
成熟度分支通常以发布流程中相应阶段的名称命名。因此,诸如“生产分支”、“staging 分支”和“QA 分支”之类的术语。偶尔我会听到人们将生产成熟度分支称为“发布分支”。
何时使用它
源代码控制系统支持协作和跟踪代码库的历史记录。使用成熟度分支允许人们通过显示发布工作流程中特定阶段的版本历史记录来获取一些重要的信息。
我可以通过查看相关分支的头部来找到最新版本,例如当前运行的生产代码。如果出现一个我确定以前不存在的错误,我可以查看分支上的先前版本,并查看生产环境中特定的代码库更改。
自动化可以与对特定分支的更改相关联——例如,每当对生产分支进行提交时,自动化流程就可以将版本部署到生产环境中。
使用成熟度分支的另一种方法是应用标记方案。一旦某个版本准备好进行 QA,就可以对其进行标记——通常以包含构建号的方式进行。因此,当构建 762 准备好进行 QA 时,可以将其标记为“qa-762”,当准备好进行生产时,将其标记为“prod-762”。然后,我们可以通过在代码存储库中搜索与我们的标记方案匹配的标记来获取历史记录。自动化同样可以基于标记分配。
因此,成熟度分支可以为工作流程增加一些便利性,但许多组织发现标记可以很好地工作。因此,我认为这是一种没有明显好处或成本的模式。然而,通常情况下,需要使用源代码管理系统进行这样的跟踪,表明团队的部署流水线工具很差。
变体:长生命周期发布分支
我可以将其视为发布分支模式的一种变体,它将其与发布候选版本的成熟度分支相结合。当我们希望进行发布时,我们将主线复制到此发布分支中。与每个版本的发布分支一样,提交只会在发布分支上进行,以提高稳定性。这些修复程序也会合并到主线中。我们在发布时对其进行标记,并且当我们想要进行另一个发布时,可以再次复制主线。
提交可以按原样复制,这在成熟度分支中更为典型,也可以合并。如果合并,我们必须小心,确保发布分支的头部与主线的头部完全匹配。一种方法是在合并之前恢复已应用于主线的所有修复程序。一些团队还会在合并后压缩提交,以确保每个提交都代表一个完整的发布候选版本。(发现这很棘手的人有充分的理由更喜欢为每个版本剪切一个新分支。)
这种方法只适用于在生产环境中只有一个版本的產品。
团队喜欢这种方法的一个原因是,它可以确保发布分支的头部始终指向下一个发布候选版本,而无需挖掘最新发布分支的头部。但是,至少在 git 中,我们可以通过拥有一个“发布”分支名称来实现相同的效果,该分支名称在团队剪切新发布分支时使用硬重置移动,并在旧发布分支上留下一个标签。
环境分支
通过应用源代码提交将产品配置为在新环境中运行。
软件通常需要在不同的环境中运行,例如开发人员的工作站、生产服务器,以及可能还有各种测试和 staging 环境。通常,在这些不同的环境中运行需要进行一些配置更改,例如用于访问数据库的 URL、消息系统的位置以及关键资源的 URL。
环境分支是一个分支,其中包含应用于源代码以重新配置产品以在不同环境中运行的提交。我们可能在主线上运行版本 2.4,现在希望在我们的 staging 服务器上运行它。我们通过从版本 2.4 开始剪切一个新分支来做到这一点,应用适当的环境更改,重建产品,并将其部署到 staging 环境。
更改通常是手动应用的,但如果负责的人员熟悉 git,他们可以从早期分支中挑选更改。
环境分支模式通常与成熟度分支结合使用。长期存在的 QA 成熟度分支可能包括对 QA 环境的配置调整。然后,合并到此分支将获取配置更改。类似地,长期存在的发布分支可能包括这些配置更改。
何时使用它
环境分支是一种很有吸引力的方法。它允许我们以任何我们需要的方式调整应用程序,使其为新环境做好准备。我们可以将这些更改保存在一个差异中,该差异可以被挑选到产品的未来版本中。然而,它是反模式的典型例子——当你开始时看起来很有吸引力,但很快就会导致一个充满痛苦、巨龙和冠状病毒的世界。
环境变化带来的潜在危险在于,当我们将应用程序从一个环境迁移到另一个环境时,应用程序的行为会发生变化。如果我们无法在开发人员的工作站上调试在生产环境中运行的版本,那么修复问题就会变得更加困难。我们可能会引入只在特定环境中出现的错误,最危险的是生产环境。由于这种危险,我们希望尽可能地确保在生产环境中运行的代码与在其他任何地方运行的代码相同。
环境分支的问题在于,正是这种灵活性使得它们如此具有吸引力。由于我们可以在这些差异中更改代码的任何方面,因此我们可以轻松地引入配置补丁,从而导致不同的行为以及随之而来的错误。
因此,许多组织明智地坚持一条铁律:一旦编译了可执行文件,它就必须是在每个环境中运行的同一个可执行文件。如果需要更改配置,则必须通过显式配置文件或环境变量等机制将其隔离。这样,它们就可以被最小化为在执行过程中不会改变的简单常量设置,从而减少错误滋生的空间。
对于直接执行源代码的软件(例如 JavaScript、Python、Ruby),可执行文件和配置之间的简单界限很容易变得非常模糊,但相同的原则仍然适用。尽量减少任何环境更改,并且不要使用源分支来应用它们。一般的经验法则是,您应该能够签出产品的任何版本并在任何环境中运行它,因此任何纯粹由于部署环境不同而发生变化的内容都不应该在源代码管理中。有一种观点认为应该在源代码管理中存储默认参数的组合,但是应用程序的每个版本都应该能够根据环境变量等动态因素根据需要在这些不同的配置之间切换。
环境分支是使用源分支作为简陋的模块化架构的一个例子。如果应用程序需要在不同的环境中运行,那么在不同环境之间切换的能力需要成为其设计中的一等公民。环境分支可以作为一种临时机制,用于缺乏这种设计的应用程序,但随后应该优先考虑使用可持续的替代方案来移除它。
修复分支
用于捕获修复紧急生产缺陷的工作的分支。
如果生产环境中出现严重错误,则需要尽快修复。处理此错误的工作将比团队正在进行的任何其他工作具有更高的优先级,并且任何其他工作都不应减慢此修复程序的工作速度。
修复工作需要在源代码管理中完成,以便团队可以正确记录并协作完成。他们可以通过在最新发布的版本处打开一个分支并在该分支上应用任何修复程序的更改来做到这一点。
一旦将修复程序应用于生产环境,并且每个人都有机会睡个好觉,就可以将修复程序应用于主线,以确保下一个版本不会出现回归。如果下一个版本有一个发布分支打开,则修复程序也需要在该分支上进行。如果发布之间的时间很长,那么修复程序很可能会在已更改的代码之上进行,因此合并起来会更加尴尬。在这种情况下,能够暴露错误的良好测试非常有用。
如果团队正在使用发布分支,则可以在发布分支上完成修复工作,并在完成后发布新版本。本质上,这会将旧的发布分支转换为修复分支。
与发布分支一样,可以在主线上进行修复并将它们挑选到发布分支。但这不太常见,因为修复程序通常是在时间压力很大的情况下完成的。
如果团队进行持续交付,则可以直接从主线发布修复程序。他们可能仍然使用修复分支,但他们将从最新的提交而不是最后发布的提交开始。
我已将新版本标记为 2.2.1,因为如果团队以这种方式工作,则 M4 和 M5 可能不会公开新功能。如果他们这样做了,那么修复程序很可能会被折叠到 2.3 版本中。当然,这说明了在持续交付的情况下,修复程序不需要绕过正常的发布流程。如果团队有一个足够响应的发布流程,那么修复程序可以像往常一样处理 - 这是持续交付理念的显着优势。
持续交付团队适用的一种特殊处理方法是,在修复程序完成之前禁止对主线进行任何提交。这符合这样一种说法,即没有人比修复主线更重要的任务了 - 事实上,这适用于在主线上发现的任何缺陷,即使是那些尚未投入生产的缺陷。(所以我认为这不是真正的特殊处理。)
何时使用它
修复程序通常是在相当大的压力下完成的,而当团队承受最大压力时,最有可能犯错误。在这种情况下,使用源代码管理并比看起来合理的频率更频繁地提交代码就显得更加宝贵。将这项工作保留在一个分支上可以让每个人都知道正在采取哪些措施来解决这个问题。唯一的例外是可以直接应用于主线的简单修复。
这里更有趣的问题是决定什么是需要修复的紧急错误,以及什么是可以保留到正常开发工作流程中的内容。团队发布的频率越高,就越可以将生产错误修复留给常规的开发节奏。在大多数情况下,决定将主要取决于错误的业务影响,以及它如何适应团队的发布频率。
发布列车
按设定的时间间隔发布,就像火车按定期时刻表发车一样。开发人员在完成其功能后选择要乘坐的列车。
使用发布列车的团队将设置常规的发布节奏,例如每两周或每六个月一次。日期设定为团队何时为每次发布剪切发布分支,遵循火车时刻表的隐喻。人们决定他们希望某个功能赶上哪趟列车,并针对该列车安排他们的工作,在列车装载时将他们的提交放到适当的分支上。一旦列车出发,该分支就是一个发布分支,并且只接受修复。
使用每月列车的团队将根据 2 月份的发布为 3 月份开始一个分支。他们将在整个月内添加新功能。在设定的日期,也许是本月的第三个星期三,火车出发 - 冻结该分支的功能。他们为 4 月份的列车开辟了一个新的分支,并为其添加了新的功能。与此同时,一些开发人员正在稳定 3 月份的列车,并在准备就绪后将其发布到生产环境中。应用于 3 月份列车的任何修复程序都会被挑选到 4 月份的列车中。
发布列车通常与功能分支一起使用。当 Scarlett 感觉到她何时完成她的功能时,她将决定乘坐哪趟列车。如果她认为她可以在 3 月份发布之前完成,她将整合到 3 月份的列车中,但如果没有,她将等待下一趟列车并在那里整合。
一些团队在列车出发(即硬冻结)前几天使用软冻结。一旦发布列车处于软冻结状态,开发人员就不应将工作推送到该列车上,除非他们确信其功能稳定且可以发布。任何在软冻结后添加的、表现出错误的功能都将被还原(从列车上推下),而不是在列车上修复。
如今,当人们听到“发布列车”时,他们通常会听到 SAFe 中的敏捷发布列车概念。SAFe 的敏捷发布列车是一种团队组织结构,指的是共享一个共同发布列车时间表的大型团队。虽然它使用了发布列车模式,但它与我在这里描述的不同。
何时使用它
发布列车模式的一个中心概念是发布过程的规律性。如果您事先知道发布列车何时应该出发,您就可以计划您的功能以赶上该列车。如果您认为您无法在 3 月份的列车之前完成您的功能,那么您就知道您将赶上下一趟列车。
当发布过程中存在很大摩擦时,发布列车特别有用。一个外部测试小组需要几周时间来验证一个版本,或者一个发布委员会需要在发布新版本之前达成一致。如果是这种情况,通常更明智的做法是尝试消除发布摩擦并允许更频繁地发布。当然,在某些情况下,这几乎是不可能的,例如移动设备上的应用程序商店使用的验证过程。调整发布列车以匹配此类发布摩擦可能会使情况得到最佳处理。
发布列车机制有助于集中每个人的注意力,让他们关注哪些功能应该在何时出现,从而帮助预测功能何时完成。
这种方法的一个明显缺点是,在列车运行初期完成的功能将在等待出发时坐在列车上看书。如果这些功能很重要,那就意味着产品会在几周或几个月内错过重要的功能。
发布列车可以成为改进团队发布流程的一个宝贵阶段。如果团队难以进行稳定的发布,那么直接跳到持续交付可能就太远了。选择一个合适的发布列车周期,一个困难但可行的周期,可能是一个良好的开端。随着团队技能的提高,他们可以增加列车的频率,最终随着能力的增长而放弃它们,转向持续交付。
变体:加载未来的列车
功能列车的基本示例是在前一列车出发时,有一列新列车到达站台以接收功能。但另一种方法是让多列列车同时接受功能。如果 Scarlett 认为她的功能无法在 3 月份的列车上完成,她仍然可以将她大部分完成的功能推送到 4 月份的列车上,并在 4 月份的列车出发之前推送更多提交以完成它。
我们会定期从 3 月份的列车拉取到 4 月份的列车中。一些团队更喜欢只在 3 月份的列车出发时才这样做,因此他们只需要进行一次合并,但我们这些知道小型合并更容易的人更喜欢尽快拉取 3 月份的每次提交。
加载未来的列车允许正在开发 4 月份功能的开发人员进行协作,而不会干扰 3 月份列车上的工作。它的缺点是,如果 4 月份的人员进行了与 3 月份工作冲突的更改,3 月份的人员就不会收到反馈,从而使未来的合并更加复杂。
与从主线进行常规发布相比
发布列车的主要优势之一是定期向生产环境发布。但是,为新开发设置多个分支会增加复杂性。如果我们的目标是定期发布,我们也可以使用主线来实现这一点。确定发布计划,然后根据该计划从主线的尖端剪切一个发布分支。
如果有可发布的主线,则不需要发布分支。使用这样的定期发布,开发人员仍然可以选择通过在定期发布日期之前不推送到主线来将几乎完成的功能保留到下一个版本。使用持续集成,如果人们希望某个功能等待下一个计划的发布,他们始终可以延迟放置关键石或保持功能标志关闭。
可发布的主线
保持主线足够健康,以便始终可以直接将主线的头部投入生产
在本节开始时,我评论说,如果您使主线成为健康分支,并且您将健康检查设置得足够高,那么您可以直接从主线发布,并在需要时使用标签记录发布。
我花了大量时间来描述作为这种简单机制的替代方案的模式,所以我认为现在是时候强调这一点了,因为如果一个团队能够做到这一点,这是一个很好的选择。
仅仅因为每次提交到主线都是可发布的,并不意味着它应该被发布。这是持续交付和持续部署之间的微妙区别。使用持续部署的团队确实会发布每个被主线接受的更改,但是使用持续交付,虽然每个更改都是可发布的,但是否发布是一个业务决策。(因此,持续部署是持续交付的一个子集。)我们可以将持续交付视为让我们可以选择在任何时候发布,而我们行使该选项的决定取决于更广泛的问题。
何时使用它
结合持续集成作为持续交付的一部分,一个可发布的主线是高绩效团队的共同特征。鉴于此,以及我对持续交付的热情,你可能认为我会说,一个可发布的主线总是比我在本节中描述的其他选择更好。
然而,模式都是关于上下文的。在一个上下文中出色的模式在另一个上下文中可能是一个陷阱。可发布主线的有效性取决于团队的集成频率。如果团队使用功能分支并且通常每月只集成一次新功能,那么该团队很可能处于糟糕的境地,而坚持可发布的主线可能是他们改进的障碍。糟糕的地方在于,他们无法响应不断变化的产品需求,因为从构思到生产的周期时间太长了。由于每个功能都很大,他们也可能进行复杂的合并和验证,从而导致许多冲突。这些问题可能会在集成时出现,或者当开发人员从主线拉入他们的功能分支时,会持续消耗开发人员的精力。这种阻力阻碍了重构,从而降低了模块化,从而加剧了问题。
摆脱这个陷阱的关键是增加集成频率,但在许多情况下,在保持可发布主线的同时很难做到这一点。在这种情况下,通常最好放弃可发布的主线,鼓励更频繁的集成,并使用发布分支来稳定主线以用于生产。当然,随着时间的推移,我们希望通过改进部署管道来消除对发布分支的需求。
在高频集成的背景下,可发布的主线具有明显的简单性优势。没有必要去理会我所描述的各种分支的复杂性。即使是修补程序也可以应用于主线,然后应用于生产,这使得它们不再特别到值得拥有一个名称。
此外,保持主线可发布鼓励了一种有价值的纪律。它将生产就绪性放在开发人员的首要位置,确保问题不会逐渐蔓延到系统中,无论是作为错误还是作为减慢产品周期时间的流程问题。持续交付的全部原则——开发人员每天多次集成到主线而不破坏它——对许多人来说似乎非常困难。然而,一旦实现并成为一种习惯,团队就会发现它显着地减少了压力,并且相对容易保持。这就是为什么它是敏捷流畅度®模型交付区域的关键要素。
其他分支模式
本文的主要内容是讨论围绕团队集成和生产路径的模式。但也有一些其他的模式我想提一下。
实验分支
将代码库上的实验性工作收集在一起,这些工作预计不会直接合并到产品中。
实验分支是开发人员想要尝试一些想法的地方,但不要期望他们的更改会被简单地集成回主线。我可能会发现一个新的库,我认为它可以很好地替代我们已经在使用的库。为了帮助决定是否切换,我启动了一个分支,并尝试使用它编写或重写系统的相关部分。练习的目的不是向代码库贡献代码,而是了解新工具在我特定上下文中的适用性。我可以自己做这件事,也可以和一些同事一起做。
类似地,我有一个新功能要实现,并且可以看到几种方法来实现它。我花几天时间研究每种方案,以帮助我决定采用哪一种。
这里的关键是,期望实验分支上的代码将被放弃,并且不会合并到主线中。这并不是绝对的——如果我碰巧喜欢结果并且代码可以很容易地集成,那么我不会忽视这个机会——但我并不*期望*会是这样。我可能会放松一些通常的习惯,减少测试,一些随意的代码重复,而不是试图干净地重构它。我希望,如果我喜欢这个实验,我会从头开始将这个想法应用到生产代码中,使用实验分支作为提醒和指南,但不使用任何提交。
一旦我完成了实验分支的工作,在 git 中,我通常会添加一个标签并删除该分支。该标签保留了代码行,以防我以后想重新检查它——我使用了一个约定,例如以“exp”开头标记名称,以使其性质清晰。
何时使用它
每当我想尝试一些东西,并且我不确定我最终是否会使用它时,实验分支就很有用。这样我就可以做任何我想做的事情,无论多么古怪,但我相信我可以很容易地把它放在一边。
有时我会认为我正在做常规工作,但意识到我正在做的事情实际上是一个实验。如果发生这种情况,我可以打开一个新的实验分支,并将我的主要工作分支重置为最后一个稳定提交。
未来分支
用于处理过于具有侵入性而无法使用其他方法处理的更改的单个分支。
这是一种罕见的模式,但在人们使用持续集成时偶尔会出现。有时,团队需要对代码库进行非常具有侵入性的更改,而用于集成正在进行的工作的常用技术并不适用。在这种情况下,团队会做一些看起来很像功能分支的事情,他们会切出一个未来分支,并且只从主线拉取,直到最后才进行主线集成。
未来分支和功能分支之间的最大区别在于,只有一个未来分支。因此,在未来分支上工作的人永远不会偏离主线太远,并且没有其他分支需要处理。
可能有多个开发人员在未来分支上工作,在这种情况下,他们使用未来分支进行持续集成。在进行集成时,他们在集成更改之前,首先从主线拉入未来分支。这将减慢集成过程,但这是使用未来分支的代价。
何时使用它
我应该强调,这是一种罕见的模式。我怀疑大多数进行持续集成的团队都不需要使用它。我已经看到它用于对系统架构进行特别具有侵入性的更改。总的来说,这是一种最后的手段,只有在我们无法弄清楚如何使用像抽象分支这样的东西时才会使用。
未来分支应该尽可能短,因为它们会在团队中造成分区,就像任何分布式系统中的分区一样,我们需要将它们保持在绝对最小值。
协作分支
为开发人员创建的一个分支,用于与团队的其他成员共享工作,而无需正式集成。
当团队使用主线时,大多数协作都是通过主线进行的。只有当主线集成发生时,团队的其他成员才会看到开发人员在做什么。
有时,开发人员希望在集成之前共享他们的工作。打开一个分支进行协作允许他们临时进行此操作。该分支可以推送到团队的中央存储库,协作者可以直接从他们的个人存储库中拉取和推送,或者可以建立一个短期存储库来处理协作工作。
协作分支通常是临时的,一旦工作集成到主线中就会关闭。
何时使用它
随着集成频率的降低,协作分支变得越来越有用。如果团队成员需要协调对几个人都很重要的代码区域的一些更改,那么长期存在的功能分支通常需要非正式协作。然而,使用持续集成的团队可能永远不需要打开协作分支,因为他们只有在他们的工作对彼此不可见的情况下才会出现短暂的时期。这方面的主要例外是实验分支,根据定义,它永远不会被集成。如果几个人一起进行一项实验,他们需要使实验分支也成为一个协作分支。
团队集成分支
允许子团队在与主线集成之前相互集成。
较大的项目可能有多个团队在单个逻辑代码库上操作。团队集成分支允许团队成员相互集成,而无需使用主线与项目的所有成员集成。
实际上,团队将团队集成分支视为团队内部的主线,并像使用整个项目主线一样与之集成。除了这些集成之外,团队还进行单独的工作以与项目主线集成。
何时使用它
使用团队集成分支的明显驱动因素是,代码库正在被如此多的开发人员积极开发,以至于将他们分成单独的团队是有意义的。但我们应该警惕这种假设,因为我遇到过很多团队,他们看起来太大而无法在一个主线上工作,但仍然设法做到了。(我已经收到了多达一百名开发人员的报告。)
团队集成分支的一个更重要的驱动因素是所需集成频率的差异。如果整个项目期望团队执行几周的功能分支,但子团队更喜欢持续集成,那么团队可以建立一个团队集成分支,与之进行持续集成,并在完成后将他们正在处理的功能与主线集成。
如果整个项目正在使用的健康分支标准与子团队的健康标准之间存在差异,则会产生类似的效果。如果更广泛的项目无法将主线维持在足够高的稳定性,则子团队可以选择在更严格的健康水平上运行。类似地,如果子团队正在努力使其提交足够健康以用于控制良好的主线,他们可以选择使用团队集成分支并使用他们自己的发布分支来稳定代码,然后再进入主线。这不是我通常会赞成的情况,但在特别困难的情况下可能是必要的。
我们还可以将团队集成分支视为一种更结构化的协作分支形式,它基于正式的项目组织而不是临时协作。
查看一些分支策略
在本文中,我已经从模式的角度讨论了分支。我这样做是因为我不想提倡分支的唯一方法,而是要阐述人们这样做的常见方式,并在软件开发中我们发现的各种不同上下文中反思它们的权衡。
多年来,人们已经描述了许多分支方法。当我试图理解它们是如何工作的以及它们何时最适合使用时,我已经通过我脑海中半成形的模式对它们进行了评估。现在我已经最终开发并写下了这些模式,我认为看看其中的一些策略,看看我如何根据这些模式来思考它们是很有用的。
Git-flow
Git-flow 已成为我遇到过的最常见的分支策略之一。它是由Vincent Driessen在 2010 年编写的,当时 git 正在流行起来。在 git 出现之前,分支通常被视为一个高级主题。Git 使分支更具吸引力,部分原因是改进了工具(例如更好地处理文件移动),但也因为克隆存储库本质上是一个分支,在推送到中央存储库时需要类似地考虑合并问题。
Git-Flow 在单个“origin”存储库中使用主线(称之为“develop”)。它使用特性分支来协调多个开发人员。鼓励开发人员使用他们的个人存储库作为协作分支,以便与从事类似工作的其他开发人员进行协调。
git 中传统上称为核心分支的是“master”,在 git-flow 中,master 被用作生产成熟度分支。Git-Flow 使用发布分支,以便工作从“develop”经过发布分支传递到“master”。修补程序通过修补程序分支进行组织。
Git-Flow 没有说明特性分支的长度,因此也没有说明预期的集成频率。它也没有说明主线是否应该是健康分支,如果是,则需要什么级别的健康状况。发布分支的存在意味着它不是可发布主线。
正如 Driessen 在今年的一份附录中指出的那样,git-flow 是为那些在生产中发布多个版本的项目而设计的,例如安装在客户站点上的软件。当然,拥有多个实时版本是使用发布分支的主要触发因素之一。然而,许多用户在单个生产 Web 应用程序的上下文中选择了 git-flow - 在这种情况下,这样的分支结构很容易变得比必要的复杂。
虽然 git-flow 非常流行,从很多人说他们使用它的意义上来说,但通常会发现那些说他们正在使用 git-flow 的人实际上在做一些完全不同的事情。通常,他们的实际方法更接近 GitHub Flow。
GitHub Flow
虽然 Git-flow 确实流行起来,但其分支结构对于 Web 应用程序来说过于复杂,这促使人们开发了许多替代方案。随着 GitHub 的流行,其开发人员使用的分支策略成为一种众所周知的策略也就不足为奇了 - 称为 GitHub Flow。最佳描述来自 Scott Chacon
像 GitHub Flow 这样的名字,毫无疑问,它是基于 git-flow 并针对 git-flow 做出的反应。两者之间的本质区别在于产品类型的不同,这意味着上下文不同,因此模式也不同。Git-Flow 假设产品在生产中有多个版本。GitHub Flow 假设生产中只有一个版本,并且可以高频率地集成到可发布主线上。在这种情况下,不需要发布分支。生产问题的修复方式与常规功能相同,因此不需要修补程序分支,因为修补程序分支通常意味着偏离正常流程。删除这些分支极大地简化了分支结构,使其只剩下主线和特性分支。
GitHub Flow 将其主线称为“master”。开发人员使用特性分支进行工作。他们定期将他们的特性分支推送到中央存储库,以支持可见性,但在特性完成之前不会与主线集成。Chacon 指出,特性分支可以是一行代码,也可以运行几周。该流程旨在在任何一种情况下都以相同的方式工作。作为 GitHub,拉取请求机制是主线集成的一部分,并使用集成前审查。
Git-flow 和 GitHub Flow 经常被混淆,因此,与往常一样,要深入了解名称背后的含义,才能真正理解正在发生的事情。它们两者的共同主题是使用主线和特性分支。
基于主干的开发
正如我之前写的,我听到的大多数“主干驱动开发”都是持续集成的同义词。但是,将主干驱动开发视为 git-flow 和 GitHub Flow 的分支策略替代方案也是合理的。Paul Hammant 撰写了一个深度网站来解释这种方法。Paul 是我在 Thoughtworks 的长期同事,他有着丰富的经验,可以用他那把可靠的 +4 开山刀来处理客户僵化的分支结构。
主干驱动开发侧重于在主线(称为“trunk”,它是“mainline”的常见同义词)上完成所有工作,从而避免任何类型的长期分支。较小的团队使用主线集成直接提交到主线,较大的团队可以使用短期特性分支,其中“短期”意味着不超过几天 - 在实践中,这可能等同于持续集成。团队可以使用发布分支(称为“branch for release”)或可发布主线(“release from trunk”)。
最后的想法和建议
从最早的程序开始,人们就发现,如果他们想要一个与现有程序略有不同的程序,那么复制源代码并根据需要进行调整是很容易的。有了所有的源代码,我就可以随心所欲地进行任何更改。但是,通过这种操作,我的副本就更难接受原始源代码中的新功能和错误修复。随着时间的推移,这可能会变得不可能,正如许多企业在其早期的 COBOL 程序中发现的那样,并且如今仍在遭受着广泛定制的 ERP 软件包的困扰。即使没有使用这个名称,每当我们复制源代码并对其进行修改时,我们都是在进行源代码分支,即使没有涉及版本控制系统。
正如我在这篇长文的开头所说:分支很容易,合并却很难。分支是一种强大的技术,但它让我想起了 goto 语句、全局变量和并发锁。强大、易于使用,但也更容易被过度使用,它们常常成为粗心和缺乏经验者的陷阱。源代码控制系统可以通过仔细跟踪更改来帮助控制分支,但最终它们只能充当问题的见证者。
我不是说分支不好。对于日常问题,例如多个开发人员为单个代码库做出贡献,明智地使用分支是必不可少的。但我们应该始终对此保持警惕,并记住 Paracelsus 的观察结果,即有益药物和毒药之间的区别在于剂量。
因此,我对分支的第一个建议是:每当您考虑使用分支时,都要弄清楚如何合并。每当您使用任何技术时,您都是在与其他选择进行权衡。如果您不了解一项技术的所有成本,就无法做出明智的权衡决定,而对于分支,当您合并时,您就需要付出代价。
因此,下一条准则是:确保您了解分支的替代方案,它们通常更好。记住Bodart 定律,是否有办法通过改进模块化来解决您的问题?您能改进您的部署管道吗?标签够用吗?对流程进行哪些更改可以使此分支变得不必要?很有可能分支实际上是目前最好的途径 - 但这是一种气味,它提醒您存在一个更深层次的问题,应该在接下来的几个月里解决。摆脱对分支的需求通常是一件好事。
记住LeRoy 的插图:分支在没有集成的情况下运行时会呈指数级发散。因此,请考虑您集成分支的频率。目标是将您的集成频率提高一倍。(这里显然有一个限制,但除非您处于持续集成的区域,否则您不会接近它。)更频繁地集成会遇到障碍,但这些障碍往往正是那些需要给予过量炸药才能改善您的开发过程的障碍。
由于合并是分支的难点,因此要注意是什么让合并变得困难。有时是流程问题,有时是架构失败。不管是什么,都不要屈服于斯德哥尔摩综合症。任何合并问题,尤其是导致危机的合并问题,都是团队提高效率的标志。请记住,错误只有在您从中吸取教训时才有价值。
我在这里描述的模式概述了我和我的同事在旅途中遇到的常见分支配置。通过对它们进行命名、解释,最重要的是,解释它们何时有用,我希望这有助于您评估何时使用它们。请记住,与任何模式一样,它们很少是普遍好或坏的 - 它们对您的价值取决于您所处的环境。当您遇到分支策略时(无论是像 git-flow 或主干驱动开发这样的知名策略,还是在开发组织中自行开发的策略),我希望了解其中的模式将有助于您决定它们是否适合您的情况,以及哪些其他模式可以很好地融入其中。
致谢
Badri Janakiraman、Brad Appleton、Dave Farley、James Shore、Kent Beck、Kevin Yeung、Marcos Brizeno、Paul Hammant、Pete Hodgson 和 Tim Cochran 阅读了本文的草稿,并就如何改进它向我提供了反馈。
Peter Becker 提醒我指出,fork 也是一种分支形式。我从 Steve Berczuk 的软件配置管理模式一书中借用了“主线”这个名称。
延伸阅读
关于分支的文章有很多,我无法对所有文章进行认真的调查。但我确实想重点推荐 Steve Berczuk 的著作:软件配置管理模式。Steve 的作品,以及他的合著者 Brad Appleton 的作品,对我如何看待源代码管理产生了持久的影响。
重大修订
2021 年 1 月 4 日:添加了关于拉取请求的侧边栏
2021 年 1 月 2 日:将模式“已审查的提交”重命名为“集成前审查”,我认为这是一个更清晰的名称
2020 年 5 月 28 日:发布最后一部分
2020 年 5 月 27 日:发布了对一些分支策略的探讨
2020 年 5 月 21 日:发布了协作分支和团队集成分支
2020 年 5 月 20 日:起草了最终想法
2020 年 5 月 19 日:发布了未来分支
2020 年 5 月 18 日:发布了实验分支
2020 年 5 月 14 日:发布了可发布主线
2020 年 5 月 13 日:起草了关于分支策略的部分
2020 年 5 月 13 日:发布了发布列车
2020 年 5 月 12 日:发布了修补程序分支
2020 年 5 月 11 日:起草了可发布主线
2020 年 5 月 11 日:发布了环境分支
2020 年 5 月 7 日:发布了成熟度分支
2020 年 5 月 6 日:发布了发布分支
2020 年 5 月 5 日:发布了集成摩擦、模块化的重要性以及我对集成模式的个人想法
2020 年 5 月 4 日:发布了已审查的提交
2020 年 4 月 30 日:发布了持续集成和特性分支的比较。
2020 年 4 月 29 日:发布了持续集成
2020 年 4 月 28 日:草稿:添加了关于模块化的部分
2020 年 4 月 28 日:发布了集成频率
2020 年 4 月 27 日:草稿:将生产分支概括为成熟度分支
2020 年 4 月 27 日:发布了特性分支
2020 年 4 月 23 日:发布了主线集成
2020 年 4 月 22 日:发布了健康分支
2020 年 4 月 21 日:发布了主线。
2020 年 4 月 20 日:发布了第一部分:源代码分支。
2020 年 4 月 5 日:第五稿:处理了发布模式的审阅意见,编写了发布列车,修改了源代码分支。
2020 年 3 月 30 日:第四稿:处理了关于基础和集成部分的大多数审阅意见。将源代码分支设为一种模式。
2020 年 3 月 12 日:第三稿:将模式重铸为特殊部分
2020 年 3 月 5 日:第二稿:将文本重新组织为集成模式和生产路径。添加了发布分支和修补程序的插图,并相应地重写了文本
2020 年 2 月 24 日:第一稿:与审阅者共享
2020 年 1 月 28 日:开始写作