前言
软件代码是任何一个软件研发团队的核心资产。
多人合作,如何协调处理不同开发人员的代码合成、冲突
基础模式
源分支
所有修改都记录到同一分支
分支的定义:
创建分支:
主干:
(master、trunk)
反映产品当前实际状态的唯一公用分支
可以随时获得当前产品版本状态,并基于此进行开发
健康分支:
为每一次提交进行自动化构建、测试,确保没有在当前分支引入故障(可工作的无故障分支)
优点:
- 健康的公用分支,可以避免从这个分支上拉取的新分支工作在错误的基础上
- 健康分支更易入直接发布到产品
- 随时保持一个健康分支能极大降低集成的工作量并减少潜在风险。
- 便于进行差异调试。(可以将调试失败的commit去除,仅保留健康的commit)
- 在集成到主干时如果出现问题,就可以非常明确地确认是冲突导致而非代码错误
可能是公用分支、也可以是自己的分支。
自测代码是健康分支的核心,自测应可以快速得到结果(分钟级)
集成模式
主线集成
开发从主干拉取代码进行开发,集成,并将健康的更改推送回主干
从主干拉取分支到本地,并在本地分支上进行开发
开发过程中,其他开发已经提交了变更到主干分支
此时如需向主干提交,则首先需要合入其他的变更
有时,本地的修改和已经提交的变更会有冲突,此时需要先解决冲突
再重新将修改并自测后的健康代码提交到主干分支
使用主干时,意味着我们会实行主干集成模式
特性分支
为每一个功能特性拉取它独立的分支,并在feture完成后集成回主干
从主干拉取不同的特性分支并分别进行开发
开发过程中会持续从主干拉取最新代码,如果有其他功能变更,要及时进行集成解决冲突(本地)。
特性完成开发后,集成回主干。(受影响的冲突已在本地解决,所有其他特性分支上直接拉取变更,不会存在冲突需要解决)
低频集成
两条独立的开发分支上分别进行了几次本地提交,但没有向主干推送
主干上合入了变更
开发分支需要拉取变更并合入到本地分支(解决冲突)
分支A上继续完成了4,5两个提交,然后推送大主干
分支B上完成了4,5,6几个提交后,需要在本地合入主干上A的提交,集成后推送到主干。(本地合入的大小代表工作量)
高频集成
每次提交都向主干进行集成和推送呢?
V完成第1次提交后就推送回主干
S在完成第1次提交后就进行本地合入,并推送回主干
S的第2次推送,主干没有变更,没有本地合入工作
V完成第2次提交和本地合入,推送
S的第3次提交,需要本地合入,推送
如此,每次本地的提交,都会向主干进行推送
比较二者,高频集成会有更多的本地集成工作,但这些集成的工作量比低频集成低得多。
同时,比工作量减少更重要的是,这样做减少了风险。(虽然大多情况下,进行大的合入也能顺利完成,但有时却会有严重的冲突问题难以解决。这种情况,偶发的巨大问题比常规的小问题更糟糕)
如果每次常规集成10分钟,但50次集成合在一起要6小时,看起来6小时比500分钟还是要少,但是这会导致集成恐惧(if it hurt,do it more often)
而且高频集成模式下,可以更早发现代码间的冲突情况,并避免在冲突代码在后期才发现而导致中间持续产生了很多本不应该存在的更多冲突代码。
源码控制系统其实更多是一个沟通平台,可以意识到其他团队成员的工作
持续集成
开发者一旦完成一个健康的提交,就会尽快完成主干集成,一般不超过1天。
(如果高频集成更有效率且可实施,但使用特性分支,意味着限制了变更不能比一个特性更小,那么集成频次就成了问题?)
持续集成-只要特性上的开发是有进展并且是健康的,就可以向主干集成。唯一的原则就是每天都要进行集成(甚至更短)
开发团队需要习惯在主干上存在未完成特性的情况并考虑如何在产品中不暴露这些特性。(通过隐藏入口、配置控制),有时我们还可以实现灰度发布。
在持续集成模式下,开发着往往不需要创建自己的特性分支,只要在本地的主干分支上工作即可。
特性分支和持续集成的区别不在于是否有特性分支存在,而在于何时向主干集成。
两者的比较:
两种模式的差异更多取决于特性的大小,如果特性足够小(一天以内可以完成),就可以同时实行特性分支和持续集成。
特性分支最明显的问题在于合并的工作量和不确定性,然而更大的问题是它可能阻止重构。重构是软件优化最有效的途径,但是重构会产生冲突,当解决冲突工作量太大时,重构往往需要极大的勇气。
特性分支和开源软件
github的特性分支?pull request模式
上下文不同:分散、不定时、团队成员不了解 vs 全职、熟悉的团队成员、可靠
提交的审核review
- 代码审核·
- 集成受阻
- 模块化的重要性
发布模式
主干分支是活动分支,代码持续在其上开发。如果能保持主干的健康状态,任何人都可以从一个稳定的基础开始自己的工作。
而且足够健康的话,也可以直接从主干分支进行产品的发布
对于实施CI/CD的团队,一个常用的实践是通过打Tag标记来跟踪每一个发布的版本。但不实践CD的团队,则需要其他的发布模式
Release Branch 发布分支
预备发布的分支,只接受以版本稳定和交付为目标的提交 (一般系统测试阶段)
Release分支从主干分支中拉取,不再接受任何新feature的合入。工作在release分支上的开发工作主要是修复发现的bug或影响交付的功能。所有的修改最终还需要合并回主干。
尽管release分支上的工作往往比新feature的开发工作量更小,但是如果在其上的开发时间过长依然会导致合并回主干非常困难(主干上的提交越多,合入就越困难)
实际工作中,因为release分支的存在,合并回主干的操作往往容易被忽视,特别是有困难的冲突需要解决并且存在发布压力的情况下。
所以有的开发会采用另一种方式,继续在主干上开发,然后通过cherry-pick将只和发布相关的提交合入到release分支。这种方式的缺点是很多时候chrry-pick有时也很困难(对之前的commit有依赖的情况),在实际发布之前对release分支往往有一些重做工作。
对于只有一个产品版本的团队来说,使用单一的release分支就好。但很多情况下,会存在需要支持多个产品版本的情况,比如客户本地部署的软件,可能会长时间运行在一个历史版本上,但是又需要继续做一些bug fix或者一些特别的需求,此时就需要维护多个不同的release 分支,并定期保持和主干同步
对开发团队这明显是大大增加了维护难度和工作量,但这更多是产品策略带来的成本。唯一可以做的就是鼓励客户更及时地升级到最近版本(保持产品稳定非常关键,一旦产品不稳定导致问题,会使客户更倾向不再升级)
Release分支对于难以保持主干分支持续在健康状态的团队非常有必要,团队可以关注在产品发布的功能验证上。但对于单一产品的团队,release分支并不必要,使用主干分支并保持健康状态即可(发布的版本进行Tag标记)
另外,对于发布流程存在很多阻塞的团队,relase分支也是必要的(比如有比较缓慢的审核流程,在应用商店上线的时间窗、安全、法务、合规等的审核等)
成熟分支
分支的head总是标记为最近版本的成熟代码基线
根据团队对产品成熟度的不同定义,拉取出的对应的成熟分支。比如QA分支(提测)、Staging分支(预发)、production分支(上线)
但很多情况下,使用清晰定义的Tag机制就可以代替成熟分支的作用。比如“成熟级别-内部版本号”,“qa-233”,“prod-445”
长期发布分支
release分支一般在产品发布后就可以删除,但我们也可以维持一个长期的发布分支。它的作用可以看作是发布分支和成熟分支的集合。这个分支上进行release前的fix工作并合并回主线,并在实际发布时打上发布Tag,然后进入下一个版本的发布准备。
环境分支
通过不同的提交来配置代码的运行环境
不同的产品运行环境往往会有不同的配置代码(测试环境、开发环境、性能验证、安全验证环境、预发环境、产品环境等),环境分支会包含这些配置的变更提交并触发对应的构建、部署。
环境分支一般也用作成熟分支,比如QA的成熟分支,会包含QA环境的配置变更。
但是环境分支并不是好的实践,不同的环境需要不同的编译代码会引入风险。如果产品程序在不同的环境上有不同的行为表现会使问题的修复、定位变得极其困难。所以最好保证不同环境下运行的可执行代码都是相同的,环境的配置都通过显式的配置文件或变量来控制。环境变更导致的任何更改都不应包含在源码控制系统中
环境分支只应是缺乏这种环境切换控制机制团队的应急机制存在。
Hotfix
用于进行紧急产品故障修复的分支
从主线分支对应的release版本拉取hotfix分支,并在完成后合入回主线(如果release时间比较久,合并工作可能会比较大)
对使用release 分支的团队,也可以直接在release分支上进行hotfix,此时相当于将原release分支转变为hotfix分支
对于实践CD的团队,甚至可以直接在主干上完成hotfix(还是会拉取hotfix分支,但会从最新的提交拉取),因为一般团队已经具备了不公开未发布特性的能力。而且在hotfix过程中,不允许向主线进行任何新的提交。(hotfix优先)
如何定义hotfix?对于可以发布频率较高的团队,一般可以将问题纳入日常发布节奏而不需要进行hotfix。取决于团队发布频率和对业务的实际影响
release train
象火车发车一样有定期的发布间隔,特性完成后选择放入哪一次发布(和SAFe的Agile release train不是一个概念,那更多是组织层面的实践)
团队定义定期的发布计划,并确定每列发布列车上的功能,据此承诺、安排各自的工作任务。一旦火车发车,当前分支就转化为release分支,只会接受bug fix。发布列车一般会和feature分支一起使用。对于发布流程中存在较多阻塞的团队来说,release train是非常有用的,不会因为这些阻塞而影响后续功能的研发。
但比较明显的缺点是,如果一个功能很早就完成,也需要等到发车时间才能上线。
release train可以看作是团队release流程的一个提升手段,对与很难进行稳定发布的团队,实施CD往往不够现实 ,此时release train会是一个好选择。
loading future trains
和旧列车发车后再向新的列车上装载需求不同,可以使用这种模式的变种。如果一些功能不能确定是否能在预定发车前完成,可以同时开启两列train(预期发车时间不同),在三月列车上完成的开发工作同时向4月列车上提交并在三月列车发车后继续在4月列车上完成剩余工作。
这种模式可以让4月列车上的工作不影响三月列车,但缺点是如果4月列车上的变更影响到3月的功能,会是后续的合并工作更加复杂。
和主线发布比较
release train的主要好处是可以有一个定期的产品发布日历,但是多个分支依然会引入复杂性。
其实使用主线模式也可以如此工作,在主线上开发,到发布日期后增加一个对应的发布分支即可。
预发布主线
确保主线是绝对健康状态,并且主线的head永远能被直接发布到产品环境。(每个提交都具备直接发布的健康状态,但是只有实际发布的提交才需要加上tag)
其他发布模式
实验分支
在代码基线上收集用于实验的工作,这些功能不会被直接合入产品环境
实验性的点子
功能的不同实现方式
未来分支
一个用于对其他分支侵入性太大的变更分支
较少使用的模式,只会从主线pull而不会merge回其他分支。一旦使用,意味着团队中存在一个独立的代码特区。应尽可能缩短这个分支存在的时间。
合作分支
用于开发者之间协同工作,但不需要进行正式的集成的情况。
团队集成分支
在向主线集成前,允许子团队间先进行互相集成。(大型项目)