在分支间复制修改
在分支间复制修改
现在你与Sally在同一个项目的并行分支上工作:你在私有分支上,而Sally在主干(trunk)或者叫做开发主线上。
由于有众多的人参与项目,大多数人拥有主干拷贝是很正常的,任何人如果进行一个长周期的修改会使得主干陷入混乱,所以通常的做法是建立一个私有分支,提交修改到自己的分支,直到这阶段工作结束。
所以,好消息就是你和Sally不会互相打扰,坏消息是有时候分离会太远。记住“闭门造车”策略的问题,当你完成你的分支后,可能因为太多冲突,已经无法轻易合并你的分支和主干的修改。
相反,在你工作的时候你和Sally仍然可以继续分享修改,这依赖于你决定什么值得分享,Subversion给你在分支间选择性“拷贝”修改的能力,当你完成了分支上的所有工作,所有的分支修改可以被拷贝回到主干。
复制特定的修改
在上一章节,我们提到你和Sally对integer.c
在不同的分支上做过修改,如果你看了Sally的344版本的日志信息,你会知道她修正了一些拼写错误,毋庸置疑,你的拷贝的文件也一定存在这些拼写错误,所以你以后的对这个文件修改也会保留这些拼写错误,所以你会在将来合并时得到许多冲突。最好是现在接收Sally的修改,而不是作了许多工作之后才来做。
是时间使用svn merge命令,这个命令的结果非常类似svn diff命令(在第 2 章 基本使用的内容),两个命令都可以比较版本库中的任何两个对象并且描述其区别,举个例子,你可以使用svn diff来查看Sally在版本344作的修改:
$ svn diff -c 344 http://svn.example.com/repos/calc/trunk Index: integer.c =================================================================== --- integer.c (revision 343) +++ integer.c (revision 344) @@ -147,7 +147,7 @@ case 6: sprintf(info->operating_system, "HPFS (OS/2 or NT)"); break; case 7: sprintf(info->operating_system, "Macintosh"); break; case 8: sprintf(info->operating_system, "Z-System"); break; - case 9: sprintf(info->operating_system, "CPM"); break; + case 9: sprintf(info->operating_system, "CP/M"); break; case 10: sprintf(info->operating_system, "TOPS-20"); break; case 11: sprintf(info->operating_system, "NTFS (Windows NT)"); break; case 12: sprintf(info->operating_system, "QDOS"); break; @@ -164,7 +164,7 @@ low = (unsigned short) read_byte(gzfile); /* read LSB */ high = (unsigned short) read_byte(gzfile); /* read MSB */ high = high << 8; /* interpret MSB correctly */ - total = low + high; /* add them togethe for correct total */ + total = low + high; /* add them together for correct total */ info->extra_header = (unsigned char *) my_malloc(total); fread(info->extra_header, total, 1, gzfile); @@ -241,7 +241,7 @@Store the offset with ftell() ! */ if ((info->data_offset = ftell(gzfile))== -1) { - printf("error: ftell() retturned -1.\n"); + printf("error: ftell() returned -1.\n"); exit(1); } @@ -249,7 +249,7 @@ printf("I believe start of compressed data is %u\n", info->data_offset); #endif - /* Set postion eight bytes from the end of the file. */ + /* Set position eight bytes from the end of the file. */ if (fseek(gzfile, -8, SEEK_END)) { printf("error: fseek() returned non-zero\n");
svn merge命令几乎完全相同,但不是打印区别到你的终端,它会直接作为本地修改作用到你的本地拷贝:
$ svn merge -c 344 http://svn.example.com/repos/calc/trunk U integer.c $ svn status M integer.c
svn merge的输出告诉你的integer.c
文件已经作了补丁(patched),现在已经保留了Sally修改—修改从主干“拷贝”到你的私有分支的工作拷贝,现在作为一个本地修改,在这种情况下,要靠你审查本地的修改来确定它们工作正常。
在另一种情境下,事情并不会运行得这样正常,也许integer.c
也许会进入冲突状态,你必须使用标准过程(见第 2 章 基本使用)来解决这种状态,或者你认为合并是一个错误的决定,你只需要运行svn revert放弃本地修改。
但是当你审查过你的合并结果后,你可以使用svn commit提交修改,在那一刻,修改已经合并到你的分支上了,在版本控制术语中,这种在分支之间拷贝修改的行为叫做搬运修改。
当你提交你的修改时,确定你的日志信息中说明你是从某一版本搬运了修改,举个例子:
$ svn commit -m "integer.c: ported r344 (spelling fixes) from trunk." Sending integer.c Transmitting file data . Committed revision 360.
你将会在下一节看到,这是一条非常重要的“最佳实践”。
为什么不使用补丁?
也许你的脑中会出现一个问题,特别如果你是Unix用户,为什么非要使用svn merge?为什么不简单的使用操作系统的patch命令来进行相同的工作?例如:
$ svn diff -c 344 http://svn.example.com/repos/calc/trunk > patchfile $ patch -p0 < patchfile Patching file integer.c using Plan A... Hunk #1 succeeded at 147. Hunk #2 succeeded at 164. Hunk #3 succeeded at 241. Hunk #4 succeeded at 249. done
在这种情况下,确实没有区别,但是svn merge有超越patch的特别能力,使用patch对文件格式有一定的限制,它只能针对文件内容,没有方法表现目录树的修改,例如添加、删除或是改名。如果Sally的修改包括增加一个新的目录,svn diff不会注意到这些,svn diff只会输出有限的补丁格式,所以有些问题无法表达。 但是svn merge命令会通过直接作用你的工作拷贝来表示目录树的结构和属性变化。
一个警告:为什么svn diff和svn merge在概念上是很接近,但语法上有许多不同,一定阅读第 9 章 Subversion 完全参考来查看其细节或者使用svn help查看帮助。举个例子,svn merge需要一个工作拷贝作为目标,就是一个地方来施展目录树修改,如果一个目标都没有指定,它会假定你要做以下某个普通的操作:
你希望合并目录修改到工作拷贝的当前目录。
你希望合并修改到你的当前工作目录的相同文件名的文件。
如果你合并一个目录而没有指定特定的目标,svn merge假定第一种情况,在你的当前目录应用修改。如果你合并一个文件,而这个文件(或是一个有相同的名字文件)在你的当前工作目录存在,svn merge假定第二种情况,你想对这个同名文件使用合并。
如果你希望修改应用到别的目录,你需要说出来。举个例子,你在工作拷贝的父目录,你需要指定目标目录:
$ svn merge -c 344 http://svn.example.com/repos/calc/trunk my-calc-branch U my-calc-branch/integer.c
合并背后的关键概念
你已经看到了svn merge命令的例子,你将会看到更多,如果你对合并是如何工作的感到迷惑,这并不奇怪,很多人和你一样。许多新用户(特别是对版本控制很陌生的用户)会对这个命令的正确语法感到不知所措,不知道怎样和什么时候使用这个特性,不要害怕,这个命令实际上比你想象的简单!有一个简单的技巧来帮助你理解svn merge的行为。
迷惑的主要原因是这个命令的名称,术语“合并”不知什么原因被用来表明分支的组合,或者是其他什么神奇的数据混合,这不是事实,一个更好的名称应该是svn diff-and-apply,这是发生的所有事件:首先两个版本库树比较,然后将区别应用到本地拷贝。
这个命令包括三个参数:
初始的版本树(通常叫做比较的左边),
最终的版本树(通常叫做比较的右边),
一个接收区别的工作拷贝(通常叫做合并的目标)。
一旦这三个参数指定以后,两个目录树将要做比较,比较结果将会作为本地修改应用到目标工作拷贝,当命令结束后,结果同你手工修改或者是使用svn add或svn delete没有什么区别,如果你喜欢这结果,你可以提交,如果不喜欢,你可以使用svn revert恢复修改。
svn merge的语法允许非常灵活的指定三个必要的参数,如下是一些例子:
$ svn merge http://svn.example.com/repos/branch1@150 \ http://svn.example.com/repos/branch2@212 \ my-working-copy $ svn merge -r 100:200 http://svn.example.com/repos/trunk my-working-copy $ svn merge -r 100:200 http://svn.example.com/repos/trunk
第一种语法使用URL@REV的形式直接列出了所有参数,第二种语法可以用来作为比较同一个URL的不同版本的简略写法,最后一种语法表示工作拷贝是可选的,如果省略,默认是当前目录。
合并的最佳实践
手工跟踪合并
合并修改听起来很简单,但是实践起来会是很头痛的事,如果你重复合并两个分支,你也许会合并两次同样的修改。当这种事情发生时,有时候事情会依然正常,当对文件打补丁时,Subversion如果注意到这个文件已经有了相应的修改,而不会作任何操作,但是如果已经应用的修改又被修改了,你会得到冲突。
理想情况下,你的版本控制系统应该会阻止对一个分支做两次改变操作,必须自动的记住那一个分支的修改已经接收了,并且可以显示出来,用来尽可能帮助自动化的合并。
不幸的是,Subversion不是这样一个系统,类似于CVS,Subversion并不记录任何合并操作,[21]当你提交本地修改,版本库并不能判断出你是通过svn merge还是手工修改得到这些文件。
这对你这样的用户意味着什么?这意味着除非Subversion以后发展这个特性,你必须手工的记录这些信息。最佳的方式是使用提交日志信息,像前面的例子提到的,推荐你在日志信息中说明合并的特定版本号(或是版本号的范围),之后,你可以运行svn log来查看你的分支包含哪些修改。这可以帮助你小心的依序运行svn merge命令而不会进行多余的合并。
在下一小节,我们要展示一些这种技巧的例子。
预览合并
首先,一定要记住合并的工作拷贝没有本地更改,并且最近已更新过。如果你的工作拷贝用这样的方法“清理”,你会发现一些头痛的事情。
因为合并只是导致本地修改,它不是一个高风险的操作,如果你在第一次操作错误,你可以运行svn revert来再试一次。
有时候你的工作拷贝很可能已经改变了,合并会针对存在的那一个文件,这时运行svn revert不会恢复你在本地作的修改,两部分的修改无法识别出来。
在这个情况下,人们很乐意能够在合并之前预测一下,一个简单的方法是使用运行svn merge同样的参数运行svn diff,另一种方式是传递--dry-run
选项给merge命令来预览:
$ svn merge --dry-run -c 344 http://svn.example.com/repos/calc/trunk U integer.c $ svn status # nothing printed, working copy is still unchanged.
--dry-run
选项实际上并不修改本地拷贝,它只是显示实际合并时的状态信息,对于得到潜在合并的“整体”预览,这个命令很有用,因为svn diff包括太多细节。
合并冲突
就像svn update命令,svn merge会把修改应用到工作拷贝,因此它也会造成冲突,因为svn merge造成的冲突有时候会有些不同,本小节会解释这些区别。
作为开始,我们假定本地没有修改,当你svn update到一个特定修订版本时,修改会“干净的”应用到工作拷贝,服务器产生比较两树的增量数据:一个工作拷贝和你关注的版本树的虚拟快照,因为比较的左边同你拥有的完全相同,增量数据确保你把工作拷贝转化到右边的树。
但是svn merge没有这样的保证,会导致很多的混乱:用户可以询问服务器比较任何两个树,即使一个与工作拷贝毫不相关的!这意味着有潜在的人为错误,用户有时候会比较两个错误的树,创建的增量数据不会干净的应用,svn merge会尽力应用更多的增量数据,但是有一些部分也许会难以完成,就像Unix下patch命令有时候会报告“failed hunks”错误,svn merge会报告“skipped targets”:
$ svn merge -r 1288:1351 http://svn.example.com/repos/branch U foo.c U bar.c Skipped missing target: 'baz.c' U glub.c C glorb.h $
在前一个例子中,baz.c
也许会存在于比较的两个分支快照里,但工作拷贝里不存在,比较的增量数据要应用到这个文件,这种情况下会发生什么?“skipped”信息意味着用户可能是在比较错误的两棵树,这是经典的用户错误,当发生这种情况,可以使用迭代恢复(svn revert --recursive)合并所作的修改,删除恢复后留下的所有未版本化的文件和目录,并且使用另外的参数运行svn merge。
也应当注意前一个例子显示glorb.h
发生了冲突,我们已经规定本地拷贝没有修改:冲突怎么会发生呢?因为用户可以使用svn merge将过去的任何变化应用到当前工作拷贝,变化包含的文本修改也许并不能干净的应用到工作拷贝文件,即使这些文件没有本地修改。
另一个svn update和svn merge的小区别是冲突产生的文件的名字不同,在“解决冲突(合并别人的修改)”一节,我们看到过更新产生的文件名字为filename.mine
、filename.rOLDREV
和filename.rNEWREV
,当svn merge产生冲突时,它产生的三个文件分别为 filename.working
、filename.left
和filename.right
。在这种情况下,术语“left”和“right”表示了两棵树比较时的两边,在两种情况下,不同的名字会帮助你区分冲突是因为更新造成的还是合并造成的。
关注还是忽视祖先
当与Subversion开发者交谈时你一定会听到提及术语祖先,这个词是用来描述两个对象的关系:如果他们互相关联,一个对象就是另一个的祖先,或者相反。
举个例子,假设你提交版本100,包括对foo.c
的修改,则foo.c@99是foo.c@100的一个“祖先”,另一方面,假设你在版本101删除这个文件,而在102版本提交一个同名的文件,在这个情况下,foo.c@99
与foo.c@102
看起来是关联的(有同样的路径),但是事实上他们是完全不同的对象,它们并不共享同一个历史或者说“祖先”。
指出svn diff和svn merge区别的重要性在于,前一个命令忽略祖先,如果你询问svn diff来比较文件foo.c
的版本99和102,你会看到行为基础的区别,diff
命令只是盲目的比较两条路径,但是如果你使用svn merge是比较同样的两个对象,它会注意到他们是不关联的,而且首先尝试删除旧文件,然后添加新文件,输出会是一个删除紧接着一个增加:
D foo.c A foo.c
大多数合并包括比较包括祖先关联的两条树,因此svn merge这样运作,然而,你也许会希望merge
命令能够比较两个不相关的目录树,举个例子,你有两个目录树分别代表了供应方软件项目的不同版本(见“供方分支”一节),如果你使用svn merge进行比较,你会看到第一个目录树被删除,而第二个树添加上!在这个情况下,你仅仅是希望svn merge以路径为基础比较两棵树,而忽略文件和目录的不相关性,当为合并命令添加--ignore-ancestry
选项时,就会像svn diff一样工作。(相反,--notice-ancestry
会导致svn diff像merge
命令一样工作。)
合并和移动
一个普遍的愿望是重构源程序,特别是Java软件项目。在改名中文件和目录变乱,通常导致每个项目成员的极大破坏。听起来好像应该使用分支,不是吗?只是创建分支,变乱事情,然后合并回主干,不对吗?
唉,这个场景下这样并不正确,可以看作Subversion当前的弱点,这个问题是因为Subversion的update还不是足够的强壮,特别是针对拷贝和移动操作。
当你使用svn copy复制文件时,版本库会记住新文件的出处,但是它不能将这个信息传递给使用svn update或svn merge的客户端,不是告诉客户端“ 将文件拷贝到新的位置”,而是传递一整个新文件。这样会导致问题,特别是因为这件事也发生在改名的文件。 一个鲜为人知的事实是Subversion缺乏真正的重命名—svn move命令只是一个svn copy和svn delete的组合。
例如,假定我们在一个私有分支工作,你将integer.c
改名为whole.c
,你这是在分支上创建了原来文件的一个拷贝,并且删除了原来的文件。同时,回到trunk
,Sally提交了一些integer.c
的修改,所以你需要将分支合并到主干:
$ cd calc/trunk $ svn merge -r 341:405 http://svn.example.com/repos/calc/branches/my-calc-branch D integer.c A whole.c
第一眼看起来不是很差,但是很可能这不是你和Sally希望的,合并操作已经删除了最新版本的integer.c
(包含了Sally最新的修改),而且盲目的添加了你的whole.c
文件—是旧版本的integer.c
复制品。最终的结果是将你的“rename”合并到分支,并且从最新修订版本删除了Sally最近的修改。
这不是真的数据丢失;Sally的修改还在版本库的历史中,但是。在Subversion改进之前,最好小心对分支进行合并和改名。
[21] 然而,写这些的时候,这些特性正在实现中!