4.在分支间复制修改

Posted on Posted in 5.分支与合并

在分支间复制修改

现在你与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 diffsvn merge在概念上是很接近,但语法上有许多不同,一定阅读第 9 章 Subversion 完全参考来查看其细节或者使用svn help查看帮助。举个例子,svn merge需要一个工作拷贝作为目标,就是一个地方来施展目录树修改,如果一个目标都没有指定,它会假定你要做以下某个普通的操作:

  1. 你希望合并目录修改到工作拷贝的当前目录。

  2. 你希望合并修改到你的当前工作目录的相同文件名的文件。

如果你合并一个目录而没有指定特定的目标,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,这是发生的所有事件:首先两个版本库树比较,然后将区别应用到本地拷贝。

这个命令包括三个参数:

  1. 初始的版本树(通常叫做比较的左边),

  2. 最终的版本树(通常叫做比较的右边),

  3. 一个接收区别的工作拷贝(通常叫做合并的目标)。

一旦这三个参数指定以后,两个目录树将要做比较,比较结果将会作为本地修改应用到目标工作拷贝,当命令结束后,结果同你手工修改或者是使用svn addsvn 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 updatesvn merge的小区别是冲突产生的文件的名字不同,在“解决冲突(合并别人的修改)”一节,我们看到过更新产生的文件名字为filename.minefilename.rOLDREVfilename.rNEWREV,当svn merge产生冲突时,它产生的三个文件分别为 filename.workingfilename.leftfilename.right。在这种情况下,术语“left”和“right”表示了两棵树比较时的两边,在两种情况下,不同的名字会帮助你区分冲突是因为更新造成的还是合并造成的。

关注还是忽视祖先

当与Subversion开发者交谈时你一定会听到提及术语祖先,这个词是用来描述两个对象的关系:如果他们互相关联,一个对象就是另一个的祖先,或者相反。

举个例子,假设你提交版本100,包括对foo.c的修改,则foo.c@99是foo.c@100的一个“祖先”,另一方面,假设你在版本101删除这个文件,而在102版本提交一个同名的文件,在这个情况下,foo.c@99foo.c@102看起来是关联的(有同样的路径),但是事实上他们是完全不同的对象,它们并不共享同一个历史或者说“祖先”。

指出svn diffsvn 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 diffmerge命令一样工作。)

合并和移动

一个普遍的愿望是重构源程序,特别是Java软件项目。在改名中文件和目录变乱,通常导致每个项目成员的极大破坏。听起来好像应该使用分支,不是吗?只是创建分支,变乱事情,然后合并回主干,不对吗?

唉,这个场景下这样并不正确,可以看作Subversion当前的弱点,这个问题是因为Subversion的update还不是足够的强壮,特别是针对拷贝和移动操作。

当你使用svn copy复制文件时,版本库会记住新文件的出处,但是它不能将这个信息传递给使用svn updatesvn merge的客户端,不是告诉客户端“ 将文件拷贝到新的位置”,而是传递一整个新文件。这样会导致问题,特别是因为这件事也发生在改名的文件。 一个鲜为人知的事实是Subversion缺乏真正的重命名—svn move命令只是一个svn copysvn 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] 然而,写这些的时候,这些特性正在实现中!