本文档描述了 Subversion 1.2。如果您运行的是更新版本的 Subversion,我们强烈建议您访问 https://svnbooks.subversion.org.cn/ 并查阅适合您 Subversion 版本的书籍。

分支间复制变更

现在您和莎莉正在项目并行分支上工作:您正在私人分支上工作,莎莉正在 主干 或主要开发线上工作。

对于拥有大量贡献者的项目来说,大多数人拥有主干的工作副本是很常见的。每当有人需要进行可能中断主干的长期更改时,标准做法是创建一个私人分支,并在所有工作完成之前将更改提交到该分支。

所以,好消息是您和莎莉没有互相干扰。坏消息是,很容易出现 过分分离的情况。请记住,“躲进洞里”策略的缺点之一是,当您完成分支工作时,可能很难将您的更改合并回主干,而不会出现大量的冲突。

相反,您和莎莉可以继续在工作中共享更改。由您决定哪些更改值得共享;Subversion 使您能够有选择地将更改“复制”到不同分支之间。当您完全完成分支工作时,您可以将整个分支更改集复制回主干。

复制特定变更

在上一节中,我们提到您和莎莉都在不同分支上对 integer.c 进行了更改。如果您查看莎莉对修订版 344 的日志消息,您会发现她修复了一些拼写错误。毫无疑问,您对同一文件的副本仍然存在相同的拼写错误。您将来对该文件的更改可能会影响到存在拼写错误的相同区域,因此当您将来合并分支时,可能会出现冲突。因此,最好现在接收莎莉的更改,您开始在相同位置进行大量工作之前。

现在是使用 svn merge 命令的时候了。事实证明,这个命令与 svn diff 命令(您在第 3 章中读过)非常相似。这两个命令都能够比较存储库中的任何两个对象并描述差异。例如,您可以要求 svn diff 向您展示莎莉在修订版 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 副本已被修补。它现在包含了莎莉的更改——更改已从主干“复制”到您私人分支的工作副本,现在作为本地修改存在。此时,由您来审查本地修改并确保它能正常工作。

在另一种情况下,事情可能进展不顺利,integer.c 可能进入了冲突状态。您可能需要使用标准程序(参见第 3 章)解决冲突,或者如果您决定合并是一个糟糕的主意,则只需放弃并 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 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 比较 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 命令一样。)



[9] 在未来,Subversion 项目计划使用(或发明)扩展的补丁格式来描述树结构和属性的更改。

TortoiseSVN 官方中文版 1.14.7 发布