来源:DevOps社区Meetup

原作者:Martin Fowler

原文网址:https://martinfowler.com/articles/branching-patterns.html

译者:崔龙波 郭颖 朱婷 余晓蒨

审校:王立杰 王英伟 高俊宁 陈文峰

452e3fe316e1db8b10d88dfe0f93ca55.jpeg

Martin Fowler

afcf5c8a173dcfd991e036ca7e143ef4.png

——2020年5月28日

目录

一、基本模式 

1.1 源分支 ✣ 

1.2 主线 ✣ 

1.3 健康的分支 ✣ 

二、集成模式 

2.1 主线集成 ✣

2.2 特性分支开发 ✣

2.3 集成频率 ✣

2.3.1 低频集成 

2.3.2 高频集成 

2.3.3 集成频率对比 

2.4 持续集成 ✣

2.5 对比特性分支开发和持续集成 ✣

2.5.1 特性分支开发和开源

2.6 对提交评审 ✣ 

2.7 集成阻力 ✣

2.8 模块化的重要性 ✣

2.9 个人对于集成模式的看法 ✣

三、从主线到生产发布的路径

3.1 发布分支 ✣

3.2 成熟度分支 ✣

3.2.1 变体:长期存在的发布分支

3.3 环境分支 ✣

3.4 热修复分支 ✣

3.5 发布火车 ✣

3.5.1 变体:承载未来的火车

3.5.2 与从主线定期发布的比较

3.6 发布就绪主线(主线发布) ✣

四、其它分支模式

4.1 试验分支 ✣

4.2 未来分支 ✣

4.3 协作分支 ✣

4.4 团队集成分支 ✣

五、考虑一些分支策略

5.1 Git-flow

5.2 GitHub Flow

5.3 基于主干开发

六、最后的想法和建议


对任何软件开发团队来说,源代码都是重要的资产。几十年来,已有一系列源代码管理工具被开发出来,用于维护代码。这些工具可以跟踪变更,因此我们可以恢复软件的历史版本并查看它的演进过程。这些工具还是开发团队的协作中心,团队中的所有程序员都在一个公共的代码库上工作。通过记录每位开发人员所做的更改,这些系统可以一次跟踪多行工作内容,并帮助开发人员解决如何把这些内容合并到一起。

将开发活动划分为分解和合并的工作流,是软件开发团队工作流程的核心,并且已演化出多种模式帮助我们处理所有这些活动。像大多数软件模式一样,几乎没有哪种模式是所有团队都应遵循的黄金法则。软件开发工作流程依赖于具体环境,特别是团队的社会结构和团队遵循的其他实践。

本文将详述这些模式,并在模式描述中夹杂可以更好地说明模式背景和相互关系的叙事部分。为便于区分,模式描述的章节将附以图标“✣”。

一、基本模式

3d82d65e7c46f1da08c502dd5d48f7fd.png

在思考这些代码分支模式时,我发现它们可以分为两大类:一类模式着眼于集成,即多个开发人员如何将他们的工作成果组合成一个连贯的整体。另一类则着眼于生产路径,即使用分支帮助管理从集成代码库到生产环境运行产品的路径。

一些模式为这两大类模式提供支撑,我将它们归类为基本模式,在本节中讲述。还有一些模式既不基本也不适合于归类到集成和生产路径这两大类模式,我把它们留到最后来讲。

1.1 源分支✣

创建一个副本并记录对该副本的所有更改。

如果几个人在同一代码基础上工作,那么很快他们就无法在相同文件上工作。如果我想运行一个编译,而我的同事还正在敲入一个表达式,那么编译将失败。我们不得不相互呼喊:“我正在编译,什么都不要更改!”即使团队只有两个人,这也难以维持正常工作;如果是更大的团队,这种混乱场景会更加令人难以想象。

对此场景案例的简单解决办法是让每个开发人员都获取一个代码库的副本,然后我们就可以轻松地进行自己负责的功能开发。但是又会出现一个新问题:开发完成后,如何将两个副本再次合并在一起?

源代码控制系统使此过程更加容易。关键在于它会将每个分支上所有的更改都记录为提交。这不仅可以确保没有人忘记他们对 utils.java 所做的微小更改,而且记录更改使执行合并更加容易,尤其是当几个人更改了同一文件时。

这就引出了本文中使用的分支(branch)的定义。我将分支定义为对代码库的特定提交序列。分支的 head 或 tip 指向该序列中的最新提交。

76aff561a341ff4183ae9d501dd5934a.png

分支是个名词,但也有动词“创建分支”的意思。这里我的意思是创建一个新分支,我们也可以将其视为将原始分支分为两个分支。当来自一个分支的提交被应用到另一分支时,即为分支合并。

313ef8ee96f40de5cc200b3f9e588757.png

我用于“分支”的定义与我观察大多数开发人员谈论它们的方式相对应。但是源代码控制系统更倾向于以特定的方式使用“分支”。

以一种常见情况来说明这一点,一个现代开发团队,该团队将其源代码保存在共享的 git 仓库中。一名开发人员 Scarlett (以猩红色表示) 需要进行一些更改,因此她克隆了 git 仓库并检出了 master 分支。她做了几处更改,然后重新提交给她的 master 分支。

同时,另一个开发人员,Violet (以紫色表示) 将仓库克隆到自己桌面上,并签出 master 分支。那么 Scarlett 和 Violet 是在同一个分支上工作还是分别在另一个分支上工作?答案是:他们都在 “master” 上工作。但是他们的提交彼此独立,并且当他们将更改推回到共享仓库时都需要合并。如果 Scarlett 不确定自己所做的更改,会发生什么情况,因此她标记了最后的提交,并将她的 master 分支重置回 origin/master(她克隆共享仓库时的最后一次提交)。

5ff6bd758c6fd79903d4740824f0b29f.png

根据我前文给出的分支定义,Scarlett 和 Violet 分别在单独的分支上工作,这两个分支彼此分开,并且与共享仓库上的 master 分支隔离。当 Scarlett 放弃带有标签的分支开发时,根据定义,它仍然是一个分支(并且她很可能将其视为分支),但是在 git 看来,这是一个带标签的代码行。

使用 git 这样的分布式版本控制系统,这意味着每当我们进一步克隆仓库时,就会获得其他分支。如果 Scarlett 在回家的火车上克隆了自己的本地仓库到笔记本电脑上,那么她将创建第 3 个 master 分支。在 GitHub 中派生也会产生相同的效果 —— 每个派生的仓库都有自己额外的分支集。

当我们遇到不同的版本控制系统时,这种术语的混乱会变得更糟,因为它们对分支的构成都有自己的定义。Mercurial 中的分支与 git 中的分支完全不同,后者更接近 Mercurial 的书签。Mercurial 也可以用未命名的 head 创建分支,使用 Mercurial 的人们经常通过克隆仓库来创建分支。

所有这些术语上的混乱导致一些人避免使用该术语。在这里更通用的术语是代码线(CodeLine)。我将代码线定义为代码库的一系列特定版本。它可以以标签结尾,或是一个分支,又或者淹没在 git 的 reflog 中。你会注意到我对分支和代码线的定义是如此相似。代码线在许多方面都是更有用的术语,我确实使用过,但是在实践中并未广泛使用。因此,对于本文而言,除非我处于 git(或其他工具)术语的特定上下文中,否则我将交替使用分支和代码线。

此定义的结果是,无论你使用的是哪种版本控制系统,一旦有开发人员在进行本地更改后,每个开发人员在本地的工作副本中都至少具有一条个人代码线。如果我克隆一个项目的 git 库,检出 master 分支并更新一些文件 —— 这就是一条新的代码线,即使我还没有提交任何内容。同样,如果我从 subversion 库的主干建了自己的工作副本,即使不涉及任何 subversion 分支,该工作副本也是独立的代码线。

1.1.1 适用场景

一个老话说,如果你从高楼上摔下来,坠落不会伤害到你,但是着陆会。对源代码来说也是一样的道理:创建分支容易,但合并困难。

记录提交中所有更改的源代码控制系统确实让合并过程更加容易,但并没有使合并过程不再重要。如果 Scarlett 和 Violet 都将变量的名称更改为不同的名称,则存在冲突,如果没有人工干预,源管理系统将无法自行处理。为了凸显这种文本冲突的尴尬,源代码控制系统至少还可以发现并提醒人们看一下。但是在文本合并没有问题的地方也经常会出现冲突,系统仍然无法正常工作。

想象一下,Scarlett 更改了函数的名称,而 Violet 向其分支添加了一些代码,以其旧名称调用该函数。这就是我所说的语义冲突。当发生此类冲突时,系统可能无法构建,也可能会构建成功但在运行时失败。

Jonny LeRoy 喜欢指出人们(包括我)绘制分支图的这个瑕疵。

b453047397fe47480ccf4296766355ff.jpeg

任何有并行计算或分布式计算工作经验的人都熟悉的问题是:当多个开发人员同时更新时,代码仓会处于某个共享状态。我们需要通过将这些更新序列化为某个共识更新的方式,把这些开发人员的更新结合起来 。

事实上,使系统正确执行和运行意味着该共识状态的有效性标准非常复杂,这使我们的任务也变得更加复杂。无法创建确定性算法来找到共识。人们需要寻求共识,并且共识可能涉及混合不同更新的选择部分。通常,只有通过原始更新解决冲突才能达成共识。

我说:“如果没有分支该怎么办”。每个人都将实时编辑代码,考虑不周的更改会使系统崩溃,人们会互相踩踏。因此,我们给个人一种时间冻结的错觉,认为他们是唯一更改系统的人,这些变更可以等到他们对系统风险考虑充分后才变更。但这是一种错觉,最终代价还是该来的会来。谁买单?什么时候?代价是多少?这些模式正在讨论的就是:选择如何支付代价。—— Kent Beck

因此,在下文中我将列出各种模式,这些模式支持友好的隔离,就像当你从高处落下时,风穿过发丝,同时又把不可避免的与坚硬地面的碰撞后果降到最低。

1.2 主线 ✣

单一、共享、代表产品当前状态的分支。

主线(mainline)是一个特殊的代码线,代表团队代码的当前状态。当我想开始一项新工作,我会从主线中拉取代码到我的本地版本库,在本地版本库上工作。当我要与团队的其他成员分享我的工作成果时,我会用我的工作成果更新主线,理想状态下将应用后面要讨论的主线集成模式。

不同的团队使用不同的名称称呼这一特殊分支,通常会受使用的版本控制系统惯例的影响。Git 用户通常称之为 “master”, subversion 用户通常称之它为 “主干”。

在这里必须强调,主线是一个单一的、共享的代码线。当人们在 git 中谈论 “master” 时,他们可能在说几件不同的事情,因为每个代码库的克隆都有自己的本地 master。通常,团队会有一个中央仓库 —— 一个作为项目单一记录点的共享仓库,并且是大多数克隆的起源。从头开始一项新工作意味着克隆该中央仓库。如果已经有了一个克隆,我会首先从中央仓库拉取 master 分支,以保持与主线同步。在这种情况下,主线就是中央仓库的 master 分支。

当我在开发自己的功能时,我在使用自己的开发分支,这个分支可以是我本地版本库的 master 分支,也可以是其他本地分支。如果需要在自己的开发分支上工作较长时间,我可以每隔一段时间拉取主线的更改,并把这些更改合并到我自己的开发分支上,以获取主线上最新的更改。

