在分支之间复制修改

现在你和Sally分别工作在项目的两个并行的分支上:你在一个私有的分支上工作,Sally在trunk,或者 说开发主线上工作。

由于项目有很多的开发者,通常大部分人有主线的一个工作副本。当某人要做需时较长的修改而可能会破坏主线时, 标准的过程是创建一个私有的分支来提交修改,直到工作全部完成。

这样做,好消息是你和Sally不会互相干扰,坏消息是很容易偏移的远。 要记得“闭门造车”策略的问题是,在你完成你分支后,要把你的修改合并到主干里 而没有大量的冲突,几乎是不可能的。

更好的办法是,在工作时,你和Sally可以继续共享修改。决定哪些修改需要共享是你们的职责。 Subversion赋予你有选择的在分支之间“复制”修改的能力。而且当你全部完成你的工作后, 你能把分支中的全部修改复制回主干去。

复制指定的修改

在前面的章节中,我们提到你和Sally都在各自的分支中对integer.c作了修改。如果你 察看在修改版344写的日志消息,你可以看到她修正了一些拼写错误。无疑,你的同一文件的拷贝仍然有同样的 拼写错误。可能你将来的修改影响这些有拼写错误的地方,因此可能会在以后某天合并你的分支时导致冲突。那么, 更好的办法是,在你在相同的地方做太多的工作之前,现在就接收Sally的修改,。

现在是使用svn merge命令的时候了。这个命令是svn diff命令 (你在第三章读到过)的很近的表兄弟。这两个命令都可用来比较资料库中任何两个对象。例如,你可以 让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的副本被修补了。 现在它包含了Sally的修改——修改已经从主干“复制”到你的私有分支了,现在就如你的 本地修改一样。这时,你需要检查本地的修改来确认它是正确的。

在另一个场景中,可能事情没那么顺利,integer.c会进入到冲突状态。 你可能需要用标准的过程(参见第三章)来解决冲突,或者如果你认定合并完全是个坏主意, 那么可以简单的放弃它,并用svn revert恢复本地的修改。

假设你已经检查完了合并后的修改,你可以像平常一样用svn commit提交修改。 这时,修改已经被合并到你的资料库分支里。在版本控制术语中,在分支之间复制修改通常被称为移植(porting) 修改。

当你提交本地修改时,应该确认你的日志消息记录了你从一个分支移植了某个特定的修改到另一个分支, 例如:

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

如你在下一节将看到的,这是一个应该遵循的、重要的“最佳实践”。

一些警告:虽然svn diffsvn merge在概念上很相似,但是很多情况下它们有不同的语法。 一定要阅读第九章以得到更多的细节,或者使用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

合并的最佳实践

手工跟踪合并

合并修改似乎很简单,但在实践中可能会出很令人头疼。问题在于,如果你不断地把修改从一个分支合并到另一个, 你可能不小心把同一个修改合并了两次。如果发生了这种事,有时事情进行良好。 当修补一个文件时,Subversion通常会注意到文件已经被修改了,而不会做任何事。但如果已存在的修改又已经被改动了, 你会得到一个冲突。

理想情况下,你的版本控制系统应该防止把修改在一个分支上应用两次。它应该自动记住一个分支已经接收到了 哪些修改,并且能给你把它们列出来。它应该用这些信息尽可能使合并自动化。

不幸的是,Subversion不是这种系统。就像CVS,Subversion 没有记录任何关于合并操作的信息。 当你提交本地修改时,资料库不知道那些修改来自于执行svn merge的结果还是只是对文件的手工修改。

这对你,一个用户,有什么意义?这意味着除非某天Subversion有了这个特性,否则你将不得不自己跟踪合并信息。 这么做的最好的地方就是提交时的日志消息。像在前面的例子中演示的,我们建议在你的日志消息中提及那个合并到 你的分支中的特定的修改版号(或修订版范围)。以后你能用svn log来检查你的分支已经包含了哪些修改。 这使你能仔细地构建一系列svn merge命令而不会和先前移植的修改重复。

在下一节,我们将展示一些应用这个技术的例子。

预览合并

因为合并只影响本地修改,所以通常不是高风险的操作。如果你第一次合并错了,简单的用svn revert 撤销修改后再试一次。

然而,也可能你的工作副本已经有了本地修改。合并带来的修改和你先前的修改混合了,执行svn revert 不再可行。这两套修改不可能分离开。

在这种情况下,能在发生前预知或检查合并会让人们感到放心。一个简单的办法是执行svn diff,并带着 和你计划传递给svn merge的相同的参数,像我们已经在合并的第一个例子里展示的那样。另一种预览的办法 是传递--dry-run选项给合并命令:

$ 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 有时会报怨“失败的块”,svn merge会抱怨“跳过了某些目标”:。

$ 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同时存在于两个被比较的分支快照中,得到的增量要改变这个文件 的内容,但是这个文件在工作副本中不存在。不管在什么情况下,“跳过”消息意味着用户很可能比较了两个错误的文件树; 它是人为错误的典型标记。当这发生时,可以很容易的递归的 撤销合并(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@99foo.c@100的一个祖先。 从另一方面说,假设你在修订版101提交了对foo.c的删除,并在修订版102添加了一个同名的文件。这种情况下,foo.c@99foo.c@102看起来好像有联系(它们有相同的路径),但事实上它们在资料库中是完全不同的对象。它们没有共同的历史或“血统”。

指出这一点是为了说明svn diffsvn merge的一个重要的不同。前一个命令忽略血统,而后一个对血统很敏感。 例如,如果你用svn diff来比较修订版99和102中的foo.c,你会看到基于行的diff输出;diff 命令只是盲目的比较这两个路径指定的文件。 但如果你用svn merge来比较同样这两个对象,它会注意到它们互不相干,并先尝试删除旧文件然后添加新文件。你将会看到D foo.c 跟着一个A foo.c

大部分合并涉及到比较有血统关系的两个树,因而svn merge。但偶尔你也可能想用合并命令比较两个无关的树。例如,你可能已经导入了两个源代码树, 它们分别代表同一个软件项目的不同的发行版本(参见???)。如果你用 svn merge 来比较这两个树,你将看到的是第一个树被整个删掉,跟着添加了整个第二个树。

在这些情形下,你会想用svn merge来只做基于路径的比较,而忽略文件和目录间的任何联系。你可以在合并命令中加上--ignore-ancestry选项,它将像svn diff那样运转。(对应的,--notice-ancestry选项会导致svn diff 像merge那样运行)。



[7] Subversion项目计划在将来使用(或发明)一个扩展的补丁格式来描述树的修改。