http://blog.sina.com.cn/s/blog_620eb3b20101hvz7.html
http://www.cnblogs.com/firstdream/p/5321366.html
解决版本冲突-使用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。
如果合并后的内容不满意,可以通过撤销来取消这次的合并操作,前提是未对合并后的文件做提交操作。
总结如下:
- 如果是需要将分支的改动合并到主线上,需要在主线的工作副本下进行合并,合并的范围是需要从分支上上次合并的版本到当前分支上最新的版本,如果主线和分支都修改了相同的文件,合并后会出现冲突,然后解决冲突,提交,如果是第一次合并,则起始版本号是上次建立分支的版本号;
- 相反,如果是需要将主线的改动合并到分支上,需要在分支的工作副本下进行合并,合并的范围是需要从主线上上次合并的版本到当前主线上最新的版本,合并后会出现冲突(冲突的前提如上种情况),然后解决冲突,如果主线修改但是分支没有修改,则主线上合并的变更内容会增加到当前副本中,提交,如果是第一次合并,则起始版本号是上次建立分支的版本号
合并的工作是把主线或者分支上合并范围内的所有改动列出,并对比当前副本的内容,由合并者手工修改冲突。如果当前工作副本是主线的,则合并的范围是分支上的改动,如果工作副本是分支的,则合并范围是主线上的改动。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
经常有人会说,树冲突是很难解决的一类冲突,其实一旦了解了其原理,要解决也不难。先回顾下对于树冲突的定义。
Delete : 其中目录结构变化,都认为是Delete
Edit: 是指修改文件
Local : 是你本地修改
Incoming :是别人修改,你要Update或Merge进来。
这样应该有4个组合,但是Edit对Edit的组合应该是File Conflict,这个容易解决,不在Tree Conflict 讨论范围,所以有3种组合。再需要区别Update和Merge,就有了6种情况。分别是
Local delete, incoming edit upon update
Local edit, incoming delete upon update
Local delete, incoming delete upon update
Local missing, incoming edit upon merge
Local edit, incoming delete upon merge
Local delete, incoming delete upon merge
分别对这几种情形解释如下:
1.Local delete, incoming edit upon update(本地删除,更新后传入修改)
产生原因:1.A修改文件Foo.c后提交到版本库中,B将Foo.c重命名为Bar.c或者删除了Foo.c或者直接将Foo.c的父目录Foo直接删除 2.B更新工作副本会提示该冲突,在working copy显示为Foo.c在本地删除,被标记为冲突。如果是重命名,则Bar.c被标记为新增,但是不包括A的修改。
解决:A与B要确认是否采用A的修改与是否重命名。如果采用A的修改,并且要重命名则修改后,标记冲突解决,svn resolved,最后提交;如果不采用A的修改,直接标记冲突解决提交即可。
2.Local edit, incoming delete upon update (本地编辑,更新后传入删除)
产生原因:1.A对Foo.c重命名为Bar.c并提交到版本库(或者A将Foo.c的上级目录Foo修改为Bar),B在他的工作副本中对Foo.c进行修改。2.B提交前更新,会提示如此错误。
解决:同样需要两个人进行协商后修改。
3.Local delete, incoming delete upon update (本地删除,更新后传入删除)
产生原因:1.A将Foo.c重命名为Bar.c后提交,B对Foo.c重命名为Bix.c。2.B更新本地工作副本是会提示该树冲突。
解决:通过日志查找文件被删除即重命名的原因,A与B协商后最终确认采用哪个名称。
4.Local missing, incoming edit upon merge (本地丢失,合并后传入修改)
产生原因:1.A在主干上修改Foo.c,B在分支上将Foo.c重命名为Bar.c。2.B合并A在主干上的修改。
解决:B先标记冲突解决,然后将Foo.c拷贝至本地,将A的修改合并至自己的文件中或者直接放弃A的修改,采用自己的修改。
5.Local edit, incoming delete upon merge (本地修改,合并后传入删除)
产生原因:1.A将Foo.c重命名为Bar.c(或者将Foo.c的父目录Foo改为Bar),B在分支上修改Foo.c。2.B合并A的修改时提示该冲突。Bar.c被标记为增加,Foo.c被标记为冲突。
解决:同样根据日志查找到修改的源头,两人协商后解决。
6.Local delete, incoming delete upon merge (本地删除,合并后传入删除)
产生原因:1.A在主干上将Foo.c重命名为Bar.c,B在分支上将Foo.c重命名为Bix.c。2.B合并A的修改时会提示冲突。重命名后的文件被标记为新增,原来文件被标记为树冲突。
解决:通过日志查找到文件被改名的时刻,两人协商后解决。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
svn merge部分总是在用的时候要搜资料,于是特意把这一部分弄出来,以备以后使用
为了做实验,要下载subversion,安装服务器,和TortoiseSVN客户端
subversion下载地址 http://subversion.apache.org/
下载下来之后如下的包
安装
成功后在命令下看
创建仓库
目录 E:\svn\repository
于是在目录 E:\svn\repository下可以看到如下的目录结构
我们安装进入subversion的安装目录可以看到如下的结构
安装目录:C:\Program Files\Subversion
其中bin里边就是subversion的所有命令
建一个库,并弄出一个branch和trunk,这两个最好是不一样的,然后我们才可以做以下的例子,不然也不用merge了,我做的库如下图所示
然后再随便找一个目录把其中睥一个branch或着trunk拉下来,我这里建了一个目录
C:\Documents and Settings\alecyan\桌面\test\abc
并把branch的代码拉了下来,下面我们开始做merge的一些例子
首先进入我们建好的目录中
进入C:\Documents and Settings\alecyan\桌面\test\abc
点空白处
开始merge
merge有三个选项,很多人对这个三个选项有点迷糊,我们这里就针对这三个选项进行详细的说明
第一个选项
这里是这个意思,这里可以把trunk的某个版本或着某个版本到某个版本的一个范围都可以merge到本地
点下一步后
下一步
下一步
在这个时候,可以先点一下test看看会出现什么情况,这个对我们的本地文件没有影响的
测试的时候可以发现文件有冲突
然后点merge
这里点resove all later就是merge之后一个文件一个文件的解决冲突
开始解决冲突
默认的解决冲突的工具,这个东西很好用,用一下就熟悉了
可以看到我们的本地多了很多文件
解决完冲突之后,点那个三角,意思是resoved已解决
第一选项完成
开始第二选项
第一个选项的意思 就是把某一个主动或着分支的某个版本merge到本地
下面的一些流程和第一选项基本一样
这里要注意,这个说明,如果选了这个选项,那么我们本地的文件必须不能有变化,要和版本库上一样才行
不然会如图所示
我们重新更新代码 ,继续
下面的操作就和第一选项一样了
我们说说第在个选项
第三个选是说可以merge不同的版本树到本地
再往下面就又和第一个第二个一样了
好了,三个选项都说明完了,以后,要是有点陌生的话,可以再看看这里就能马上想起来,心中有数就不会操作的时候犹犹豫豫的了。