同样,如果我想创建产品发布的新版本,我可以从当前主线开始。如果我需要修复错误,以发布足够稳定的产品,我可以使用某一发布分支。

1.2.1 适用场景

我记得在 21 世纪初常和一个客户端构建工程师讨论。他的工作是集成团队正在开发的产品。他会给团队的每个成员发一封电子邮件,团队成员则会发回各自代码库中等待集成的各种不同文件。这位构建工程师就把这些文件复制到他的集成树中,并尝试编译代码库。创建一个能够编译,并可供某种形式进行测试的构建,通常需要耗费这位构建工程师几周的时间。

相比之下,通过主线,任何人都可以从主线的一部分快速开始产品最新的构建。更重要的是,主线不仅仅使得观察代码库状态更容易,它还是许多其他模式的基础,这些模式将后文中描述。

主线的一个替代方案是发布火车。

1.3 健康的分支 ✣

在每次提交时执行自动检查,以确保分支没有缺陷,自动检查通常包括构建和运行测试。

由于主线具有共享的并且是已被认可的状态,因此保持主线处于稳定状态非常重要。还是在 21 世纪初,我记得曾和某一组织的一个开发团队一起讨论,这个组织因对所有产品执行每日构建而广为人知。在当时,每日构建被认为是相当先进的做法,这个组织也因此而获得赞誉。在这些赞扬的文章中没有提到的是,那些每日构建并不总是成功的。实际上,一些团队的日常构建连续数月都无法编译成功,这在当年并不罕见。

为了解决这个问题,我们可以努力去保持一个分支是健康的——也就是这个分支是可以成功构建并且运行时几乎没有 bug 的。为了确保这一点,我发现编写自测代码是至关重要的。这种开发实践是指我们在编写生产代码时,还要编写一套全面的自动化测试,让我们可以确信,如果这些测试通过,那么这些代码就不会有 bug。

如果我们这样做,就可以通过每次提交运行一个构建来保持分支健康,这个构建过程也包括运行这套测试。如果系统无法编译,或者测试失败,那么我们的第一要务就是在我们对该分支进行任何其他操作之前就先对其进行修复。通常这意味着我们“冻结”了这个分支——除为了修复以使其恢复正常的提交之外,不会允许在这个分支进行任何提交。

为了给保持分支健康提供足够的信心,在测试的程度上存在一定矛盾。许多更彻底的测试需要大量的时间去运行,这就会延迟对提交是否正常的反馈。一些团队通过将测试分散到部署流水线的多个阶段来解决这个问题。这些测试的第一个阶段应运行快速,一般不超过十分钟,但仍应相当全面。我将这样的测试集称为提交套件 (不过它通常会被称为“单元测试”,因为这样的提交套件中的测试大多数是单元测试)。

理想情况下,应在每次提交时运行全方位的测试。但是,如果测试执行很慢,例如需要占用服务器几个小时的性能测试,那就有点不切实际。如今,团队通常会构建一个提交套件,在每次提交时运行,而对部署流水线后续的阶段,会尽可能频繁地运行。

代码运行没有错误并不足以说明就是好的代码。为了保持稳定的交付节奏,我们需要保持足够高的代码内建质量。一种流行的方法是使用提交审核(Reviewed Commits),然而我们也要看到还有其他选择。

1.3.1 适用场景

每个团队都应当在他们的开发工作流程中明确每个分支的健康状况标准。保持主线健康有无比重要的价值。如果主线是健康的,那么开发人员只要从当前的主线拉取代码就可以开始新的工作,而不会纠结于那些可能会妨碍他们工作的缺陷。我们经常听说有人在开始新的工作前要花几天时间去尝试修复或绕过他们拉取代码中的问题。

健康的主线也可以简化生产路径。可以随时从主线的最新版本构建新的生产候选对象。最好的团队发现他们几乎不需要做任何工作来稳定这样的代码库,这些代码库通常能够直接从主线发布到生产环境。

主线健康的关键是自测代码,以及一个可在几分钟内运行完成的提交套件。建设这样的能力会是很有意义的投入,一旦我们可以在几分钟之内确保我的提交不会搞砸任何东西,那将彻底改变我们的整个开发过程。我们可以更快地进行更改,自信地重构我们的代码让它更好用,并大大减少从期望功能到生产中运行代码的交付周期。

保持个人开发分支的健康是明智的做法,因为这样可以启用差异调试。但是,这种期望和频繁提交当前状态为检查点是背道而驰的。如果我要尝试一个不同的路径,那么即使编译失败可能也会去创建一个检查点。解决这种矛盾的方法是,一旦完成我最近的工作,就去除所有不健康的提交。这样,只有健康的提交会在我的分支上保留超过几个小时。

如果我保持个人分支的健康,这也能使提交到主线变得更加容易——我会知道任何在主线集成(Mainline Integration)中突然出现的错误都纯粹是由于集成问题引起的,而不单单是我代码库中的错误。这将使查找和修复错误变得更快也更容易。

二、集成模式

0e79ec25686f7b94629b0ac1e82748b2.png

分支开发涉及到在管理分离和合并时的相互影响。由于所有人始终使用同一套共享代码库,如果你正在输入变量名,我这边就无法编译程序,这是行不通的。因此,至少在某种程度上,我们需要有一个私有工作区的概念,让我可以暂时在这个私有工作区里工作。现代的源代码控制工具使得创建分支和监视这些分支的变更变得很容易。然而,在某些时候,我们还需要合并分支。考虑分支开发策略实际上就是决定我们合并分支的方式和时机。

2.1 主线集成 ✣

开发人员通过从主线中拉取、合并,以及(在健康的情况下)推回主线来集成他们的工作。

主线清晰定义了团队软件当前的状态。使用主线的最大好处之一是简化了集成。如果没有主线,这就是我前面描述的要与团队中每个人进行协调的复杂任务。然而,有了主线,每个开发人员都可以自己集成。

我将通过一个例子来说明它的工作原理。有一个名为 Scarlett 的开发人员,通过将主线克隆到自己的仓库中开始某项工作。在 git 中,如果她还没有中央仓库的克隆,她将会克隆中央仓库,检出 master 分支。如果她已经有了中央仓库的克隆,她将拉取主线到她的本地 master 分支。然后,她就可以在本地工作,在她的本地 master 分支上进行提交。

8684af25fd81f4c146ec81d38f3f40fc.png

当她工作的时候,她的同事 Violet 把一些变更推送到了主线上。由于 Scarlett 是在自己的代码线上工作,所以当她在做自己的事情时,可以忽略这些变化。

4387089255a925c73b71fde3e9b8f7ba.png

在某个时间点,Scarlett 达到了可以集成的程度。第一步,是将当前的主线状态提取(fetch)到本地主分支中,这将拉取到 Violet 的变更。当她在本地分支工作时,提交将在 origin/master (本地主分支名)上作为一个单独的代码线显示。

4e7c45d89b1b22f088579ef1eb2b4ccf.png

现在她需要把她的变更和 Violet 的变更合并起来。有些团队喜欢通过 merge(合并)来做到这一点,而另一些团队则喜欢通过 rebase(变基)来实现。通常,人们在谈到将分支融合在一起时,无论是实际使用 git merge 还是 rebase 操作,都会使用“merge(合并)”一词。我将遵循这种用法,因此,除非我实际上正在讨论合并和变基之间的区别,否则请考虑将“merge(合并)”作为可以以两者中任意一个方法实现的逻辑操作。

关于是使用普通的合并,还是使用或避免 fast-forward 快速合并,或者是使用 rebase ,另外还有一些其他的讨论。这超出了本文的范围,但是如果人们寄给我足够多的 Tripel Karmeliet(卡美里特啤酒)的话,我可能会写一篇关于这个问题的文章,毕竟如今比较流行“投桃报李”嘛。

如果 Scarlett 幸运的话,合并 Violet 的代码将是一个清晰的过程,否则,她将会遇到一些冲突。这些可能是文本冲突,大部分源代码控制系统可以自动处理这些冲突。但是语义冲突更难处理,这就是有“自测代码”的方便之处。(由于冲突会产生很多的工作量,而且总是会引入许多工作中的风险,所以我用一块醒目的黄色来标记它们。)

41f5d1ff17e346686993b1ebce7b71ec.png

此时,Scarlett 需要验证合并的代码满足主线的健康标准 (假设主线是一个健康分支)。这通常意味着构建代码并运行构成主线提交套件的所有测试。即使这是一个干净的合并,她也需要做这些工作,因为尽管是一个干净的合并也可能隐藏语义冲突。提交套件中的任何故障都应该完全归因于这次合并,因为用于合并的两个父版本都应该是绿色的(译者注:即没有故障,在套件中测试通过显示为绿色)。知道这一点将有助于她追踪问题,因为她可以查看差异以寻找线索。

通过这个构建和测试,她已经成功地把主线拉到了她的代码线,但是——还有一件既重要又常常被人忽略的事——她还没有完成与主线的集成。要完成集成,她必须将所做的更改推入主线。如果她不这么做,团队中的其他人都将与她的变更隔离开来——本质上没有集成。集成既是拉取也是推送——只有在 Scarlett 把更改推入主线之后,她的工作内容才与项目中的其余部分集成。

f25aa2e3537c741a6c575761747cf20f.png

现在许多团队在将代码提交添加到主线之前,需要一个代码评审的步骤——我称之为“提交评审”模式,后面会进行讨论。

有时候,在 Scarlett 进行推送前,其他人会和主线集成。在这种情况下,她必须再次拉取和合并分支。通常,这只是一个偶然的事件,在不需要任何进一步协调的情况下就可以被解决。我见过长时间构建的团队使用集成接力棒,这样只有持有接力棒的开发人员才能集成。但是近年来,随着构建时间的缩短,我还没有听到太多这样的情况。

2.1.1 适用场景

顾名思义,只有当我们在产品上使用主线时,我才能使用主线集成。

使用主线集成的一个替代方法是从主线拉取这些变更,合并到个人开发分支中。这可能是有用的——至少拉取时可以让 Scarlett 意识到其他人已经集成了变更,并发现她的工作和主线之间的冲突。但是,在 Scarlett 推送上传之前, Violet 将无法发现她的工作内容与 Scarlett 的变更之间有任何冲突。

当人们使用“integrate(集成)”这个词时,他们往往忽略了一个要点。经常听到有人说,他们正在集成主线到他们的分支,而实际上他们只是在从主线拉取。我已经学会了对此保持警惕,并进一步确认,看看它们是指拉取还是真正的主线集成。两者的结果是有很大差异的,所以不要混淆术语是很重要的。

另一种选择是,当 Scarlett 在做的一些工作还没有准备好与团队其他成员的工作完全集成,但和 Violet 的有重叠之处,并想和她一起共享。在这种情况下,他们可以开启一个协作分支。

2.2 特性分支开发 ✣

为某个功能特性建立独立的分支,在该分支上完成与该特性相关的所有工作,在功能特性完成后集成到主线中。

按照特性分支开发这种模式,当开发人员要开始开发某个功能特性时,他们会开启一个分支,并持续在这个分支上工作直到功能特性完成,然后再与主线集成。

例如,让我们来看下 Scarlett。她领取的是一个给他们的网站中增加本地营业税集合的功能。她从产品最新的稳定版本开始,从主线拉取到她的本地仓库,然后从当前主线的顶端创建一个新的分支。不管多久,她会为完成这个功能,在这个本地分支上进行一系列提交。

