一、基本模式
1.1 Source Branching
Create a copy and record all changes to that copy.
源代码管理系统记录每一次提交的每个改变,让代码合并变得简单,但不能让代码冲突消失。如果两个人同时修改同一个文件中的同一个变量定义,并且修改的值不一样,那么源代码管理系统在没有人为干预的情况下就无法解决这个冲突。让它更尴尬的是,这种文本冲突至少是源代码控制系统可以发现的,并提醒人们去查看,还有一些冲突是合并时发现不了,但是系统就是不工作。比如,一个人改变了函数名称,但另一个人还是引用的原来的函数 名称,这就会导致语义冲突。当这种情况出现时,系统可能会编译失败,也可能编译成功但在运行时失败。
大部分人会像上面那张一样画分支图,实际应该时下面这张图,随着时间的推移变化会越来越大。
任何从事并发和分布式计算工作的人都对这种问题很熟悉。开发者并行更新同一个状态。我们需要组合这些更改并序列化成一致的状态。事实上,让系统执行并运行正确已经越来越复杂了。这意味着对于共享的状态有复杂的验证标准。不可能创造一个确定的算法来达到一致。需要人为的达到一致,这个一致也许需要混合不同的更新。
1.2 Mainline
A single, shared, branch that acts as the current state of the product
mainline是一个特殊的分支,代表的是团队代码的当前状态。无论什么时候我想开始新的任务,我就会将mainline克隆到本地开始工作。无论何时我想和其他团队分享我的工作,我就会更新mainline*,理想状态是使用Mainline Integration 模式,稍后会讲到。
不同的团队对这个特别的分支取不同的名字。常常会鼓励使用这个源代码系统默认的。比如在git通常叫“master”,svn通常叫“trunk”。
必须强调的是mainline是一个单独的、共享的分支。当人们在git中谈论“master”时,他们可能表示的不同的东西,这是由于每个仓库克隆有自动本地master。通常团队有一个中心仓库---一个共享的仓库作为项目的单点记录,也是所有克隆的原始副本。开始一项新的工作通常就是克隆中心仓库。如果已经克隆过,当我开始工作时,我会先从中心仓库拉取一份最新的。这种情况下,在中心仓库中mainline就是master分支。
当我开发一个新功能时,我会有一个自己的开发分支,这个分支就是我的本地master,或者我会创建一个单独的本地分支。如果我在这个分支上开发了一段时间,我会不停的从mainline上拉取新的变更,然后合并到我的单独的分支上。
相同的,如果我想为产品创建一个新的发布版本,我可以从当前的mainline开始。如果我想修复bug,可以使用Release Branch。
When to use it
还记得在2000年的时候,和一个工程师聊天,他的工作是合并团队开发的代码。每次他会给团队的每个成员发送邮件,然后成员会发送自己的代码文件给他。他然后拷贝这些文件和自己的代码集成,然后编译代码库。这通常会花费他几周的时间。
相反的,通过mainline,任何人可以很快的使用最新的代码开始工作。更进一步,mainline不仅让当前代码库的状态更易看见,也是我要讲的其他模式的基础。
1.3 Healthy Branch
On each commit, perform automated checks, usually building and running tests, to ensure there are no defects on the branch
由于Mainline是共享的、经过验证的状态,所以保持它在一个稳定的状态很重要。
为了做到这一点,我们需要保证编译都成功并且代码没有或者很少有bug出现。有以下经验可以借鉴:
- 写Self Testing Code
- 单元测试失败立即修复
通常测试要花费大量的时间,所以可以使用 Deployment Pipeline将测试分成几个阶段。第一个阶段需要尽可能地快,通常不长于10分钟,但仍要有测试力度。这一阶段被称为“commit suite”,也叫做“单元测试(the unit test)”。
理想状态下每一个提交都应该触发一次编译。然后如果测试很慢,比如是性能测试,那么这样就不实用。
代码运行没有bug并不意味着就是好代码。为了保证交付地稳定性,我们需要保证代码地内部质量。一个常用地方法是 Reviewed Commits。
When to use it
每个团队应该在他们的开发流程中具有清晰的标准来保证分支健康,这具有巨大的价值。如果mainline健康,一个开发者可以随时开始工作只要拉取最新的分支即可。如果mainline不稳定,需要花费大量的时间来修复分支问题。
也要保证本地分支代码健康,这样就可以很容易合并到主干分支。
二、Integration Patterns
2.1 Mainline Integration
Developers integrate their work by pulling from mainline, merging, and - if healthy - pushing back into mainline
mainline表示了团队软件的当前状态。有mainline的一个好处就是简化了集成。每个开发可以在自己本地分支上做集成。
下面举一个例子来说明,比如有个开发者叫小s,她要开发一个新的功能,用git将主干分支克隆到自己的本地仓库:
当她在工作的时候,她的同事小V推送了一些变更到mainline。由于小V在自己的分支上开发,所以她并不知道mainline上的变动:
不久,她想开始集成了。首先她要拉取mainline当前的代码到本地,这会拉取到小V的变更。由于她在本地仓库上工作,提交将在origin/master上显示为一条单独的代码线。
如果小S幸运,合并小V的代码没有冲突,否则就会有冲突需要解决。如果是文本上的冲突,源码控制系统可以解决,但如果是语义上的冲突就很难解决。这时, Self Testing Code就非常有帮助。由于解决冲突会花费大量的时间,并且对代码质量也会产生风险,所以我将他们标记成黄色
这时,小S需要验证她合并的代码满足mainline的健康标准。这通常意味着编译代码并运行所需要的测试。即使是没有冲突的合并,也要进行编译测试。任何提交中的错误都可能是由于合并导致的。知道这个可以帮忙她定位问题,至少首先应该从合并代码中找到线索。
通过这种编译和测试,她成功地将mainline的代码合并到自己的代码库,但是她还没完成和mainline的集成。为了完成集成,她必须推送她的变更到mainline。集成包括拉取和推送。只有她完成推送后,她的工作才能开始和其他项目集成。
2.2 Feature Branching
Put all work for a feature on its own branch, integrate into mainline when the feature is complete.
使用功能分支,开发者在开发一个功能特性时打开分支,然后持续在这个分支上工作直到完成,最后集成到mainline。
例如,还是以小S为例,她开发一个新功能:添加本地的销售费率到他们的网站。她以当前产品的稳定版本开始,拉取mainline到自己的本地,然后以当前mainline为基础创建一个新的分支。她以后一直在这个分支上开发,提交了很多代码。
She might push that branch to the project repo so that others may look at her changes.
她也可以推送这个分支到远程仓库,这样别的开发者也可以看到她的变更。
当她工作的时候,其他提交会在mainline。所以时不时的,她会从mainline拉取新的代码,这样就可以知道有哪些改变会影响她的功能。
需要注意到这并不是我上面描述的集成,是因为她并没有推送回mainline。这时她只能看到她自己的工作,其他人的看不见。
一些团队喜欢让所有的代码都保存在远程仓库。这时候小S就必须推送她的分支到远程仓库。这也允许团队中的其他成员可以看到她当前的工作状态,即使还没和其他人的代码集成。
当她完成自己的功能开发时,她会执行 Mainline Integration 来合并这个功能到产品中。
如果小S在开发多个功能特性,那么她可以为每个功能创建独立的分支。
When to use it
Feature Branching在当今的工业生产中是一种很受欢迎的模式。为了谈论何时用它,我需要介绍它的主要的替代方法- Continuous Integration。但是首先需要谈论频繁集成的作用。
2.2.1 Integration Frequency
集成的频繁程度对一个团队运作有很大的影响。来自State Of Dev Ops Report 的调查研究表明精英开发团队比低绩效开发团队更频繁的集成。这个调查符合我和大多数同行的经验预期。
Low-Frequency Integration
以low-frequency 案例为例,开始讲述。还是以小S和小V为例,她们各自开始自己的工作,拉取mainline到自己的分支,然后做了一些变更但还未提交
他们工作时,其他人提交了一个变更到mainline:
这个团队通过保证分支健康来工作,每次提交都会拉取mainline。小S前两次提交没有拉取mainline是由于mainline未发生变更,但是现在她需要拉取M1:
我将这次merge用黄色方框表示。这次merge提交S1..3到M1。不久小V也需要做同样的事情:
这时两位开发者都和mainline保持同步,但是她们之间还未集成,因为代码都是隔离的。小S无法察觉到小V的V1..3做的变更。
小S又做了两次提交,然后准备向mainline集成,这次合并很容易,因为她已经拉取了M1的变更。
然后,小V就会有一个更复杂的体验。当她集成到mainline时,她不得不集成S1..5到V1..6。
High-Frequency Integration
在前面的例子中,两位开发者做了很多次提交后才开始集成。让我偶们看看如果每次提交就和mainline集成会发生什么。
小V第一次提交后就和mainline集成,此时集成很容易。
小S第一次提交后也和mainline集成,但因为小V已经提交了,所以她需要做一次merge,但由于只需要merge V1和S1,这次合并的代码量很少。
小S的下一次集成只需要推送代码就行了,这意味着小V的下次提交要合并小S的最近两次提交。然后,这仍然是一个相对较小的merge。
当有其他人推送代码到mainline时,小S和小V只需要按照以往的节奏合并代码即可。
和之前一样,这次小S只需要集成S3和M1,因为S1和S2已经集成过。这意味着小G在推送M1的时候不得不集成S1..2,V1..2。
开发者继续剩下的工作,每次提交都集成:
Comparing integration frequencies
让我们来看看上面两种方式的区别
这里有两个明显的不同。第一就如high-frequency integration名字所表示的那样,它比 low-frequency integration多很多次,而且更重要的是每次合并的代码量很少。越小的集成意味着越少的工作量,因为冲突越少。比工作量少还要重要的是,风险也少了。大合并的问题不在于工作本身,而在于工作本身的不确定性。
大多数时候,即使是大型合并也会进展顺利,但偶尔也会进展得非常非常糟糕。偶尔的疼痛会比一般的疼痛更严重。如果我比较花费额外的10分钟每个集成与1 / 50的机会花费6小时修复一个集成-我更喜欢哪一个。如果我只看努力程度,那么1 / 50更好,因为是6小时而不是8小时20分钟。但这种不确定性让50%的人感觉更糟,这种不确定性导致了对融合的恐惧。
我们从另一个角度来看一看两者之间的差别。如果小S和小V在第一次提交时就出现冲突会发生什么。在low-frequency场景下,他们直到小V的最后一次merge才会发现这个冲突。而在high-frequency 场景下,在小S第一次提交时就会发现。
频繁的集成增加了合并的频率但也减少了复杂性和风险。频繁的集成还能更快地提醒团队发生冲突。这两者是有联系的。令人讨厌的合并通常是团队工作中潜在的冲突的结果,只有在集成发生时才会浮出水面。
很多人没有意识到的是源码控制系统是一个交流工具。它可以让一个开发者知道其他人正在做的事情。通过频繁的集成,不仅可以立即知道代码有冲突,也可以知道每一个人的最新开发进展,以及代码库是如何演进的。我们不是孤身一人而是作为一个团队在一起工作。