代码评审记录_技术 | 源代码分支管理模式

原作者:Martin Fowler 来源(原文网址): https://martinfowler.com/articles/branching-patterns.html 译者:崔龙波 郭颖 朱婷 余晓蒨 审校:王立杰 王英伟 高俊宁 陈文峰

6a35e06198c244e1ec653b6335c5a9d6.png

Martin Fowler

cfac7c5c1b696025c46db9b16284ac0f.png

现代的源代码控制系统提供了强大的工具,可以非常轻松的在源代码上创建分支。但最终分支还是要合并在一起,许多团队不得不花相当多的时间去处理相互纠缠的分支。这里有几种模式让团队可以有效地使用分支,专注于集成多个开发人员的工作并组织产品发布的路线。最重要的一点,分支应该频繁集成,尽力保持一个无需过多干预就可部署生产的健康主线。

2020年5月28日

目 录

1. 基本模式 

1.1 源分支 ✣ 

1.2 主线 ✣ 

1.3 健康的分支 ✣ 

2. 集成模式 

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. 从主线到生产发布的路径

3.1 发布分支 ✣

3.2 成熟度分支 ✣

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

3.3 环境分支 ✣

3.4 补丁分支 ✣

3.5 发布火车 ✣

3.5.1 变体:载入未来的火车

3.5.2 与主线外的常规发布比较

3.6 准备发布的主线 ✣

4. 其它分支模式

4.1 实验分支 ✣

4.2 未来分支 ✣

4.3 协作分支 ✣

4.4 团队集成分支 ✣

5. 考虑一些分支策略

5.1 Git-flow

5.2 GitHub Flow

5.3 基于主干开发

6. 最后的想法和建议

7. 边栏

7.1 集成恐惧

7.2 持续集成和基于主干开发

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

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

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

基本模式

在思考这些代码分支模式时,我发现它们可以分为两大类。一类模式着眼于集成,即多个开发人员如何将他们的工作成果组合成一个连贯的整体。另一类则着眼于生产路径,即使用分支帮助管理从集成代码库到生产环境运行产品的路径。一些模式为这两大类模式提供支撑,我将它们归类为基本模式,在本节中讲述。还有一些模式既不基本也不适合于归类到集成和生产路径这两大类模式,我把它们留到最后来讲。

✣ 源分支 ✣

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

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

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

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

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

2c6b05905613f1e587e307c07575c3ed.png

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

a06009a8c996d232f7f912fefcca3885.png

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

以一种常见情况来说明这一点,一个现代开发团队,该团队将其源代码保存在共享的 git 仓库中。一名开发人员 Scarlett (以猩红色表示) 需要进行一些更改,因此她克隆了 git 仓库并检出了 master 分支。她做了几处更改,然后重新提交给她的 master 分支。同时,另一个开发人员,Violet (以紫色表示) 将仓库克隆到自己桌面上,并签出 master 分支。那么 Scarlett 和 Violet 是在同一个分支上工作还是分别在另一个分支上工作?答案是:他们都在 “master” 上工作。但是他们的提交彼此独立,并且当他们将更改推回到共享仓库时都需要合并。如果 Scarlett 不确定自己所做的更改,会发生什么情况,因此她标记了最后的提交,并将她的 master 分支重置回 origin/master(她克隆共享仓库时的最后一次提交)。

45c1edd2dab997ce73d1f45ee5e11d62.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 分支,该工作副本也是独立的代码线。

适用场景

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

记录提交中所有更改的源代码控制系统确实让合并过程更加容易,但并没有使合并过程不再重要。如果 Scarlett 和 Violet 都将变量的名称更改为不同的名称,则存在冲突,如果没有人工干预,源管理系统将无法自行处理。为了凸显这种文本冲突的尴尬,源代码控制系统至少还可以发现并提醒人们看一下。但是在文本合并没有问题的地方也经常会出现冲突,系统仍然无法正常工作。想象一下,Scarlett 更改了函数的名称,而 Violet 向其分支添加了一些代码,以其旧名称调用该函数。这就是我所说的语义冲突。当发生此类冲突时,系统可能无法构建,也可能会构建成功但在运行时失败。

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

7cbda417326b863660b74c8be81f0a85.png

任何有并行计算或分布式计算工作经验的人都熟悉的问题是:当多个开发人员同时更新时,代码仓会处于某个共享状态。我们需要通过将这些更新序列化为某个共识更新的方式,把这些开发人员的更新结合起来 。事实上,使系统正确执行和运行意味着该共识状态的有效性标准非常复杂,这使我们的任务也变得更加复杂。无法创建确定性算法来找到共识。人们需要寻求共识,并且共识可能涉及混合不同更新的选择部分。通常,只有通过原始更新解决冲突才能达成共识。

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

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

✣ 主线 ✣

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

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

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

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

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

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

适用场景

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

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

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

✣ 健康的分支 ✣

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

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

为了解决这个问题,我们可以努力去保持一个分支是健康的——也就是这个分支是可以成功构建并且运行时几乎没有 bug 的。为了确保这一点,我发现编写自测代码是至关重要的。这种开发实践是指我们在编写生产代码时,还要编写一套全面的自动化测试,让我们可以确信,如果这些测试通过,那么这些代码就不会有 bug。如果我们这样做,就可以通过每次提交运行一个构建来保持分支健康,这个构建过程也包括运行这套测试。如果系统无法编译,或者测试失败,那么我们的第一要务就是在我们对该分支进行任何其他操作之前就先对其进行修复。通常这意味着我们“冻结”了这个分支——除为了修复以使其恢复正常的提交之外,不会允许在这个分支进行任何提交。

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

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

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

适用场景

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

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

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

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

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

未完待续……

(下一篇将介绍集成模式)

3516e7a2c3d2209841fd71b53074c65d.gif

- End -

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值