1df7d394194f3940af782dad06b4ec1a.png

她可能会将该分支推送到项目仓库,以便其他人可以查看她的更改。

当她在工作时,主线上也会有其他提交。因此,她可能要不时地从主线拉取版本,以便获知是否有任何改变可能会影响她正在开发的功能。

52d0467e4b5995e386b6075e6f270672.png

请注意,这不是我们上文说过的集成,因为她没有推送回主线。在这个点上,只有她在看自己的工作内容,其他人则没有。

一些团队希望确保所有代码都保存在中央仓库中,无论这些代码是否已被集成。在这种情况下,Scarlett 会将她的特性分支推送到中央仓库中。这将允许其他团队成员查看她正在进行的工作,即使该工作尚未集成到其他人的工作中。

当她完成了这个功能特性的开发后,她将执行主线集成,将这个功能特性集成到产品中。

c979c817c004666728bea529d30beae6.png

如果 Scarlett 同时进行多个功能特性的工作,那她将为每个特性开启一个独立的分支。

2.2.1 适用场景

特性分支开发是如今业界一种流行的模式。要讨论何时使用它,我需要介绍它的主要替代方案——持续集成。但是首先我要谈谈集成频率的作用。

2.3 集成频率 ✣

我们进行集成的频率对团队的运作有着显著的影响。《DevOps现状调查报告》的研究表明,精英开发团队的集成频率要比绩效低下的团队高得多 —— 这一观察结果符合我和众多业界同行的经验。我将通过由 Scarlett 和 Violet 为主角的两个集成频率的案例来说明这一点。

2.3.1 低频集成

我先从低频集成的示例开始。在这里,我们的两个主人公从克隆主线到各自的本地分支展开工作,然后各自执行了几个还不愿推送的本地提交。

c683b1f4fd94dab016b068df44e552a7.png

当他们工作时,另外有人向主线进行了一个提交。(我不能很快想出另一个人名,那是一种颜色,就叫Grayham?)

4519c05b56933f70791ab3198aa96981.png

这个团队通过保持一个健康分支,并在每次提交后拉取主线代码进行团队协作。Scarlett 的前两个提交没有任何新代码可拉取,因为当时主线没有变化,但现在她需要拉取标记为 M1 的代码。

30dd4dd60db438d6975e632fc746f151.png

我用黄色框标记了此次合并。这次是将 S1 到 S3 与 M1 合并。很快,Violet 需要做同样的事情。

e938d0d3bd2c98be3e77352f1262656d.png

这时,两个开发人员的本地代码都已跟上主线的变化,但由于他们的本地代码彼此隔离,所以他们尚未彼此集成。Scarlett 不知道 Violet 在 V1 到 V3 的更改。

Scarlett 进行了更多的本地提交,准备好了进行主线集成。对她来说,这是一个轻松的推送,因为她较早拉取了 M1。

c442babcaee7bc64ef143fecdab5cffb.png

而 Violet 的操作则更为复杂。当她进行主线集成时,她现在需要集成 S1..5 与 V1..6。

e35c2464d6c12812383c6c7f52c19d37.png

我已经根据涉及的提交个数科学地计算了合并工作量的大小。然而,即使你没注意到上图的那些舌状凸起,你也会意识到 Violet 的合并很有可能比较困难。

2.3.2 高频集成

在前面的示例中,我们两个多彩的开发人员是在进行了几个本地提交之后集成的。让我们看看如果他们在每个本地提交之后进行主线集成会发生什么。

当 Violet 在第一个本地提交后就立即集成到主线时,第一个变更是显而易见的。由于主线没有任何更改,因此这就是一个简单的推送。

a72f83b921d7c8766f01b7b49153d698.png

Scarlett 的第一个提交也需要主线集成,但是由于 Violet 先进行了集成,因此 Scarlett 需要做一次合并。但是由于她只需合并 V1 与 S1,所以合并的工作量很小。

7c2fa0333cc824e09acb218b7093ac0c.png

Scarlett 的下一个集成是一个简单的推送,这意味着 Violet 的下一个提交也将需要与 Scarlett 的最近两个提交合并。但这仍然是一个很小的合并,仅仅是 Violet 的一个提交和 Scarlett 的两个提交的合并。

fda735b221a3e98af6537eaf8b3516f0.png

当有外部的提交推送到主线时,它会按照 Scarlett 和 Violet 正常的集成节奏被提取过来。

7db3af5c1e4ac7ded884a9dae3ee421b.png

尽管它与以前发生的情况相似,但集成难度较小。Scarlett 这次只需要将 S3 与 M1 集成在一起,因为 S1 和 S2 已经在主线上了。这意味着 Grayham 在推 M1 之前就必须集成主线上已经存在的内容(S1..2,V1..2)。

开发人员继续进行剩余的工作,并在每次提交时进行集成。

be56149003c812bf49eab727f3c633aa.png

2.3.3 集成频率对比

让我们再整体看一下这两张图

637c1b849348cc9313d6346a76af37af.png

(低频)

06b8f65729b7a0626f197c404c0b68e2.png

(高频)

这里有两个非常明显的区别。

首先,顾名思义,高频集成意味着做更多的集成——在这个小例子中,后者集成次数是前者的两倍。但更重要的是,这些集成比低频例子中的集成要小得多。较小的集成意味着更少的工作量,因为可能引起冲突的代码更改会更少。但是比减少工作量更重要的是,它也降低了风险。大规模合并的问题与其认为是处理合并产生的工作量,还不如说是这里面的不确定性。多数情况下,大规模的合并也会很顺利,但有时候,大规模合并会非常非常糟糕。偶尔的痛苦最后会比常态化的痛苦更糟。

如果比较两种情况,一种是每次集成需要额外花费 10 分钟,另一种是有 1/50 的概率需要花费 6 小时做一次集成修复——我更喜欢哪个?如果仅看花费工作量,那么 1/50 看起来更好,因为它是 6 小时而不是 8 小时 20 分钟。但是不确定性使 1/50 的案例变得更加糟糕,这种不确定性会导致集成恐惧。

2.3.4 集成恐惧

当团队获得一些糟糕的合并体验时,他们往往会更谨慎地进行集成。这很容易变成一种正反馈回路——像许多正反馈回路一样,有着非常消极的后果。(译者注:正反馈回路也叫自增强回路,是一种叠加增强的过程)

最明显的结果是,团队进行集成的频率降低了,这会导致更严重的合并冲突,而合并冲突会导致更低的集成频率……从而陷入恶性循环。

一个更加不易察觉的问题是,团队会停止执行那些他们认为会使集成变得更加困难的事情。尤其是,这会让他们抗拒重构。但是减少重构会导致代码库变得越来越不健康,难以理解和修改,从而降低了团队的功能特性交付速度。由于完成功能特性所需的时间更长,因此进一步增加了集成频率(译者注:原文可能有误,这里应该是降低集成频率),从而使这种正反馈环路变得更不堪一击。

这个问题有个反直觉的答案——“如果一件事令人痛苦……那就更频繁地去做它”

让我们从另一个角度来看这些频率之间的差异。如果 Scarlett 和 Violet 在第一次提交时发生冲突,会发生什么?他们将在何时发现出现了冲突?在低频的例子中,直到 Violet 最后一次合并,他们才发现冲突,因为那是 S1 和 V1 第一次放到一起。但是在高频的例子中,在 Scarlett 的第一次合并中就会发现它们。

c1f0394486d5abf73eaa1392f950c9d6.png

(低频)

332435e03b5be994dfa83e7429cc0116.png

(高频)

频繁的集成会增加合并的频率,但可以降低合并的复杂性和风险。频繁的集成还可以提醒团队更快地解决冲突。当然,这两件事是联系在一起的。糟糕的合并通常是团队工作中隐藏着冲突的结果,只有在进行集成时才浮现出来。

比如 Violet 正在看账单计费功能,并且看到代码的编写者有按一种特定的税收制度评估税额。而她的功能特性需要用不同的方式处理税额,因此最直接的方法是将税额从账单的计算中剔除,一会儿再把它作为独立的功能进行开发。计费功能仅在少数的几个地方被调用,因此使用“ 搬移语句到调用者”(译者注:《重构:改善既有代码的设计》8.4 )进行重构很容易——这让程序在未来的演进中更为合理。然而,Scarlett 不知道 Violet 正在做这件事,她按账单函数处理税款的假定实现她的功能特性。

自测代码是我们的救命稻草。如果我们有一个强大的测试套件,把它作为健康分支的一部分使用,将可以发现那些冲突,从而让问题进入生产环境的可能性大大降低。但是,即使有强大的测试套件充当了主线的看门人,大规模集成依然令人头疼。我们需要集成的代码越多,发现问题的难度就越大。我们也会有更大的概率遇到各种各样妨碍运行且难以理解的问题。除了通过较小的提交来降低影响,我们还可以使用“差异调试”来帮助定位哪一次变更导致问题。

很多人没有意识到的是,源代码控制系统其实是一种交流工具。它使 Scarlett 可以看到团队中其他人在做什么。通过频繁的集成,她不仅会在出现冲突时立即得到警告,而且她还能更了解每个人都在干什么,以及代码库是如何演进的。我们不是一个人在冲锋,而是和团队在一起工作。

增加集成频率是缩减功能特性大小的重要原因,同时这还有其他优点。功能越小,构建速度越快,投生速度越快,价值交付的启动也就越迅速。此外,较小的功能特性减少了反馈时间,使团队可以在更加了解客户后做出更好的功能决策。

2.4 持续集成 ✣ 

一旦有可共享的健康提交,开发人员就进行主线集成,这样的工作量通常是不到一天。

一旦团队在体验到高频集成既高效又轻松后,很自然地就会问“我们的集成频率能有多快?”。特性分支意味着变更集粒度的下限 —— 不可能有比内聚的特性更小的粒度。

持续集成为集成提供了一种不同的触发方式——只要在特性功能开发上取得了大的进展,并且分支仍然健康,就可以集成。我们不指望功能特性已经完整实现,只要对代码库有足够的修改就行。经验法则是“每个人每天都要提交到主线”,或者更确切地说,“本地代码库中永远不要存放超过 1 天未经集成的代码”。实际上,大多数持续集成的践行者每天会多次集成,他们乐于集成 1 小时或更少的工作。

要了解更多关于如何有效持续集成的详细信息,请查看我的详细文章。欲了解更多细节,请查阅 Paul Duvall, Steve Matyas 和 Andrew Glover 的著作。Paul Hammant 维护了一个名为trunkbaseddevelopment.com的网站,其中有很多持续集成的技术。

使用持续集成的开发人员需要习惯集成半成品达成频繁集成的想法。他们还要考虑如何在运行的系统中不暴露半成品来做到这一点。通常这并不复杂——如果我正在实现一个依赖优惠码的折扣算法,而这个优惠码还不在有效列表中,那么我的代码就不会被调用,即使已经是生产版本。

同样,如果我添加了一个功能,询问保险索赔人是否是吸烟者,我可以构建和测试代码背后的逻辑,并通过将询问问题的用户界面留到构建这个特性的最后一天再做,来确保它不会在生产中被使用。通过最后连接接口映射(Keystone Interface)来隐藏半成品通常是一种有效的技术。

如果没法轻松地隐藏掉半成品,我们可以使用特性开关。除了隐藏半成品之外,特性开关还可以有选择地向其中一部分用户显示某一功能特性——这通常便于逐步推出一个新的功能特性。

