[0] 解决版本冲突-使用SVN主干与分支功能
解决版本冲突-使用SVN主干与分支功能
1 前言
大多数产品开发存在这样一个生命周期:编码、测试、发布,然后不断重复。通常是这样的开发步骤:
1) 开发人员开发完毕某一版本(如版本A)功能后,提交测试;
2) 测试人员对待发布版本A进行测试,同时开发人员继续开发新功能(如版本B);
3) 测试人员提交bug,研发人员修复bug,同时继续开发新功能;
4) 重复第3步骤,直到待发布版本A测试通过测试后,发布第一版本
这样就会存在以下问题:
1) 如何从代码库中(A+B)分离出待发布版本A,进行测试和发布;
2) 如果单独存放待发布版本A,那么开发组必须同时维护此版本库A以及当前最新代码库(A+B),操作冗余且容易出错。
在SVN中,通常采用主干(trunk)与分支(branches)的方法,解决以上问题。
2 相关概念和原理
在SVN中创建代码库时,通常会创建trunk、branches、tags三个子目录,当然,你也可以用其他名称来实现主干和分支的功能
trunk-主干,或称主线,顾名思义,是开发的主线。
branches-分支,是从主线上分出来,独立于主线的另一条线。可以创建多个分支。一个分支总是从主干一个备份开始的,从那里开始,发展自己独有的历史(如下图所示)。在版本控制的系统中,我们经常需要对开发周期中的单独生命线作单独的修改,这条单独的开发生命线就可以称为Branches,即分支。分支经常用于添加新的功能以及产品发布后的bug修复等,这样可以不影响主要的产品开发线以及避免编译错误等。当我们添加的新功能完成后可以将其合并到主干中。
tags-标记,主要用于项目开发中的里程碑,比如开发到一定阶段可以单独一个版本作为发布等,它往往代表一个可以固定的完整的版本。即主干和分支都是用来进行开发,而标记是用来进行阶段发布的。安全公司的配置库有专门的发布区,所以tags并不需要创建,在这里只是提供说明,不推荐使用。
branches以及tags在TortoiseSVN中创建方法是一致的,它们都是通过存储类似Linux中的lunch快捷方式一样,只是创建了指向某个版本的链接,而不会真正将此版本的内容复制到分支或者标记中,这样既可以节省空间,也可以很快速的创建,被称为“廉价的拷贝”。
为了便于创建分支和标记,通常习惯于将Repository版本库的结构布置为:/branches,/tags,/trunk。分别代表分支,标记以及主干。
还有一点值得注意的是,SVN不推荐在创建的tag基础上Revision,这种情况应用branches,因为tag一般保持不变不作任何修改。
3 代码的分支管理策略
关于代码管理的分支和发布策略,目前主要有两种:一种是主干作为新功能开发主线,分支用作发布。另一种是分支用作新功能开发,主干作为稳定版的发布。
3.1 分支用来发布
典型操作步骤如下:
1) 开发者提交所有的新特性到主干。 每日的修改提交到/trunk:新特性,bug修正和其他。
2) 这个主干被拷贝到“待发布”分支。 当小组认为软件已经做好发布的准备(如,版本1.0)然后/trunk会被拷贝到/branches/1.0。
3) 项目组继续并行工作,一个小组开始对分支进行严酷的测试,同时另一个小组在/trunk继续新的工作(如,准备2.0),如果一个bug在任何一个位置被发现,错误修正需要来回运送。然而这个过程有时候也会结束,例如分支已经为发布前的最终测试“停滞”了。
4) 分支已经作了标记并且发布,当测试结束,/branches/1.0作为引用快照已经拷贝到/tags/1.0.0,这个标记被打包发布给客户。
5) 分支多次维护。当继续在/trunk上为版本2.0工作,bug修正继续从/trunk运送到/branches/1.0,如果积累了足够的bug修正,管理部门决定发布1.0.1版本:拷贝/branches/1.0到/tags/1.0.1,标记被打包发布。
整个过程随着软件的成熟不断重复:当2.0完成,一个新的2.0分支被创建,测试、打标记和最终发布,经过许多年,版本库结束了许多版本发布,进入了“维护”模式,许多标记代表了最终的发布版本。
这种分支管理策略被广泛的应用于开源项目。比如freebsd的发布就是一个典型的例子。
freebsd的主干永远是current,也就是包括所有最新特性的不稳定版本。然后随着新特性的逐步稳定,达到一个发布的里程碑以后,从主干分出来一个stable分支。freebsd是每个大版本一个分支。也就是说4.x,5.x,6,x各一个分支。每个发布分支上只有bug修改和现有功能的完善,而不会再增加新特性。新特性会继续在主干上开发。当稳定分支上发生的修改积累到一定程度以后,就会有一次发布。发布的时候会在稳定分支上再分出来一个 release分支。以6.x为例,就会有6.0,6.1,6.2…等发布分支。
这种发布方法非常适用于产品线的发布管理。产品是要卖的,以前卖给客户的版本仍需要继续维护,而为了以后的市场,新功能也不断地在增加。这种管理方法对已发布产品的维护工作和下一代产品的开发工作进行了隔离。对于已经发布的产品,只有维护的补丁发布。而新发行的产品不仅包括了所有的bug修改,还包括了新功能。
这种方法具有如下缺点:首先,必须对主干上的新功能增加进行控制。只能增加下一个发布里面计划集成进去的新特性。而且,已经在主干上集成的新特性中的任何一个,如果达不到里程碑的要求,稳定分支就不能创建,这很有可能影响下一个发布的计划。开源项目可能这方面的压力小一些,但是商业产品开发如果碰到这种情况就危险了。还有一个缺点就是bug修改必须在各个分支之间合并。从分支和合并的一些实践经验上看,各个长期存在的分支之间必须要周期性的进行合并,否则很容易引发合并冲突。可是各个stable分支以及release分支之间恰好是不能进行合并而且还要长期存在的。因此,采用这种分支策略可能碰到的最大问题就是某个分支上的bug修改内容往其它分支merge的时候出现的冲突。而且一旦发现一个bug,调查这个bug影响哪些分支的工作会随着维护的发布分支的数量而增加。
在非产品开发的外包软件项目里面,这种发布方法的好处体现不出来,而缺点仍然存在。外包项目的特点是客户永远需要“最新”的代码,因此对已经发布的某个分支进行维护的情况很少出现(在测试的时候会出现)。而且发布的方法和产品的发布也不一样。产品的发布,只要把发布分支上的代码编译成安装盘就可以了,而外包的发布往往是把上一次发布和这一次发布之间发生变化的代码送给客户。如果每次发布都是一个分支的话,将会出现两个分支上的比较。强大的版本控制工具当然支持这种比较,但是很多版本工具不支持分支之间的比较,而只支持分支内的不同版本之间的比较。因此为了避免发布方法受工具的限制,就要避免出现分支间比较的情况。针对外包开发的特殊情况,只有采用另外一种分支管理策略。
3.2 主干用来发布
与第一种分支策略正好相反,主干上永远是稳定版本,可以随时发布。bug的修改和新功能的增加,全部在分支上进行。而且每个bug和新功能都有不同的开发分支,完全分离。而对主干上的每一次发布都做一个标记而不是分支。分支上的开发和测试完毕以后才合并到主干。
这种发布方法的好处是每次发布的内容调整起来比较容易。如果某个新功能或者bug在下一次发布之前无法完成,就不可能合并到主干,也就不会影响其他变更的发布。另外,每个分支的生命期比较短,唯一长期存在的就是主干,这样每次合并的风险很小。每次发布之前,只要比较主干上的最新版本和上一次发布的版本就能够知道这次发布的文件范围了。
这种发布模式也有缺点。如果某个开发分支因为功能比较复杂,或者应发布计划的要求而长期没有合并到主干上,很可能在最后合并的时候出现冲突。因此必须时刻注意分支离开主干的时间。如果有的分支确实因为特殊的需要必须长期存在,那就必须定期把主干的更新往这个分支上合并。为了减少这种合并发生的次数,并且限定合并的范围,要为每次发布预先建立一个发布分支,然后所有的开发分支根据自己的发布计划向各个发布分支合并。当下一次发布的分支上已经集成了所有的变更并且测试完毕以后,把这个发布分支内容合并到主干,发布主干,然后锁定或者删除这个分支。然后把主干上的所有更新合并到后面几个发布分支里面去。外包项目的发布周期一般都比较短,往往客户验收测试的周期就是发布周期。所以这种方法就够用了。如果发布周期很长,各个发布分支之间还要定期的从前向后合并。这种发布方法还有一个缺点就是测试。不像第一种分支策略,发布的分支就是测试的分支。这种发布模式的测试分支往往是各个发布分支,在正式发布之前才把下一个发布分支上的更新合并到主干,这就引入了合并出错的风险,而主干上的程序是没有经过测试的。幸好从这个发布模式上看,下一个发布分支的合并基础应该和主干上一次发布内容相同,所以引入合并错误的风险很低。还有一种建议就是不设置主干,下一个发布分支就是主干,直接发布下一个发布分支的变更内容,然后把变更合并到再下一个发布分支上去。以此类推。
3.3 注意事项
1) 做分支上做开发的时候,必须定期使分支与主干同步,避免开发完成后合并(merge)回主干时出现严重冲突(confict);
2) 进行合并前,处理掉工作副本上的所有本地修改,方便合并失败时进行回滚(revert);
3) 进行合并时,特别注意 新增/删除 操作,因为很多冲突都是这类操作引起的;
4) 完成一个分支的功能并合并回主干后,抛弃该分支,后续其它功能的开发使用新建的分支。当然,也有办法继续使用该分支;
5) 辅助文档是必需的。为了观察分支的创建和合并的过程,至少需要一份类似泳道图的文档标记每一次分支创建和合并的过程;
6) 开发分支往主干或者发布分支合并的次数应该尽可能少。一般来讲应该在单体测试结束合并到主干或者发布分支,然后进行结合测试。如果结合测试里发现bug不应该在原来的开发分支上继续修改,而应该创建新的分支进行修改;
7) 分支创建和合并的log必须规范。便于以后查找。基本的log信息应该包括从哪个分支的哪个版本创建分支;把哪个分支的从哪版本到哪个版本范围内的变更合并到了哪个分支的哪个版本,合并后的版本号。这些信息有一些是版本控制工具本身可以很方便查找到的,就可以省略
4 操作步骤
在代码库中创建trunk、branches、tags目录,分别为主干、分支和标记,这样的布局是为了更清晰的区别主线、分支和标记三者的位置。在主干上提交代码,到可发布的程度时,创建分支。
为便于比较结果,我们在主干中上传一个文件readme.txt(版本为659):
4.1 创建分支(标记)
将主干trunk签出(checkout)到本地,在本地checkout的trunk目录上单击鼠标右键,在弹出菜单中选择“TortoiseSVN” →“Branch/tag…”
在下图弹出的窗口中,将“To URL” 指向branches目录并输入分支的具体目录名。默认的目标URL将会是你当前工作拷贝所处的源URL,必须给分支/标记编辑一个新路径。SVN不会自动递归创建目录,要自己先创建好父目录。比如想创建分支/branches/V1.0,那么V1.0可以不用自己创建,但是/branches要先创建好。这里是branches/V1.0,我们即将创建的分支便存放于此处,点击OK
上图中红色方框内Create copy revision in the repository下的选项:
u HEAD revision in the repository:拷贝当前主干中的最新版本。不需要从你的工作副本中传输任何数据,这个分支的建立是非常快的。
u Specific revision in repository:拷贝主干中的某个指定版本。假如你在上周发布了项目时忘记了做标记,这将非常有用。如果记不起来版本号,通过点击鼠标右键来显示版本日志,同时从这里选取版本号。和上次一样不需要从你的工作副本中传输任何数据,这个分支建立起来是非常快的。
u Working copy:新的分支是一个完全等同于你的本地工作副本的一个拷贝。如果你更新了一些文件到你的工作副本的某个旧版本里,或者你在本地做出了修改,这些改变将准确无误地进入拷贝中。自然而然地这种综合的标记会包含正在从工作副本传输到版本库中的数据,如果这些数据还不存在的话。
选择完毕后单击【OK】按钮,则分支创建完毕。再次查看配置库,可以看到刚才创建的分支中包括主干中的文档“readme.txt”,版本为659,同主干一致。
标记的创建方法同分支一样,都是对主干的拷贝操作(实际是对某一版本的链接)。
4.2 合并分支
分支用来维护独立的开发支线,在一些阶段,你可能需要将分支上的修改合并到最新版本,或者将最新版本的修改合并到分支
为便于比较结果,我们修改分支中的readme文件(此时版本为664),同时添加一个文件:
如果想将分支合并到主干上,在本地checkout出的主干(trunk)目录上单击鼠标右键,在弹出菜单中选择“TortoisesSVN”→“Merge”
在弹出的“Merge”菜单中选择类别:
在“URL to merge form”输入框中选择分支的URL,在“Reverse range to merge”填入版本,可点击【show log】按钮选择需要合并的版本。需要注意的是Merge并非字面上所示的将两个分支归并到一起,而是diff-and-apply的意思,比较两个分支的差异并归并差异。输入完毕后单击【Next】:
选择合并选项后(如“Compare whitespaces”),单击【Merge】,完成合并操作。
如果在合并过程中发生冲突,SVN会进行提示:
进行合并后,在本地的trunk目录会显示以下文件:
冲突的文件图标中会有一个叹号,同时系统自动生成3个文件:
u readme.txt为合并前主干中的版本
u readme.txt.merge-left.r.664:为664版本,即创建分支时主干中的版本
u readme.txt.merge-right.r665:为665版本,即合并前分支中的版本
可以直接打开文件进行手动修改,冲突的内容会以议<<<<<<<…………>>>>>>>标识
也可以选中该文件,右键→TortoiseSVN→Edit conflicts,TortoiseMerge窗口会显示冲突文件对比,可以在merged对话框中进行编辑:
修改完毕后,右键→TortoiseSVN→Resolved,此时系统自动生成的3个文件会自动删除,冲突文件的图标会变为未提交状态,右键→SVN commit,提交到配置库。
当有多个文件conflict时,需要逐个resolve。
如果合并后的内容不满意,可以通过撤销来取消这次的合并操作,前提是未对合并后的文件做提交操作。
总结如下:
- 如果是需要将分支的改动合并到主线上,需要在主线的工作副本下进行合并,合并的范围是需要从分支上上次合并的版本到当前分支上最新的版本,如果主线和分支都修改了相同的文件,合并后会出现冲突,然后解决冲突,提交,如果是第一次合并,则起始版本号是上次建立分支的版本号;
- 相反,如果是需要将主线的改动合并到分支上,需要在分支的工作副本下进行合并,合并的范围是需要从主线上上次合并的版本到当前主线上最新的版本,合并后会出现冲突(冲突的前提如上种情况),然后解决冲突,如果主线修改但是分支没有修改,则主线上合并的变更内容会增加到当前副本中,提交,如果是第一次合并,则起始版本号是上次建立分支的版本号
合并的工作是把主线或者分支上合并范围内的所有改动列出,并对比当前副本的内容,由合并者手工修改冲突。如果当前工作副本是主线的,则合并的范围是分支上的改动,如果工作副本是分支的,则合并范围是主线上的改动。