导语
之所以探讨代码管理的话题,是因为维持良好的代码状态有利于开发团队顺畅地对代码进行可持续的重构和演绎,高效高质地响应不断变化的业务需求,提高团队产能。
从开发人员自身的角度出发,做好代码管理,也能让自己更愉快地投入代码编写中,不至于时常感受到像在一个摇摇欲坠陷阱重重的庞然大物中小心翼翼地堆砌,而是在稳固的地基上搭建城堡。
如何理解代码分支管理?
分支管理是软件配置管理(software configuration management)工作的组成部分,通过版本控制系统所提供的分支(branch)、标签(tag)、权限等功能对代码入库进行管控,以适配整个的软件开发工作流程。
软件配置管理的工作涵盖整个软件生存期,包含源代码的版本控制、各阶段的文档和变更,以及软件发布后的运行状态跟踪等。受篇幅所限,本文只讨论源代码的版本控制经验,后文简称分支管理。
工程源代码是软件开发中最重要的一类资产。为了维持源代码的健康度,通常会使用版本控制系统(VersionControl System,VCS)对源代码的变更历史进行记录追踪、标记版本等一系列源代码管理动作。
延伸阅读
按架构模式区分,版本控制软件可以分成中心化(centralized)与分布式(Distributed)两大类。
目前经常用到的两个开源版本控制系统是SVN与GIT。其中,SVN是中心化的。
使用SVN时要建立一个服务端,用以保存所有文件的历次修订记录并为客户端提供访问接口,访问修订记录则需要使用客户端访问连接服务端,客户端只保存有文件的最近提交记录。
GIT则是分布式的,每个客户端都完整保存有文件的历次修订记录,并且具备与其他客户端同步数据的能力。
(表1:常见的版本控制系统与特点)
4大类分支模式
在版本控制软件中最重要的一环是用户的访问权限控制。通过权限控制对代码库的检出、分支、合并、提交等动作实现配置管理的目标。
理想情况下对代码的变更是依次逐渐叠加,变更历史呈一条直线,如下图:
(图1:单一主干)
但实际工作中,受限于项目计划、交付方式、团队协作、研发环境、技术更替等因素,代码的演进路线通常不会是简单明了的一竿子见底,而是会衍生出许多分支,于是就出现了各具特色的分支模式。
笔者通过实际工作实践中总结的经验,将分支模式归类为四种情况:
1
单一分支开发&单一发布分支
单一分支协作开发是指仅使用一条分支来进行开发协作——提交代码、对特别的版本进行标记(TAG)等。查看分支图为一条直线式的(如下图),在集中式的代码库更为常见。
(图2:集中式代码库中的单一分支协作开发)
分布式代码库中也存在单一分支协作开发的模式,虽然会多出不少的合并,但本质上也是单一分支。
(图3:分布式代码库中的单一分支协作开发)
单一分支协作开发常见于项目初期、小型项目或贡献者人数少的项目,也可以视为主干开发(trunk base development)。
2
单一分支开发&多个发布分支
在软件的上线运行或交付使用过程中,开发团队常常会面临两方面的挑战:
一方面是继续跟踪维护交付的软件为其修复缺陷、增加功能。
二方面是要继续进行开发迭代新的版本。这时就需要使用分支管理过往版本。一般会使用开发代号进行分支命名,当过往版本的软件需要做修订,就在指定版本的对应的开发代号分支上进行修订并提交代码。
当修订后的代码经过验证达到发布条件后进行发布并使用TAG标记基线。
(图4:单一分支开发&多分支发布)
使用多分支进行发布还需要注意一项改动需要横跨多个分支进行合并的情况(例如缺陷)。缺陷在某个分支进行修复后,需要将变更(一个或多个commit)合并(pick)到其他同样受到影响的分支。有时这个操作可以使用代码库的指令来完成(例如gitcherry-pick)。有时需要人工修复多个版本(可能别的分支中的代码经过重构优化无法merge)。
总的来说,单一分支开发并在发布后创建分支的方式仍然可以视为主干开发。
3
多分支并行开发&单一发布分支
随着软件规模的扩大,开发团队人数增多,单一分支协作开发的模式开始遭遇瓶颈:能力稍弱的成员(或者刚刚进入团队尚未适应)编写出的代码可能与规范的要求有差距,需要二次加工;软件在规划中制定了一些尚未明确的功能,有待商榷;部分功能延期,需要剥离……
情况不再是单一、明确、可控的,种种困境迫使开发团队采取“多分支并行开发&单一发布分支”的方式来管理代码库。
以SAAS服务为例,线上的版本是一致递增的,不会有维护多个发布分支的需要。但是选择哪些功能上线是需要在发布前确定好,这时多分支并行开发就有好处。
(图5:多分支开发&单一分支发布)
在实际使用中,为了方便测试与发布准备,实际的多分支并行开发&单一发布分支中间会增加几个分支用作缓冲。例如先使用一个test分支用作合并feature并先行测试验证。测试验证过后,再合并到staging分支以备发布。发布实际完成以后再合入main。
(图6:gitflow 图片来源:https://www.eclipse.org/community/eclipse_newsletter/2015/february/article3.php)
在采用多分支并行开发时,待开发功能和分支存在对应关系。一般每个需求会通过编号对应一个分支(需求拆分)。
因此采取了多分支并行开发,可以在合并前增加统一前置检查或评审(原始svn、git等的代码库软件没有内建的线上评审机制,此类功能都需要在代码库软件的基础上进行自主扩展),并由专门指定角色进行合并。前置检查也可以设计为程序化的检查方法。
多分支并行开发还有很多的变体。现在在git上经常能见到的,pull-requestmerge-request (githubgitlab bitbucket)也归属此类。
(图7:以spring为例,每个人可以提交feature供合并)
如图,每一个待合入的修订,都可以设置前置检查项,既可以是指定人的评审同意,也可以是程序化的自动检查。
金无足赤,多分支开发也有其劣势:
缺陷修复成本高:既需要在发布分支中修复,也需要将变更合并到几乎所有的开发分支;
优化重构成本高:与缺陷修复类似,对代码的优化和重构也需要合并到几乎所有开发分支;
冲突滞后,合并成本高:并行开发会延后冲突发现。多个功能对同一个文件的修改会造成冲突,越晚合并的人冲突解决越困难。特别是指定专门负责合并时,负责合并的人并不实际编写产生冲突的代码,难以处理合并冲突,只能找实际写文件并造成冲突的几方协商,这中间的协调与时间成本会增大;
延迟风险:功能经合并(评审)后才能进入后续的流程,这在遇到紧急缺陷处理时会造成延迟;
回滚困难:合并后的变更撤销(回滚)依然困难,实际操作中往往会通过删除合并分支并重新合并。
4
多分支并行开发&多个发布分支
多分支多环境是这四类中最复杂的情况,混合了多分支并行开发与多发布分支。
视开发发布过程的繁简程度差异可进一步细分为两种类型:
1.多分支并行开发,多个版本分支(发布后创建分支)
这一类实际是两个分支以上的并行开发。大的范围上看在github上管理的大型开源项目(例如spring、linux)可以归属此类。特点是有众多的内部成员提交代码,并且有众多的三方可以folk项目进行修订。项目主体负责跟踪多个版本的缺陷修复。
(图8:多个分支开发,跟踪多个版本)
2.多分支并行开发,多版本准备分支(提前创建发布分支)
对比上面那一类,这一类不仅仅是多分支并行开发,同时在版本的发布准备上采取了多分支并行的策略。为了准备多个版本(当期、下期、下下期,或者每个测试环境对应一个分支),必须提前创建好发布用分支,在并行开发时需要选择开发的功能要合并到哪一个版本分支进行测试和发布。这相当于延迟发布决策,直到发布临近才根据开发进展或测试进展决定发布哪个版本(或者临时再开立投产分支合并代码)。
(图9:AoneFlow,来源:https://zhuanlan.zhihu.com/p/93125600)
多分支多环境的方式更适合用来解决提前开发功能或者超前开发功能的问题,也支持延迟发布决策与功能上线决策。(多分支多环境往往到最后会只有一个版本选中来正式发布,这就变成了多分支并行开发,单一发布分支)。
有利也有弊,多分支多环境的灵活性背后对管理和控制有着更高的要求。多分支并行开发会遇到的问题在这种模式下也通通都要克服。不仅如此,由于采用了多个环境分支,团队在开发上还会额外面临新的问题:
更多的功能合并及撤销成本;
更多的测试验证成本;
延迟决策带来的需求不确定性,返工以及功能废弃造成的浪费;
更繁重的代码库管理操作,以及更多的持续集成配置工作;
以上是按照分支在开发运维的多少来进行划分,供大家参考。
代码分支管理实践
使用代码库已是软件开发企业的必备操作,企业也会结合自身需求对代码库管理践行相应的策略。上文谈到我们所理解的代码分支管理内涵,这一节来谈谈我们在长期咨询工作中所观察到的代码分支管理实践过程中遇到的问题与解决方案。
首先看常见的5类问题,这些问题不分先后,也不是全部,出现这些现象可能是企业的管理偏好所致,妥善地处理这些问题可能会帮助改善代码管理质量,这也是我们强调代码分支管理的必要性所在。
(图10:常见代码分支管理的5类问题)
理解了代码管理的含义与必要性,再看看具体怎么做。
1
规划好版本发行计划
代码库的本质是一个带修订记录功能及读写权限控制的文件系统,无论如何实施管理都无法实现完全自动化的合并或撤销任意变更。而一个软件需求会分散到多个源文件中,于是需求之间产生依赖关系。
理想情况下,软件的发布路线应当同代码库的分支模式是匹配的。但是在实践经常会碰到软件版本路线简单但代码库分支混杂的情况(因为版本管理不力而导致了多分支多环境开发)。
(图11:spring代码库的分支)
(图12:spring代码库的部分tag,总计200+)
以著名的开源框架springf ramework为例。该软件所采取的是多个分支开发(对内部一般一个主干分支,对外开放合并请求),在版本达成后创建分支进行发布,对完成正式发布的那次commit使用tag进行基线标记。并对该版本持续维护一段时间的策略。从软件的配置管理策略上看,该软件采取的是多版本共存维护的方式,每个分支版本会维护一个周期。在代码库上也能看出这一点。
在企业中内部自研、自运维的软件中常常会看到另一种情况——软件的是自用自研,发布运维也是自己,软件发布路线也简单,版本始终是递增,不太需要多版本并存的支持,但代码库管理却很复杂。造成这一现象的原因主要在于版本发行计划规划不清晰。
在采用迭代增量式开发的团队,每一个迭代都要对软件进行变更。每一个迭代之前必须规划好这次迭代的所要开发的功能。这时会存在几种现象:
分析好的需求,并经过评审确定上线日期,分迭代开发,并分批投产(scrum式的决策);
有需求但是要边做边细化,作出原型以后才决策投产时间或废弃(延迟决策);
大型需求,分迭代开发,但必须到最后才能一次性投产(超前开发)。
各种因素混合在一起,往往导致迭代开发走样,不得已变成了多分支多环境的开发测试。而这也会给研发团队带来代码管理上的混乱(特别是对于自动化能力程度低的开发团队):
多次合并,冲突增加
多次测试,对主要采取人工测试的团队压力更大
上线遗漏或合并遗漏
主干开发分支发布,并不仅仅是在代码库的分支管理采取单一分支编码提交就可以。
主干开发实际上还会要求软件的版本发布规划或功能规划也是按照一个路线图来执行,有稳定的发布计划,并能通过管理与技术手段控制好需求变更。
当开发组织有频繁的需求变更、废弃,或者延时上线决策的情况存在时是无法简单采用主干开发分支发布的。
2
一种双版本分支并行开发的策略
如果团队不能开展单一主干开发,也无法施行多分支并行开发单一发布分支的方式。那么我们还有一个建议:就是采取版本火车分支策略:最多两个环境分支。
(图12:版本火车分支策略)
版本火车分支策略的核心要点包括:
在进行版本规划时需要规划当期与下期2个版本,超期的需求只做分析和规划工作,不进入编码环节;
开立两个环境分支R1、R2,对应两个环境;
开设功能分支来开发对应功能,自测完成后审核并合并指定环境分支;
R1投产后R2 rebase R1;
具体的实施细节包括:
功能合入前须进行代码评审;
各项功能分支尽量保持自由代码而不去合并其他的功能。如果遇到了bug fix,可采取rebase main分支的方式;
合并冲突的处理方式:
当两个文件中的字词产生歧义,通常是语义冲突,例如两个程序员都新增了一个方法“isBlank”但定义不同,此类冲突应当由双方协商重新改变语义,并追加修订再次提交;
如果是纯粹的文件无法合并——受到合并工具机能限制,看上去并没有语义冲突的代码,报告冲突,例如空白、换行位置、插入位置等——此时要合入代码的人应该采取人工解决冲突的方式,解决冲突后再次提交。为了避免在自己代码中混合其他需求的代码,此时程序员应该新建一个分支来解决冲突,并在后续的代码提交中始终在这个分支上解决冲突。
遇到需求废弃的情况,可通过重建环境分支,各个需求重新合并入环境分支的方式解决。以上操作本着尽量保持各功能分支代码独立的目标。但,对于缺陷、重构等类型的修订并不适用。
对于缺陷,应当及时在各分支生效。其中,缺陷修复要优先在main生效。
main生效的修复,需要通知各个开发人员rebase main。
对于重构,如历史技术债务修改或代码检查工具集中检查出的问题,由于涉及文件较广,建议按照文件为单位进行批量修改。按照问题类型进行修改也并非不可行,但会造成提交变更文件太多,冲突概率提高,因此我们更提倡以文件为单位。或者按照功能模块划分,各自认领的方式分开修改。
3
设计功能开关
除了做好版本规划、用功能分支隔离功能外,还可以考虑在代码的设计上植入功能开关来减少分支管理的混乱。
功能开关可以分为两种机制,其一是静态编译开关,其二是运行时功能开关。
静态编译开关在C一类的源代码中比较常见,通过设置编译开关,可以在构建可执行文件时构建出含有不同功能的二进制文件。
运行时功能开关指的是一项功能在运行时可以通过管理控制台人为的开启或关闭。在WEB类、SAAS类开发中比较常见。运行时功能开关也可以进一步细分为:需要重启进程和无需重启即时生效两种。
一项需求在进行分析与概要设计时,就应该考虑是否需要服务的升降机机制,以及对该项功能设置运行时开关。
通过功能开关的开启和关闭实现功能在运行时的在线与下线。在设计功能开关时需要注意:
功能开关是一项架构设计决策,要在需求分析或概要设计时加以讨论并明确说明;
功能开关增加了代码逻辑,本质上是一个(if-else);
测试案例设计要考虑开关分支;
额外增加的部署复杂性,比如要考虑不正确的生产环境下开关配置的风险。对功能开关值的设置工作也需要纳入配置管理,并支持版本控制。
功能开关不能完全替代分支管理,但功能开关的引入可以简化一部分的分支管理操作,简化分支管理操作的同时自动化的持续集成也会简化。
4
完善工具链与自动化处理
工具链与自动化处理的完善可以从代码库的可用性、工具可用性体验、信息同步和代码库自动化几个方面着手进行。
1.高可用的代码库
代码库是所有软件开发人员天天要面对的,必须具备高可用的能力,此外还需要能够轻松的集成到开发环境中。即便是GIT分布式代码库,企业内部总会有集中管理的诉求,并搭建统一的代码库基础设施。
代码库要具备高可用特性,防止在关键时刻造成工作中断。中断往往发生在工作集中、大量负载时刻,会造成工作延误。代码库也会与持续集成服务相连接,代码库服务的中断也会造成构建中断。
开发人员日常工作中可以从以下几点来提升代码库的可用性:
按照生产的标准维护代码库基础设置,设置冗余备份和负载均衡;
内部公开运维计划与代码库系统监控页面,比如提供平均无故障时间指标和系统负载指标;
2.增强工具可用性体验
代码库不仅仅是一个web服务,还会有客户端,每个开发者都需要使用客户端来进行代码提交等等的操作。因此工具好用与否也是衡量配置管理工作或者版本控制系统功能的标准。以下是优化工具体验的几个具体策略:
版本控制客户端要能够集成到IDE(集成开发环境)中,在IDE中直接进行版本控制操作减少工具切换;
好用的差异比较工具,通过好的工具辅助开发者比较文件差异,并且开发人员应熟练使用差异比较工具;
自定义的集成:工具之间的间隙,往往需要人工操作来补偿,最好通过自动化脚本对工具集成。例如可以在提交之前提醒开发者写错作者或遗漏提交备注、或者文件内容有问题;
配合内部规划对建库(init)过程进行定制,以集成全部的初始化操作。
3.信息同步
打通代码库工具与上游下游的连接,并实现信息的聚合,一方面可以优化代码管理质量,二方面可以提升开发团队协作效率。
常见的操作例如:
(图13:信息同步常见操作)
4.代码库的自动化
对代码库管理要进行脚本编程或功能扩展,以实现自动化处理。代码库软件通常都会提供扩展机制,以便于在工作流程中进行拦截处理。
例如:
在提交时,提交之前进行拦截处理。以检查提交人名字格式是否符合规范并记录在册,备注消息是否按照格式书写;
对提交的内容进行快速的检查,排除危险的内容:私人密钥、明文密码、错误的文件名、字符集错误的文件、超大的文件,在黑名单中的文件或路径;
提交完成之后的自动化检查:一般会检查:编译错误、缺少依赖、增加的技术负债、自动化测试失败。自动化检查放在提交后做的原因是通常这些都比较耗时间,超过1分钟会对程序员造成影响且增加了代码库系统的负载;
定时做检查:例如周期性检查代码是否存在安全漏洞,并提醒修复;
方便分支管理操作:例如自制需求合并发布分支的脚本(界面)通过界面可便于批量合并功能到发布分支,并在合并失败时告警。再如,在上线后自动(自助)合入main分支,并标记tag,做好上下游关联。或者,提醒不正确的分支合并。一位开发者正准备发起合并操作时提醒他此项功能不应该在这个分支中。
类似的自动化措施还有很多,这通常需要深入开发团队观察开发团队的工作并汇集成为一个自动化脚本,同时需要配备一位专业的工具管理员(或工具管理团队或工程效能团队)来设计自动化脚本。
作者|雷晓宝,Agilean资深咨询顾问
参考文献:
使用功能开关更好地实现持续部署-InfoQ
https://www.infoq.cn/article/function-switch-realize-better-continuous-implementations
谈谈代码分支管理
https://zhuanlan.zhihu.com/p/93125600
GB/T 20158-2006 信息技术 软件生存周期过程 配置管理
GB/T 19017-2020 质量管理 技术状态管理指南
往期回顾
01
9个从零开始的B端产品自动化测试实践
02
金融组织数字化研发管理的12种武器
03
10人小团队如何打造支持千人组织的B端产品?
04
四剑客与code review的恩怨情仇