集成半成品尤其会引起那些担心主线中有错误代码者的忧心。因此,使用持续集成需要自测代码,这样就有信心把半成品合并到主线,而不会增加出现错误的几率。这种方法要求开发人员在编写功能代码时,为半成品编写测试,并将功能代码和测试一起提交到主线中 (或许可以用测试驱动开发)。

就本地代码库而言,大多数使用持续集成的人不会想要在单独的本地分支上工作。通常是直接在本地 master 分支上提交,工作完成后进行主线集成。然而,如果开发人员喜欢的话,开一个特性分支并在上面工作,每隔一段时间就集成回本地 master 分支和主线,那也相当不错。特性分支开发与持续集成之间的区别,不在于是否有特征分支,而是在于开发人员何时与主线集成。

2.4.1 适用场景

持续集成是特性分支开发的另一种选择。两者之间的权衡值得在本文中用单独的章节描述,下面将对这两者进行对比。

持续集成和基于主干开发

在 ThoughtWorks 于 2000 年开始使用持续集成时,我们编写了 CruiseControl, 这是一个守护程序,每当有代码提交到主线后,就会自动构建软件产品。从那时起,许多这样的工具 (如 Jenkins、TeamCity、Travis CI、Circle CI、Bamboo 等等) 被开发出来。但是大多数使用这些工具的组织都是在提交时自动构建特性分支——这虽然有用,但也意味着这些组织并非真正在实践持续集成。(还不如叫它们持续构建工具。)

因为这样语义扩散,有些人开始使用 “主干开发” 一词来代替“持续集成”。(部分人确实对这两个术语进行了细微的区分,但是没有一致的用法)。虽然在语言方面我通常是描述派,但我更喜欢使用 “持续集成”。一部分原因是我不认为试图不断提出新术语是对抗语义扩散的可行方法。然而,或许更主要的原因是,我认为改变术语将粗暴地抹杀早期的极限编程先驱者们的贡献,尤其是 Kent Beck 的,他在 20 世纪 90 年代创造并明确定义了持续集成的实践。

2.5 对比特性分支开发和持续集成 ✣ 

目前,特性分支看起来是业界最常见的分支策略,但是一些实践者强烈认为持续集成是一种更好的方法。持续集成的主要优势是支持更高的集成频率,而且通常是高很多的集成频率。

集成频率的差异取决于团队能够把功能拆分到多小。如果团队拆分的所有功能特性都可以在一天之内完成,那么他们既可以实行特性分支开发,也可实行持续集成。但是大多数团队的特性持续时间都比这更长——特性持续的时间越长,这两种模式之间的差异就越大。

正如我已经指出的那样,更高的集成频率可以减少复杂的集成,并减少对集成的恐惧。这通常是一件很难沟通的事情。如果你生活在每隔几周或几个月进行集成的世界中,那么集成很可能是一项令人焦虑的活动。很难相信一天可以进行很多次集成。但集成是可通过加快频率降低难度的事情之一。

这是一种违反直觉的想法——“如果一件事令人痛苦——那就更频繁地去做它”。集成的规模越小,集成就越不可能变成充满痛苦和绝望的史诗般的合并。对于特性分支开发,高频集成鼓励更小的特性规模:几天而不是几周(几个月根本行不通)。

持续集成使团队可以从高频集成中受益,同时将特性规模与集成频率解耦。如果团队更喜欢一两个星期的特性粒度,持续集成支持这样的粒度拆分,同时仍让团队获得最高集成频率的所有好处。合并规模越小,所需的工作越少。

更重要的是,正如我在上文中所解释的,更频繁地进行合并可以减少出现极为糟糕的合并的风险,这既消除了这种合并带来的惊吓,也减少了合并的整体恐惧感。如果代码中出现冲突,则高频集成会在导致这些讨厌的集成问题之前迅速发现它们。持续集成可为团队带来极强的效益,以至于有的团队,有些功能只需几天完成,还依旧在做持续集成。

持续集成的明显缺点是,缺乏向主线进行最重要的集成的封闭。如果一个团队不善于保持健康的分支,这不仅是一个对失败的庆祝(译者注:庆祝失败是为了改进),更是一个风险。将一个功能特性的所有提交聚在一起,还可以在后期决定是否在即将发布的版本中包含一个特性。虽然功能开关允许从用户角度打开或关闭功能,但该功能的代码仍在产品中。

对这一点的担忧通常会被过分夸大,毕竟代码不会太重要,但这确实意味着想要实行持续集成的团队必须开发一组强大的测试集,以便他们可以确信主线能保持健康,即使每天进行多次集成。有些团队觉得这种技能是难以想象的,但另一些团队则认为这不仅是可能的而且游刃有余。此先决条件确实意味着,特性分支开发这种方式更适合那些不强制保持健康分支、并且需要用发布分支在发布之前稳定代码的团队。

虽然合并的规模大小和不确定性是特性分支开发最明显的问题,但最大的问题可能是特性分支开发遏制重构。定期进行且几乎没有冲突的重构最为有效。重构会引入冲突,如果这些冲突没有被发现并迅速解决,合并就会变得困难重重。因此,重构在高频集成中效果最好,所以重构作为极限编程的一部分流行起来也不足为奇,而且持续集成也是极限编程最初的实践之一。特性分支开发也不鼓励开发人员做当前特性外的更改,这会破坏团队的重构能力,影响代码库稳定性的提升。

当我遇到有关软件开发实践的科学研究时,由于他们的方法学存在严重问题,通常我并不买账。但 《DevOps 现状调查报告》是一个例外,该报告揭露了软件交付效能的度量指标,并将其与更广泛的组织绩效度量相关联,而组织绩效又与投资回报率和盈利能力等业务度量指标相关。在2016年,他们首先评估了持续集成,发现它有助于提高软件开发效能,此后的每项调查中都重复印证了这一发现。

我们发现在合并到主干之前,具有极短生命周期(少于一天)的分支或派生,并且总共少于三个活动分支,是持续交付的重要特征,并且所有这些都有助于提高绩效。每天将代码合并到主干或 master 中也是如此。

——《 2016 年 DevOps 现状调查报告》

使用持续集成并不会消除保持功能粒度小的其他优势。频繁发布小的功能特性可提供快速的反馈周期,从而为改进产品创造奇迹。许多使用持续集成的团队还在努力构建产品的分层,并尽可能频繁地发布新功能。

fc47689d0317daf6ebc3de106466a5ce.png

2.5.1 特性分支和开源 

许多人将特性分支开发的流行归因于github 和起源于开源开发的拉取请求模型。有鉴于此,有必要了解一下开源工作与许多商业软件开发之间截然不同的环境。开源项目的结构有许多不同的方式,但是一个常见的结构是一个人或一小群人作为开源项目的维护者,承担大部分编程工作。维护者与更多的开发贡献者一起工作。维护者通常不了解贡献者,因此对他们贡献的代码的质量一无所知。维护者还不确定贡献者将在开源项目中实际投入多少时间,更不用说他们的工作成效。

在这种情况下,特性分支开发非常有意义。如果有人要添加一个或大或小的功能,而我不知道这项功能什么时候(或者是否)会被完成,那么对我来说,等到它完成后再集成是有意义的。另外,更为重要的是要能够审核代码,以确保它通过我为代码库设置的任何质量门禁。

但是许多商业软件团队的工作环境截然不同。有一个全职的团队,他们全都为软件开发投入大量时间,通常是全职的。项目负责人非常了解这些人(除了刚开始的时候),并且可以对代码质量和交付能力有可靠的预期。由于他们是带薪雇员,项目负责人对项目投入的时间,编码标准和团队习惯也有更好的掌控。

在这迥然不同的环境下,应该清楚地知道,此类商业团队的分支策略不必与在开源世界运用的分支策略相同。持续集成几乎不可能适合偶尔为开源工作做出贡献的人,但是对于商业工作而言,这是一个现实的选择方案。团队不应假定那些在开源环境行得通的做法可以自动适应他们与之不同的工作环境。

2.6 对提交评审 ✣ 

每个对主线的提交都要先经同行评审才会被接纳。

长期以来,代码审查一直被推荐用于提升代码质量,提高模块化和可读性,以及消除缺陷。尽管如此,商业机构往往发现很难把代码审查融入到软件开发工作流程中。然而,开源世界广泛采用了这样的信念:在项目贡献被接受纳入项目主线之前,应先对其进行评审,并且这种方式近年来在开发组织中广泛传播,尤其是在硅谷。这样的工作流程特别适合 GitHub 的拉取请求机制。

类似这样的流程会在 Scarlett 完成希望被集成的工作内容时开始。一旦她成功完成构建,就要进行主线集成(如果她的团队有这样的惯例),但是在推送到主线前,她要先发送她的提交进行评审。团队的其他成员,例如 Violet,接着对这个提交进行代码审核。如果她认为提交有问题,会反馈一些意见,然后会有一些反复,直到 Scarlett 和 Violet 都满意为止。提交只有在通过评审后才会被纳入主线。

对提交评审(Reviewed Commits)在开源中越来越受欢迎,它非常适合由提交维护者和临时贡献者组成这样模式的组织。对提交评审使得维护人员可以密切关注任何一个贡献,也非常适合特性分支开发,因为一个完成的特性清晰地标记出需要代码评审的节点。如果您不确定贡献者是否完成了功能,为什么还要评审他们的半成品?最好还是等功能完成时再做。这种做法在更大的互联网公司中也广泛传播,Google 和 Facebook 都开发有专用工具支持平滑开展对提交评审。

约定及时对提交评审的行为准则非常重要。如果开发人员完成了某项工作,并花了几天时间进行其他工作,那么当他们收到返回的评审意见时,他们对被评审工作的印象已经不再清晰。如果被评审的提交是已经完成的功能,这会令人沮丧,但对于部分完成的功能,情况会严重得多,因为在确认评审通过之前,工作可能很难进一步开展。

理论上,可以结合对提交评审来进行持续集成,而且实践上也确实是有可能的—— Google 就遵循这个方法。但是,尽管可能,但很难执行,而且相对罕见。对提交评审和特性分支开发是更为常见的组合。

2.6.1 适用场景

将开源软件和私有软件开发团队的需求混为一谈就像是当前软件开发仪式的原罪。

—— Camille Fournier

尽管在过去十年中,对提交评审已成为一种流行的做法,但仍有弊端和替代方案。即使做得很好,对提交评审也总是会在集成过程中引入一些延迟,从而导致了更低的集成频率。结对编程提供了持续的代码审核过程,带来比等待代码评审更快的反馈周期。(就像持续集成和重构一样,结对编程是极限编程最初的实践之一)。

许多使用对提交评审的团队并没有做到足够迅速。他们能够提供有价值的反馈往往因为来得太迟而不再有效。那时就会面临一个令人尴尬的选择,要么大量返工,要么接受能行得通但损害代码库质量的工作。

代码评审并不局限于只在代码合入主线前进行。许多技术领导者发现在提交后评审代码会很有用,当他们发现问题时,就可以及时与开发人员联系。重构文化在这里是非常有价值,做得好可以形成一种社区氛围,团队中的每个人都将定期评审代码库中的代码并修复他们看到的问题。

