在分支间拷贝修改

现在你与Sally在同一个项目的并行分支上工作:你在私有分支上,而Sally在主干(trunk)或者叫做开发主线上。

由于有众多的人参与项目,大多数人拥有主干拷贝是很正常的,任何人如果进行一个长周期的修改会使得主干陷入混乱,所以通常的做法是建立一个私有分支,提交修改到自己的分支,直到这阶段工作结束。

所以,好消息就是你和Sally不会互相打扰,坏消息是有时候分离会远。记住“闭门造车”策略的问题,当你完成你的分支后,可能因为太多冲突,已经无法轻易合并你的分支和主干的修改。

相反,在你工作的时候你和Sally仍然可以继续分享修改,这依赖于你决定什么值得分享,Subversion给你在分支间选择性“拷贝”修改的能力,当你完成了分支上的所有工作,所有的分支修改可以被拷贝回到主干。

拷贝特定的修改

在上一章节,我们提到你和Sally对integer.c在不同的分支上做过修改,如果你看了Sally的344版本的日志信息,你会知道她修正了一些拼写错误,毋庸置疑,你的拷贝的文件也一定存在这些拼写错误,所以你以后的对这个文件修改也会保留这些拼写错误,所以你会在将来合并时得到许多冲突。最好是现在接收Sally的修改,而不是作了许多工作之后才来做。

是时间使用svn merge命令,这个命令的结果非常类似svn diff命令(在第3章的内容),两个命令都可以比较版本库中的任何两个对象并且描述其区别,举个例子,你可以使用svn diff来查看Sally在版本344作的修改:

$ svn diff -r 343: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 -r 343:344 http://svn.example.com/repos/calc/trunk
U  integer.c

$ svn status
M  integer.c

svn merge的输出告诉你的integer.c文件已经作了补丁(patched),现在已经保留了Sally修改—修改从主干“拷贝”到你的私有分支的工作拷贝,现在作为一个本地修改,在这种情况下,要靠你审查本地的修改来确定它们工作正常。

在另一种情境下,事情并不会运行得这样正常,也许integer.c也许会进入冲突状态,你必须使用标准过程(见第三章)来解决这种状态,或者你认为合并是一个错误的决定,你只需要运行svn revert放弃。

但是当你审查过你的合并结果后,你可以使用svn commit提交修改,在那一刻,修改已经合并到你的分支上了,在版本控制术语中,这种在分支之间拷贝修改的行为叫做搬运修改。

当你提交你的修改时,确定你的日志信息中说明你是从某一版本搬运了修改,举个例子:

$ svn commit -m "integer.c: ported r344 (spelling fixes) from trunk."
Sending        integer.c
Transmitting file data .
Committed revision 360.

你将会在下一节看到,这是一条非常重要的“最佳实践”。

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

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

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

如果你合并一个目录而没有指定特定的目标,svn merge假定第一种情况,在你的当前目录应用修改。如果你合并一个文件,而这个文件(或是一个有相同的名字文件)在你的当前工作目录存在,svn merge假定第二种情况,你想对这个同名文件使用合并。

如果你希望修改应用到别的目录,你需要说出来。举个例子,你在工作拷贝的父目录,你需要指定目标目录:

$ svn merge -r 343: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并不记录任何合并操作,当你提交本地修改,版本库并不能判断出你是通过svn merge还是手工修改得到这些文件。

这对你这样的用户意味着什么?这意味着除非Subversion以后发展这个特性,你必须手工的记录这些信息。最佳的方式是使用提交日志信息,像前面的例子提到的,推荐你在日志信息中说明合并的特定版本号(或是版本号的范围),之后,你可以运行svn log来查看你的分支包含哪些修改。这可以帮助你小心的依序运行svn merge命令而不会进行多余的合并。

在下一小节,我们要展示一些这种技巧的例子。

预览合并

因为合并只是导致本地修改,它不是一个高风险的操作,如果你在第一次操作错误,你可以运行svn revert来再试一次。

有时候你的工作拷贝很可能已经改变了,合并会针对存在的那一个文件,这时运行svn revert不会恢复你在本地作的修改,两部分的修改无法识别出来。

在这个情况下,人们很乐意能够在合并之前预测一下,一个简单的方法是使用运行svn merge同样的参数运行svn diff,另一种方式是传递--dry-run选项给merge命令:

$ svn merge --dry-run -r 343: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,你会看到行为基础的区别,区别命令只是盲目的比较两条路径,但是如果你使用svn merge是比较同样的两个对象,它会注意到他们是不关联的,而且首先尝试删除旧文件,然后添加新文件,你会看到A foo.c后面紧跟D foo.c

大多数合并包括比较包括祖先关联的两条树,因此svn merge这样运作,然而,你也许会希望合并命令能够比较两个不相关的目录树,举个例子,你有两个目录树分别代表了卖主软件项目的不同版本(见“卖主分支”一节),如果你使用svn merge进行比较,你会看到第一个目录树被删除,而第二个树添加上!

在这个情况下,你只是希望svn merge能够做一个以路径为基础的比较,忽略所有文件和目录的关系,增加--ignore-ancestry选项会导致命令象svn diff一样。(相应的,--notice-ancestry选项会使svn diff象合并命令一样行事。)



[8] 在将来,Subversion项目将会计划(或者发明)一种扩展补丁格式来描述目录树改变。