本手册是为描述 Subversion 1.1 而编写的。如果您正在运行更新版本的 Subversion,我们强烈建议您访问 https://svnbooks.subversion.org.cn/ 并查阅适合您 Subversion 版本的本手册。

在分支之间复制变更

现在,您和 Sally 在项目的平行分支上工作:您在私人分支上工作,而 Sally 在 主干 或主要开发线上工作。

对于拥有大量贡献者的项目,大多数人拥有主干的工作副本是很常见的。每当有人需要进行可能中断主干的长期变更时,标准程序是创建一个私人分支,并在那里提交变更,直到所有工作完成。

所以,好消息是您和 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副本已修补。它现在包含 Sally 的变更,该变更已从主干“复制”到您私人分支的工作副本中,现在作为本地修改存在。此时,您需要查看本地修改并确保它能正确工作。

在另一种情况下,事情可能并不顺利,并且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 的行为方式。

混淆的主要来源是命令的 名称。“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选项传递给合并命令

$ 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.

The--dry-runoption 不会真正对工作副本进行任何本地更改。它只显示在实际合并中将打印的状态代码。它有助于在运行 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.mine, filename.rOLDREVfilename.rNEWREV的文件。但是,当 svn merge 产生冲突时,它会创建三个名为filename.working, filename.leftfilename.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 默认使用此行为。但是,你偶尔可能希望合并命令比较两棵不相关的树。例如,你可能已经导入了表示软件项目的不同供应商版本的两个源代码树(参见 名为“供应商分支”的部分)。如果你要求 svn merge 比较这两棵树,你将看到整个第一棵树被删除,然后添加整个第二棵树!

在这些情况下,你希望 svn merge 只进行基于路径的比较,忽略文件和目录之间的任何关系。在你的合并命令中添加--ignore-ancestry选项,它将像 svn diff 一样运行。(反之,--notice-ancestry选项将导致 svn diff 像合并命令一样运行。)



[8] 将来,Subversion 项目计划使用(或发明)一种扩展的补丁格式来描述树更改。

TortoiseSVN 官方中文版 1.14.7 发布