围绕对提交评审的利弊权衡主要取决于团队的社会结构。正如我已经提到的,开源项目通常具有一些受信任的维护者和许多不受信任的贡献者的结构。商业团队通常都是全职的,但结构可能相似。项目负责人(类似于一个维护者)信赖一小组(也可能是某个)维护者,并且对团队其他成员贡献的代码保持警惕。团队成员可能同时分配到多个项目中,使他们更像开源贡献者。如果存在这样的社会结构,那么对提交评审和特性分支开发将具有很大的意义。但是,团队在互相具有较高信任度时,通常能找到机制来保持代码高质量,且不会增加集成过程的冲突。

因此,尽管对提交评审可以是一种有价值的实践,但并不是通向健康代码库的必要途径。如果你希望团队平衡成长,而不过度依赖其最初的领导者时尤其如此。

2.7 集成阻力 ✣ 

对提交评审的问题之一,是它往往让集成变得更加麻烦。这是集成阻力(Integration Friction)的一个例子——这些活动让集成耗时或费力。集成阻力越多,开发人员就越倾向于降低集成频率。想象某个 (功能不健全的) 组织坚持认为所有对主线的提交都要填写一份需要耗时半小时的表格。这样的制度会阻碍人们频繁集成。无论你对特性分支开发和持续集成的态度如何,审视任何增加这种冲突的东西都是有价值的。任何这样的冲突都应该被移除,除非它有明显的增值作用。

拉取请求增加了额外的开销以应对低信任度情景,例如,允许你不认识的人为你的项目做出贡献。而把拉取请求强加给你自己团队中的开发人员,就像让你的家人通过机场安检进入你家一样。

—— Kief Morris

手动过程是这里常见的冲突源,尤其是当涉及与不同组织的协调时。这种摩擦通常可以通过使用自动化流程、加强开发人员培训 (以消除需求) 以及将步骤推到部署流水线或生产中质量保证的后续步骤来减少。您可以在关于持续集成和持续交付的资料中找到更多消除这种冲突的方法。这种冲突也会在生产的路径中出现,有着同样的困难和处理方法。

让人们不愿意考虑持续集成的原因之一是,设想他们只在集成阻力严重的环境中工作过。如果做一次集成需要一个小时,那么一天做几次集成显然是荒谬的。而如果加入一个团队,在那里集成是一个分分钟可以完成的小事,就会感觉像是一个完全不同的世界。关于特征分支开发和持续集成优点的许多争论是混乱复杂的,我怀疑就是因为人们没有经历过这两个世界,因此不能完全理解这两种观点。

文化因素影响集成阻力——尤其是团队成员之间的信任。如果我是一个团队的领导者,而我不信任我的同事会做得很好,那么我很可能会想要阻止损害代码库的提交。这自然也是对提交评审的驱动因素之一。但如果我在信任同事判断的一个团队里,那么我可能会更愿意接受提交后的审查,或者完全砍掉审查,而去依靠集体重构来解决问题。在这种环境下,我的收获是消除了提交前评审所带来的摩擦,从而鼓励了更高频率的集成。团队信任通常是特性分支与持续集成争论的最重要因素。

2.8 模块化的重要性 ✣ 

大多数关心软件架构的人都会强调模块化对一个行为良好的系统的重要性。如果我面临着对一个模块化程度较差的系统做一个小的改动,我必须理解几乎所有的模块,因为即使是一个小的改动也会波及到代码库的许多部分。然而,如果模块化程度好,我只需要理解一两个模块的代码,再理解几个模块的接口,就可以忽略其他的模块。这种能力能够减少我需要在理解上花费的精力,这就是为什么随着系统的发展,值得在模块化上投入这么多精力的原因。

模块化也会影响集成。如果一个系统有很好的模块,那么大多数时候Scarlett和Violet会在代码库中分离良好的部分工作,他们的变化不会引起冲突。良好的模块化还可以增强接口映射(Keystone Interface)和抽象分支(Branch By Abstraction)等技术,以避免因分支而产生的对隔离的需求。通常团队被迫使用源分支,是因为缺乏模块化使他们没有其他选择。

特性分支是一种低级的模块化方法,它们要通过手动合并把自己结合到提供这种机制的源码控制系统里,而不是构建一个能够在运行/部署时轻松交换特性的系统。

—— Dan Bodart

支撑应该是双向的。尽管做了很多尝试,但在我们开始编程之前,建立一个好的模块化架构仍然是非常困难的。为了实现模块化,我们需要在系统成长的过程中不断观察系统,并使其趋向于更加模块化的方向。重构是实现这一目标的关键,而重构需要高频率的集成。因此,模块化和快速集成在一个健康的代码库中是相互支撑的。

这都是说,模块化虽然很难实现,但值得努力。这种努力包括良好的开发实践,学习设计模式,以及从代码库的经验中学习。混乱的合并不应该只是因为某个可以理解的愿望来封闭和遗忘——而是要问为什么合并是混乱的。这些答案往往会成为如何改进模块化的重要线索,改善代码库的健康状况,从而提高团队的工作效率。

2.9 个人对于持续集成模式的看法 ✣ 

作为作家,我的目的不是要说服您遵循特定的路线,而是要告诉你,当您决定选择哪种道路时,应该考虑的因素。尽管如此,我还是在这里表明我的观点,我更喜欢前面提到的哪种模式。

总的来说,我更喜欢在一个实行持续集成的团队中工作。我认识到环境是关键因素,在很多情况下,持续集成并不是最好的选择——但我的反应是努力改变这种环境。之所以我有这种偏好,是因为我想工作在这样一种环境中,每个人都可以轻松地不断重构代码库、提升代码库的模块化程度,保持代码库的健康状态——所有这些都是为了使我们能够快速响应不断变化的业务需求。

如今,我更像是一个作家,而不是一个开发者,但我仍然选择在 ThoughtWorks 工作,这家公司到处都是喜欢这种工作方式的人。这是因为我相信这种极限编程风格是我们开发软件最有效的方式之一,我想观察团队进一步发展这种方式,以提高我们这个行业的效率。

三、从主线到生产发布的路径

0d0978096265363224b106853e69daf5.png

主线是一个活跃的分支,定期会有新修改的代码加入。保持主线的健康很重要,这样人们就能在一个稳定的基础上开始新的开发工作。当它足够健康时,你也可以直接从主线向生产环境发布代码。

1498e717767d001e78022e9e85bebc67.png

这种使主线始终处于可发布状态的理念是持续交付的核心原则。要做到这一点,必须具备将主线维护为健康分支的决心和技术手段,通常使用部署流水线来支持所需的密集测试。以这种方式工作的团队通常可以在每个发布版本上用标签来跟踪他们的发布。而不实施持续交付的团队则需要用另一种方法。

3.1 发布分支 ✣

一个只接受特定提交的分支,这些提交因稳固产品的某个待发布版本而被接受。一个典型的发布分支会从当前主线复制,但不允许向该分支添加任何新的功能特性。这些新的功能特性会由主力开发团队继续向主线添加,并被未来的某个发布获取。在发布分支工作的开发人员则专注于消除那些影响该发布生产就绪的缺陷。任何对这些缺陷的修复都将在发布分支上创建并合并到主线。一旦不再有需要修复的缺陷,该分支就可被用于生产发布。

6864de757e2c3ad1529f0517c487f4c7.png

尽管在发布分支上修复错误比开发新的功能特性代码工作量小(希望如此),但随着时间的推移,将这些修复合并回主线将变得越来越难。分支不可避免地会偏离,随着对主线的提交增多,将发布分支合并到主线变得越来越难。

以这种方式向发布分支提交有一个问题,就是很容易忽略将这些提交复制到主线,特别是在因分支偏离造成合并越来越难的时候。由此产生的回退令人左右为难。因此,有些人倾向于在主线上创建提交,并且只有当这些提交在主线可以工作时,才会被拣选(cherry-pick)到发布分支。

c586ba0fd7dc7835ffab5a1d70351d1b.png

拣选(cherry-pick)是指将某个提交从一个分支复制到另一个分支,但分支并没有被合并。也就是说,仅复制一个提交,而不是复制自分支分叉点开始的多个先前的提交。在上图中的示例中,如果我要将 F1 合并到发布分支中,就会包括 M4 和 M5。但是通过拣选,可以做到只获取 F1 的改动。拣选可能无法完全适用于发布分支,因为它可能依赖于先前提交的更改,如本例中的 M4 和 M5。

在主线上编写发布修复的缺点是:许多团队发现这样做会更加困难,并且令人沮丧的是,在主线上以某种方式修复后,在发布之前又不得不在发布分支上重复同样的操作。在有发布进度压力时,尤为如此。

单一生产版本的团队只需要一个发布分支,但有些产品在生产使用中有多个发布版本并存。在客户的配套设备上运行的软件只有在客户愿意时才会被升级。许多客户不愿意升级,除非有必须的新功能,因为他们曾因升级失败遭受过损失。然而,这些客户仍然希望修复缺陷,特别涉及到安全问题的缺陷。在这种情况下,开发团队会为每个仍在使用的发布保留发布分支,并根据需要对这些分支进行修复。

0b8e00e23df885f58862f0a09b642df1.png

随着开发的进行,对旧版本进行修复变得越来越困难,但这通常是开展业务的代价,只能通过鼓励客户频繁升级到最新版本来缓解。为此保持产品稳定至关重要,一旦遇到升级受挫,客户可能会不愿意再做不必要的升级。

我听过的其他发布分支的术语有:“预发布分支(release-preparation branch)”、“稳定分支(stabilization branch)”、“候选分支(candidate branch)”和“固化分支(hardening branch)”。但“发布分支”似乎是最常见的。

3.1.1 适用场景

当团队无法保持主线处于健康状态时,发布分支是一个有价值的工具。它使团队的部分成员能够专注于那些必要的缺陷修复,这些缺陷修复是使发布达到生产就绪状态所必须的。测试人员可以从这个分支的顶端拉取最新、最稳定的待发布代码。每个人都可以看到为了稳定产品做了哪些改动。

尽管发布分支很有价值,但大多数最优秀的团队并不会在单件生产(single-production)的产品上采用这种模式,因为他们不需要。如果主线保持足够健康,那么任何对主线的提交都可以直接发布。在这种情况下,应该使用公开可见的版本构建编号( version and build number)标记发行版本。

你可能已经注意到,我在上一段中用了一个笨拙的形容词 “单件生产”。这是因为当团队需要同时管理多个生产版本时,这种模式变得至关重要。

当发布过程存在显著的阻力时,发布分支也可能会很方便,例如,所有生产发布都需经发布委员会批准。正如 Chris Oldwood 所说,“在这些情况下,发布分支更像是一个等待公司齿轮缓慢转动的隔离区”。一般来说,在发布过程中应该尽可能地消除这种阻力,就像我们需要消除集成阻力一样。然而,在某些情况下这并不可行,比如移动应用商店。在很多类似情况下,多数时候一个标签就已足够,只有当需要对代码进行一些关键的更改时,才会打开分支。

发布分支也可以是环境分支(Environment Branch),这取决于使用该模式的关注点。还有一个变体,长期存在的发布分支(long-lived release branch),我将在稍后介绍。

3.2 成熟度分支 ✣

一个分支,其最新引用就是代码库某一级别成熟度的最新版本。

团队通常希望知道源代码的最新版本是什么,但事实可能会因为代码库的成熟度不同而变得复杂。一位质量保证工程师可能希望查看产品最新的试运行版本,而生产故障的调试人原则希望查看最新的生产版本。

成熟度分支提供了一种进行这种跟踪的方式。一旦某个代码库的版本达到一定程度的就绪状态,它就会被复制到特定的分支中。

考虑一个用于生产的成熟度分支。当我们要准备一次生产发布时,我们会打开一个发布分支来稳定制品。一旦准备好,我们就把它复制到一个长期运行的生产分支上。我之所以认为这是复制而不是合并,是因为我们希望生产代码与在上游分支上测试的代码完全相同。

28693b98ace7c02535744b8ebf19f6e4.png

成熟度分支的吸引力之一是,它清楚地显示了在发布工作流中达到该阶段代码的每个版本。因此,在上面的示例中,我们只需要向生产分支进行一次组合了 M1-3 和 F1-2 的提交。这里也有几个源代码控制管理的小技巧(SCM-jiggery-pokery)可以实现这一点,但无论如何,这都丢失了和主线上那些细粒度提交的联系。而这些提交应该被记录在提交消息中,以帮助人们事后追溯它们。

成熟度分支通常以开发流程中的相应阶段命名,比如像“生产分支(production branch)”、“试运行分支(staging branch)”和“测试分支(QA branch)”之类的术语。偶尔也听说人们把生产成熟度分支称为“发布分支”。

3.2.1 适用场景

源代码控制系统支持协作和跟踪代码库的历史信息。使用成熟度分支,人们可以通过显示发布工作流程中特定阶段的版本历史来获取一些重要的信息。

通过查看相关分支的最新引用,我就可以找到所需的最新版本,比如当前正在运行的生产代码。如果出现了一个我确定之前不存在的缺陷,我可以查看该分支的历史版本以及生产中特定代码库更改。

自动化可以与特定分支上的变更绑定——例如,每当生产分支有新的提交时,自动化流程就可以将版本部署到生产环境中。

使用成熟度分支的一个替代方法是采用标签方案。一旦一个版本为 QA 做好了准备,就可以像这样对其进行标记——通常是以包含一个构建版本号的方式进行打标签。因此,当 762 号构建为 QA 做好准备时,它可被打上“qa-762”的标签;当该构建生产就绪时,它可被标记为“prod-762”。然后,我们可以通过搜索代码库查找和我们标签方案相匹配的标签,获得历史记录。自动化同样可以基于标签分配。

因此,尽管成熟度分支可以为工作流程增加一些便利,但许多组织发现打标签也可以工作得很顺畅。所以我认为这是一种没有很强收益或成本的模式。然而,如果需要使用源代码管理系统来进行这样的跟踪,也标志着该团队部署流水线的工具并不完善。

3.2.2 变体:长期存在的发布分支

我可以把这看作是发布分支模式的一个变种,它结合成熟度分支一起作为发布的候选版本。当我们想要做一次发布时,我们从主线复制到这个发布分支。就像每一个发布分支一样,提交只在发布分支上进行,以提高稳定性。这些修复也会被合并到主线中。当一个版本发布时,我们会给它打上发布的标签,当想要发布另一个版本时,我们可以再次复制主线。

601270fe71db5323c269122d277969ca.png

提交可以像成熟度分支中经典的做法那样复制,或者合并进来。如果合并进来,我们必须注意让发布分支的最新引用和主线的最新引用完全匹配。一种方法是在合并前重置所有已应用于主线的修复。有些团队也会在合并后限制提交,以确保每个提交都代表一个完整的发布候选版本。(觉得这很棘手的人会有充分的理由倾向于为每个版本剪一个新的分支)。

这种方式只适用于生产中同时只有一个版本的产品。

团队喜欢这种方式的一个原因是,它可以确保发布分支的最新引用始终指向下一个发布候选版本,而不必翻出最近的发布分支的最新版本。然而,至少在 git 中,我们通过在分支命名中包含 “release” 来实现同样的效果,这个分支名在团队切分一个新的发布分支时,会通过强制重置而移动,并在旧的发布分支上留下一个标签。

3.3 环境分支 ✣

通过源代码提交,配置产品在新环境中运行。

软件通常需要在不同的环境中运行,例如开发人员的工作站、生产服务器,以及可能的各种测试和试运行环境。通常,在这些不同的环境中运行需要更改一些配置,例如用于访问数据库的 URL、消息系统的位置以及一些关键资源的 URL。

环境分支是包含产品配置提交的分支,这些对源代码修改的提交重新配置产品以在不同的环境中运行。环境分支是用于提交产品配置源代码的分支,这些配置代码使产品在不同的环境中运行。我们可能有一个在主线上运行的 2.4 版,现在我们希望在试运行服务器上运行。为此,我们从 2.4 版开始剪切一个新分支,适配环境参数,重建产品,然后将其部署到试运行环境。

09c20c7f8dddd38f22e1f9846a7cde9f.png

这些更改通常是手工进行,不过要是这些负责人员熟悉 git,他们也可能会从较早期的分支中拣选出这些更改。

环境分支模式通常与成熟度分支结合使用。一个长期存在的 QA 成熟度分支可能包括针对 QA 环境的配置调整。合并到该分支中就会获取这些配置变更。同样,长期存在的发布分支可能也会包含这些配置更改。

3.3.1 适用场景

环境分支是一种具有吸引力的方法。它允许我们可以按任何需要的方式调整应用程序,来为一个新环境做准备。我们可以将这些更改保留在一个差异文件(diff)中,以便于可以被拣选到产品的未来版本中。但是,这是“反模式”的典型示例——开始时看起来很吸引人,但是很快就会走向一个充斥着痛苦、暴力与瘟疫的世界。

任何环境的转移会伴有潜在的风险,那就是当我们把应用程序从一个环境转移到另一个环境时,它的行为是否发生了变化。如果我们不能把一个在生产环境中运行的版本拿到开发者的工作站上调试,就会使问题的修复变得更加困难。我们会引入仅在某些环境中出现的缺陷,风险最高的就是生产环境。由于这种风险的存在,我们希望尽可能确保在生产环境中运行的代码与其他地方相同。

环境分支的问题在于使其吸引人的极度灵活性。由于我们可以在这些差异文件中更改代码的任何方面,因此我们可以轻松地引入配置补丁,这些补丁会导致行为变化以及随之而来的缺陷。

因此,许多组织明智地坚持一个铁律,那就是一旦编译了可执行文件,在每个环境中运行的就必须是同一个可执行文件。如果需要更改配置,则必须通过显式配置文件或环境变量之类的机制将其隔离。这样,可以将它们最小化为简单的常量设置,这些常量在执行期间不会更改,从而减少了缺陷滋生的余地。

对于直接执行源代码的软件(例如JavaScript,Python,Ruby)来说,可执行文件和配置间的简单划分很容易变得非常模糊,但原理仍然适用。使任何环境变化保持最小,并且不要使用源分支来应用它们。一般的经验法则是,您应该能够签出该产品的任何版本,并可以在任何环境中运行它,因此,纯粹因不同的部署环境而发生的任何更改都不应置于源代码管理中。在源代码管理中存储默认参数的组合是有道理的,但是应用程序的每个版本都应该能够根据动态因素(例如环境变量)按需在这些不同的配置之间进行切换。

环境分支是使用源分支作为低级的模块化架构示例。如果应用程序需要在不同的环境中运行,那么在不同环境之间切换的能力就必须成为其设计的首要部分。对于缺乏这种设计的应用来说,环境分支可以作为一种偷工减料的机制,但随后应该优先考虑用可持续的替代方案来移除它。

3.4 热修复分支 ✣

用于修复紧急生产缺陷的分支。

一旦生产中出现严重缺陷,就需要尽快修复。处理此缺陷的工作将比团队正在进行的任何其他工作具有更高的优先级,其他任何影响热修复工作进度的事都不应该做。

热修复工作需要在源代码控制下完成,以便团队可以准确记录和协同工作。通过在最新发布的版本上打开一个分支,并在该分支上进行热修复的更改,可以实现这一点。

e9f04c822fbf0c5b6e1234d4c8e1143c.png

一旦修复被应用到生产中,每个人都有机会睡个好觉,然后,该修补程序就可以应用到主线,以确保已被修复的缺陷不会在下一个版本重现。如果有针对下一版本的发布分支,修补程序也需要应用到该发布分支上。如果发布版本之间的时间跨度大,那么修补程序很可能需要在已经改动过的代码上进行,合并起来就会更麻烦。在这种情况下,那些可以暴露缺陷的优质测试会很有帮助。

如果团队正在使用发布分支,那么修复缺陷的工作可以在发布分支上进行,并在修复完成后创建新的发布分支。本质上,这将旧的发布分支转变成了热修复分支。

f721b71e1f786b8ce6d5d4f4fcfa04cb.png

与发布分支一样,修复也可以在主线上进行,然后再拣选到发布分支。但这种方式比较少见,因为热修复通常是在很大的时间压力下完成的。

如果团队实行持续交付,就可以直接从主线发布修补程序。他们可能仍然使用热修复分支,但是他们会从最新的提交开始,而不是从上次发布的提交开始。

1a4702a9315dc0b9c2041ac39c35726c.png

在上图中,我为新的发布打的标签是 2.2.1,因为如果团队采用这种工作方式,M4 和 M5 很可能不会公开新功能。如果确实有新功能,那么这个修复程序很可能就会被包含在 2.3 版的发布中。当然,这说明,有了持续交付,修复缺陷不用避开正常的发布过程。如果一个团队的发布过程响应足够迅速,就可以像正常的发布一样处理修复程序——这是持续交付思维的一个重要好处。

有一个适合持续交付团队的特殊处理方式,在修复程序完成之前,禁止对主线进行任何提交。这符合“没有人有比修复主线更重要的任务”这一准则——实际上,主线上发现的任何缺陷,包括尚未交付生产的缺陷,都是如此。(所以我想这并不是真正的特殊处理方式。)

3.4.1 适用场景

修复程序通常是在压力很大的情况下完成的,当团队处于最大压力之下时,很可能会犯错误。在这种情况下,使用源代码控制,并以比常理更频繁地提交,比平常更有价值。把修复工作放在一个分支上,可以让每个人都知道为了解决问题正在做的事。唯一的例外是可以直接应用到主线的简单修复。

这里更令人感兴趣的问题是区分什么是需要紧急修复的缺陷,什么是可以留在常规开发工作流程中处理的问题。团队发布的频率越高,就越能将生产缺陷的修复留在正常的开发节奏中。在大多数情况下,如何决定将主要取决于缺陷对业务的影响,以及其与团队发布频率的匹配程度。

3.5 发布火车 ✣

以固定的时间间隔内发布,就像火车按确定的时刻表发车一样。开发人员在完成功能开发后,选择要赶哪趟火车。

使用发布火车 (release train) 的团队会设置一个定期的发布节奏,例如每两周或每六个月发布一次。就像列车时刻表一样,对于每一次发布,安排好拉出对应发布分支的时间。人们决定他们想要某一个功能赶哪列火车,然后以那列火车为工作目标,当火车装车时将他们的提交放到相应的分支上。一旦火车出发,该分支就是发布分支,将只接受修复。

一个使用月度火车的团队,会基于 2 月份的发布,开启一个 3 月份的分支。他们会随着时间的推移增加新的功能。在一个设定的日期,或许是当月的第 3 个周三,火车出发——该分支的功能被冻结(feature-freezing)。他们为 4 月的火车开设一个新的分支,并在其中添加新的功能。与此同时,一些开发人员会稳定 3 月的火车,在其就绪后投入生产。任何对 3 月火车的修复都会被拣选到 4 月火车上。

8d6229693b7e7c7f59da6dd9a1c0613a.png

发布火车通常与特性分支开发一起使用。当 Scarlett 意识到何时可以完成自己的功能时,她将决定去搭乘哪趟火车。如果她认为可以赶上 3 月的发布,她会将该功能集成到 3 月的火车,但如果不能,她就会等下一趟,并向那里集成。

一些团队在火车发车(即硬冻结)前几天使用软冻结。一旦发布火车处于软冻结状态,开发人员就不应该再向该列火车推送工作,除非他们确信他们的功能是稳定的并且是发布就绪的。任何在软冻结后被发现有缺陷的功能将被回退(推下火车),而不是在火车上修复缺陷。

如今,当人们听到 “发布火车” 时,往往听到的是来自 SAFe 的敏捷发布火车(Agile Release Train)。SAFe 的敏捷发布火车是一种团队组织结构,是指一个大规模团队,这个大规模团队中的小团队共享一个通用发布火车时刻表。虽然 SAFe 的敏捷发布火车也使用发布火车的模式,但和我在这里描述的不是一回事。

3.5.1 适用场景

发布火车模式的一个核心概念是发布过程的规律性。如果你事先知道发布火车何时出发,你就可以计划在这趟火车上要完成的功能。如果你认为不能在 3 月的列车中完成你的功能,你就会知道要赶下一趟。

当发布过程存在明显阻力时,发布火车特别有用。比如有一个外部测试组,需要几周的时间来验证一个发布;或者有一个发布委员会,需要在产品有新版之前达成一致。如果是这种情况,通常更为明智的做法是尝试消除发布阻力,并允许更频繁的发布。当然,或许在某些情况下,这几乎是不可能的,例如移动设备的应用商店验证过程。配合这种发布阻力对发布火车进行调整,可能就可改善这种状况。

发布火车的机制有助于将所有人的注意力集中在什么功能应该何时出现上,从而有助于预测功能何时完成。

这种方法的一个明显缺点是,在火车早期完成的功能将坐在火车上一边看书一边等待发车(译者注:这是个有趣的比喻,已经完成的功能被搁置了)。如果这些功能很重要,那就意味着该产品会有几周或几月的时间错失一项重要功能。

发布火车可以成为团队改进发布过程难能可贵的阶段。如果团队难以进行稳定的发布,那么直接跳到持续交付就可能是不切实际的。选择一个恰当的发布火车期限,一个困难但又合理的期限,可以是一个好的开始。随着团队技能的提高,他们可以增加发车频率,最终随着能力的增长,放弃发布火车,转而进行持续交付。

3.5.2 变体:承载未来的火车

功能火车的基本例子是在前一列火车出发的同时,有一列新的列车到达站台接收功能。但还有另一种方法是让多个列车同时接收功能。如果 Scarlett 认为她的功能无法在 3 月的火车出发前完成,她还可以将她基本完成的功能推送到 4 月的火车上,并在 4 月的火车出发前推送更多的提交直到功能完成。

outside_default.png

我们定期从 3 月的火车拉取改动到 4 月的火车。有些团队更喜欢只在 3 月的火车出发时做这件事,这样他们就只需要一次合并。但是我们当中那些知道小规模合并会倍加容易的人,更愿意尽快把 3 月的提交拉到 4 月的火车上。

承载未来的火车可以让那些正在开发 4 月功能的开发人员在不会干扰 3 月火车工作的情况下进行协作。它的缺点是,如果开发 4 月功能的人员做出了与 3 月工作相冲突的更改,3 月的人员得不到任何反馈,从而会使未来的合并更加复杂。

3.5.3 与从主线定期发布的比较

发布列车的主要好处之一是有规律的生产发布节奏。但为新开发设置多个分支会增加复杂性。如果我们的目标是定期发布,我们同样可以使用主线来实现。确定发布计划是什么,然后按预定时间从主线顶端分一个发布分支。

938b4ffa172b7de404b960dd0031c811.png

如果有一个发布就绪的主线(Release-Ready Mainline),就不再需要发布分支。使用像这样有规律的发布,如果刚好在定期发布日期之前,开发人员仍然可以选择不向主线推送接近完成的功能从而将其延期到下一次发布。在持续集成时,如果人们希望将某个功能推迟到下一期,他们总是可以延迟放置服务映射或保持功能特性开关处于关闭状态。

3.6 发布就绪主线(主线发布) ✣

保持主线足够的健康,以便于主线的最新引用总是可以直接投入生产

我在“从主线到生产发布的路径”这一章内容开始的时候曾说过,如果你使主线成为一个健康分支,并且让健康检查足够先进,那么你就可以如愿地直接从主线发布,并使用标签记录发布版本。

1ffa75eaa947facf16c35aeac26a8cc2.png

我已经花了很多时间来描述可替代这种简单机制的模式,所以我认为是时候强调这一点了,因为,如果一个团队能够做到的话,它会是一个绝佳的选择。

仅仅因为主线的每一次提交都是可发布的,并不意味着它就应该被发布。这就是持续交付和持续部署之间的微妙区别。使用持续部署的团队确实会发布每一个被接受到主线的变更,但在用持续交付的情况下,虽然每一个变更都是可以发布的,但是否发布是一个业务决策。(因此,持续部署是持续交付的一个子集。) 我们可以认为持续交付给我们提供了随时发布的选择权,我们是否行使这一选择权则取决于更广泛的议题。

3.6.1 适用场景

连同作为持续交付一部分的持续集成,主线发布是高绩效团队的一个共同特征。考虑到这一点,以及我对持续交付众所周知的热忱,你可能会期望我说,与我在本章中所描述的替代方案相比,主线发布总是更优的选择。

然而,模式都是和环境相关的。在一种环境下出彩的模式,可能在另一个环境中却是陷阱。主线发布的有效性受团队的集成频率制约。如果团队使用特性分支开发,通常每个月只集成一次新功能,那么,团队很可能处于一个糟糕的境地,并且对主线发布的坚持可能是他们改进的障碍。

这种糟糕处境就是他们无法响应不断变化的产品需求,因为从想法到生产的周期太长。因为每个功能都很大,他们还可能会有复杂的合并和验证,从而导致了很多冲突。这些可能会在集成时表现出来,或者在开发人员从主线拉到他们的特性分支时,对他们造成持续的消耗。这些累赘阻碍了重构,从而降低了模块化,使问题更加严重。

摆脱这个陷阱的关键是增加集成频率,但在许多情况下,可能很难在维持主线发布的同时实现这点。在这种情况下,最好放弃主线发布,鼓励更频繁的集成,并使用发布分支来稳定生产的主线。当然,随着时间的推移,我们希望通过改进部署流水线来消除对发布分支的需求。

在高频集成环境中,主线发布具有明显的简单性优势。无需烦恼于我所描述的各种分支的复杂性。即使是热修复也可以应用于主线,然后发布到生产中,使它们不再特殊到值得关注。

此外,保持主线发布就绪可以推行一种宝贵的纪律。它使得生产准备工作在开发人员的意识中保持最高优先,确保问题不会逐渐蔓延到系统中,无论是以缺陷还是流程问题的形式,从而降低产品的周期时间。对于许多人来说,完整的持续交付原则“开发人员每日多次集成到主线而不破坏它”似乎非常困难。然而,一旦实现并成为一种习惯,团队会发现它能显著减轻压力,并且相对容易跟上。这就是为什么它是敏捷流畅度®模型(Agile Fluency® Model)中交付区域的关键元素。

四、其他分支模式

96fe7565eb38b78e1e5f91f91b60695e.png

本文的主旨是围绕团队集成的模式和生产的路径进行讨论。但还有一些其他的模式我也想提一下。

4.1 试验分支 ✣

把一个代码库上试验性的工作收集在一起,这些工作不希望直接合并到产品中。

试验分支是开发人员希望尝试一些想法的地方,但并不希望他们的变化会被简单地集成回主线。我要是发现了一个新的库,我认为它可以很好地替代我们已经在使用的库。为了有助于决定是否要切换,我开启一个分支,然后就尝试用这个库来编写,或者重写系统的相关部分。练习的目的不是为了向代码库贡献代码,而是为了了解一个新工具在我的特定环境中的适用性。我可以自己做这项工作,也可以和一些同事一起做。

类似的,我有一个新的功能要实现,可以看到有几种方法来处理它。我花了几天的时间来研究每一种选择,以帮助我决定选择哪一种。

这里的关键点是期望试验分支上的代码会被放弃,而不是被合并到主线。这并非绝对,“如果刚好我喜欢试验的结果,而且代码可以很容易地集成,那么我不会对这样的机会视而不见”,可我不指望会是这样的情况。我可能会放松一些惯常的习惯,比如减少测试、写的一些乱糟糟重复的代码,而不是试图整洁地重构进去。如果我喜欢这个试验,我会从头开始,把这个想法应用到生产代码中,用试验分支作为提醒和指导,但不使用任何试验分支上的提交。

一旦我完成了一个试验分支的工作,在git 中,我通常会添加一个标签,然后删除该分支。这个标签可以保留代码基线,以备我以后重新检视——我使用一个惯例,比如以 "exp "开头的标签名,以明确其性质。

4.1.1 适用场景

每当我想尝试某件事情而又不确定最终是否会用到它时,试验分支就很有用。这样我就可以做任何我喜欢的事情,不管多么癫狂,但我有信心可以轻松地把它放到一边。

有时我会以为自己在做常规的工作,但意识到我在做的其实是一个试验。如果发生这种情况,我可以打开一个新的试验分支,并将我的主工作分支重置为最后的稳定提交。

4.2 未来分支 ✣

一个单独的分支,用于处理因侵入性过强而难以用其它方法处理的变更。

这是一种罕见的分支模式,当人们使用持续集成时,才会偶尔会出现这种情况。有时,团队需要对代码库进行非常具有侵入性的修改,而集成在制品的通用技术并不适用此修改。在这种情况下,团队所做的事情看起来很像特性分支:他们建了一个只从主线中提取的未来分支,直到最后才进行主线合并。

未来分支与特性分支的最大区别在于,未来分支只有一个。因此,从事未来分支工作的人从不会偏离主线太远,而且他们也没有其他不同的分支需要处理。

可能有几个开发人员在未来分支工作,在这种情况下,他们会与未来分支做持续集成。在进行集成时,他们首先会从主线拉取代码到未来分支,然后再集成他们的更改。这将减缓集成进程,但这也正是使用未来分支的成本。

4.2.1 适用场景

我要强调,这是一种罕见的模式。我怀疑大多数进行持续集成的团队永远都不需要使用它。我曾见过它用于对系统中的架构进行特别有侵入性的变更。一般来说,这是最后的手段,只有当我们无法想出如何使用类似于抽象分支的方法时才使用。

未来的分支仍然应该尽可能小,因为它们在团队中创建了一个分区,并且就像在任何分布式系统中的分区一样,我们需要将它们保持在一个绝对最小值的水平。

4.3 协作分支 ✣

一个为开发人员在没有正式集成时和团队其他成员共享工作而创建的分支。

当一个团队使用主线时,那么大多数协作都是通过主线进行的。只有在发生主线集成时,团队的其他成员才会看到开发人员正在做什么。

有时,开发人员想在集成之前共享他们的工作。开启一个分支供协作者使用,可以让他们在临时性的基础上做到这一点。分支可以被推送到团队的中心仓库,协作者可以直接从他们的个人仓库中拉取和推送,或者建立一个短期存储的仓库来处理协作工作。

一个协作分支通常是临时的,一旦工作集成到主线中就会被关闭。

4.3.1 适用场景

随着集成频率的降低,协作分支变得越来越有用。如果团队成员需要配合修改对几个人都很重要的一块代码,长期存在的特性分支经常需要非正式的协作。然而,使用持续集成的团队可能永远不需要开启协作分支,因为他们只有很短的时间彼此看不到他们的工作。主要的例外是一个试验分支,从定义上讲,它永远不会被集成。如果几个人一起做一个试验,他们需要使试验分支也是一个协作分支。

4.4 团队集成分支 ✣

在向主线集成前,允许某个子团队互相集成。

大型项目可能有几个团队在一个逻辑上独立的代码库上运行。团队集成分支允许团队成员彼此集成,而不必使用主线与项目的所有成员集成。

实际上,团队将团队集成分支视为团队内部的主线,像和整个项目的主线集成一样与之集成。除了这些集成之外,团队还要执行一项单独的工作去和项目主线集成。

4.4.1 适用场景

使用团队集成分支显而易见的驱动力是由于有太多开发人员活跃在代码库上进行开发,这些人员多到有必要拆分成单独的团队。但我们也要对那种假设持谨慎态度,因为我遇到过许多团队,他们看起来规模太大了,以至于无法全部在一条单独的主线上工作,然而还是设法做到了(我曾收到过上百位开发人员这样工作的报告。)

团队集成分支的一个更重要的驱动因素是在集成频率期望上的差异。如果项目总体上期望团队做长达几周的特性分支,但是子团队更喜欢持续集成,那么团队可以建立一个团队集成分支,使用它进行持续集成,一旦他们正在做的特性完成后就把它集成到主线。

如果整个项目对健康分支采用的标准与子团队的健康标准之间存在差异,也会产生类似的效果。如果更大范围的项目不能保持主线处于足够高的稳定程度,子团队可能会选择在更严格的健康水平下运行。同理,如果子团队很难让自己的提交对于控制良好的主线来说足够健康,他们可能会选择使用团队集成分支,在进入主线之前使用自己的发布分支来稳定代码。这不是我通常赞成的情况,但在特别焦虑的情况下可能是必要的。

我们还可以将团队集成分支看作是一种更结构化的协作分支形式,它基于正式的项目组织而不是临时协作。

五、考虑一些分支策略

7aade23597939a73fb96af14580b80c5.png

在这篇文章中,我从模式的角度来谈论分支。之所以这样做,是因为我并不愿鼓吹某种最佳分支方法,而是想阐述人们常用的方法,并在各种不同环境下的软件开发中,反思这些方法的取舍。

多年来,我们已经描述了很多分支方法。当我试图了解它们是如何工作的,以及什么时候使用它们最好时,我通过我脑海中半成形的模式来评估它们。现在我终于思考成熟并写下了这些模式,我认为考虑这些策略中的一部分是有价值的,看看我是如何从模式角度来思考它们的。

5.1 Git-flow

Git-flow 已成为我遇到的最常见的分支策略之一。Git-flow 是在 2010 年由 Vincent Driessen 写的,当时 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。但经常会发现那些声称自己在使用 git-flow 的人实际上在做一些完全不同的事情。通常,他们的实际做法更接近于 GitHub Flow。

5.2 GitHub Flow

虽然 Git-flow 确实很流行,但对 web 应用来说,其分支结构过于复杂,这催生了大量竞品。随着 GitHub 的流行,其开发人员使用的分支策略 GitHub Flow 成为众所周知的策略,也就不足为奇。对 GitHub Flow 的最佳描述来自 Scott Chacon。

有 GitHub Flow 这样的名称,毫无疑问,它是基于 git-flow,并有所变化的。两者之间的本质区别在于产品的种类不同,这意味着不同的环境以及不同的模式。Git-Flow 假定一个产品有多个生产版本。GitHub Flow 假设一个产品只有一个生产版本,并以高频次集成到发布就绪主线上。在这种情况下,不需要发布分支。生产问题的修复方式也与常规功能的开发方式相同,也就不需要热修复分支,从某种意义上讲,热修复分支通常意味着与正常流程的偏差。去除这些分支带来极大的简化,变成了一个主线和多个特性分支的分支结构。

GitHub Flow 将其主线称为“master”。开发人员使用特性分支模式工作,他们定期将自己的特性分支推送到中心仓库,以支持可见性,但直到功能特性完成,才会与主线集成。Chacon 表示特性分支可以是一行代码,也可以是需要持续数周的工作。无论哪种情况下,该过程均以相同的方式进行。使用 GitHub,拉取请求机制是主线集成的一部分,并应用对提交评审(Reviewed Commits)。

Git-flow 和 GitHub Flow 经常会被混淆,因此像往常一样,对这些东西的研究要比名称更深入,才能真正了解是怎么回事。两者共同的主题思想是使用主线和特性分支。

5.3 基于主干的开发

正如我在前面所写的,我大多数时候听到的“主干驱动开发”是持续集成的代名词。但是将主干驱动开发视为 git-flow 和 GitHub Flow 的分支策略替代方案也是合理的。Paul Hammant 写了一个深入详尽的网站来解释这种方法。Paul 是我在 ThoughtWorks 的一位老同事,他有一个可信赖的记录,佩戴着他+4级可靠的砍刀跋涉过一个客户僵化的分支结构。

基于主干的开发专注于在主线(也称为“主干”,这是“主线”的常见同义词)上进行所有工作,从而避免任何形式的长期分支。较小的团队使用主线集成模式直接向主线提交,较大的团队可能会使用短期的特性分支,其中“短”意味着不超过几天——这大概相当于实践中的持续集成。团队可能使用发布分支(用于发布的分支)或发布就绪主线(从主干发布)。

六、最终想法和建议

2dd1b8357069c2e8f32935f527be46e5.png

从最早的编程开始,人们就发现,如果他们想要一个与现有程序有些不同的程序,很容易拿一份源码,复制后根据需要进行调整。有了所有的源码,我就可做出我想要的任何改变。但是这样做,让我的副本很难接受原始来源中的新功能和错误修复。随着时间的流逝,这或许会变为不可能,就如同许多企业在其早期的 COBOL 程序中发现了这些问题,并在如今广泛定制的 ERP 软件包中遭受影响。无论是否使用源分支这个名字,只要我们复制代码并对其进行修改,我们就在应用这种模式,即使没有用到任何版本控制系统。

正如我在长篇文章开始时所说的那样:分支很容易,合并却比较困难。分支是一项强大的技术,但它使我想到了 goto 语句、全局变量和并发锁。功能强大,易于使用,但更容易过度使用,它们常常成为那些粗心和缺乏经验者的陷阱。源代码控制系统可以通过仔细跟踪变更来帮助控制分支,但最终它们只能充当问题的见证者。

我不是说分支是有害的。有些日常的问题,例如多个开发人员对单个代码库做出贡献,在这种情况下,明智地使用分支至关重要。但是我们应该始终保持警惕,并记住 Paracelsus(译者注:瑞典科学家 Paracelsus 被认为是16世纪“毒理学之父”)所指出的,“剂量的不同区分药物和毒物”。

因此,我进行分支的第一个提示是:每当你考虑使用分支时,都要弄清楚你要如何合并。无论何时,你使用任何技术,都是在权衡其他选择。在不了解某项技术所有成本的情况下,你无法做出明智的决策,对于分支,在你合并时,吹笛者会收取费用(译者注:吹笛者收取费用隐喻付出成本)。

因此,下一个准则是:确保你了解分支的替代方案,它们通常是更好的选择。记住 Bodart 的法则(译者注:参见本文集成模式篇“模块化的重要性”),有没有办法通过提高模块化来解决您的问题?你可以改善部署流水线吗?是否一个标签就足够了?你对流程进行哪些更改会使该分支变得不必要?实际上,这个分支就目前来说很可能是最好的解决方案——但却是一种坏味道,警醒着你在未来几个月内,还有一个更深层的问题应当解决。摆脱对分支的需求通常是一件好事。

请记住LeRoy的插图:分支在没有集成的情况下运行时会以指数方式偏离。因此,请考虑你集成分支的频率。目标旨在使您的集成频率提高一倍。(这里显然有一个限制,但是除非你处于持续集成的地带,否则你不会接近它。)更频繁的集成会有障碍,但是这些障碍往往正是需要给予过量炸药才能改善你的开发过程的。

由于合并是分支的难点,因此要注意是什么导致了合并困难。有时是一个流程问题,有时是架构的缺点。无论是什么,都不要屈服于斯德哥尔摩综合症(Stockholm Syndrome)。任何合并问题,尤其是引起危机的问题,都是提高团队效率的标志。请记住,只有从错误中学习,错误才有价值。

我在这里描述的模式概述了我和同事们旅行中遇到的常见分支配置。通过命名它们,进行解释,最重要的是,解释它们何时有用,我希望可以帮助你评估何时使用它们。请记住,与任何模式一样,它们很少有普遍的好坏之分——它们对你的价值取决于你所处的环境。

当你遇到分支策略时(无论是像git-flow还是基于主干开发这样众所周知的策略,还是在开发组织中自行开发的东西),我希望了解其中的模式能帮助你确定它们是否适合您的情况,以及加入哪些其它模式会有帮助。

致谢  

感谢巴德里·贾纳基拉曼(Badri Janakiraman)、布拉德·阿普尔顿(Brad Appleton)、戴夫·法利(Dave Farley)、詹姆斯·肖尔(James Shore)、肯特·贝克(Kent Beck)、凯文·杨(Kevin Yeung)、马科斯·布里泽诺(Marcos Brizeno)、保罗·哈曼特(Paul Hammant)、皮特·霍奇森(Pete Hodgson)和蒂姆·科克伦(Tim Cochran)阅读了本文的草稿,并给了我有关如何改进它的反馈。

彼得·贝克尔(Peter Becker)提醒我并指出,派生(forks)也是一种分支形式。我使用了 Steve Berczuk 的《软件配置管理模式》中“主线”这个名字。

延伸阅读  

关于分支有很多资料,我无法详尽调查所有资料。但是,我确实想强调一下 Steve Berczuk 的书:《软件配置管理模式》。Steve 的著作以及他的贡献者布拉德·阿普尔顿(Brad Appleton)的著作,对我就源代码管理的看法产生了深远的影响。

重大修订

  • 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日:开始撰4a0f1bda123895d4f0a102edeeffdee7.jpeg文章来源:MiniStarClub北京,致力于提供最具价值的测试及测试管理领域原创文章。包括测试技术、测试方法、测试思想、测试管理等。

· 推 荐 阅 读 ·

RECOMMENDATION

京东金融云测平台方案

基于风险的测试策略

基于业务分层的测试策略

UI自动化应该实施哪些内容?

全面的质量保障体系之发现缺陷

全面的质量保障体系之回归测试策略

全面的质量保障体系之测试用例分级

每个模型都有组合方式|测试金字塔的思考与总结

服务端性能问题排查及优化---CPU高问题分析

服务端性能问题排查及优化---高延迟问题分析

京东金融App端链路服务端全链路压测策略

一次服务端性能问题排查过程

京东金融移动端测试实践

接口测试经验与实践

你点的每个“在看”,我都认真当成

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