本文档旨在描述 Apache™ Subversion® 的 1.7.x 系列。如果您运行的是其他版本的 Subversion,强烈建议您访问 https://svnbook.subversion.org.cn/ 并查阅适合您 Subversion 版本的文档。
自动化的魔力到此结束。迟早,一旦您掌握了分支和合并的技巧,您将不得不要求 Subversion 将 特定 的更改从一个地方合并到另一个地方。为此,您需要开始向 svn merge 传递更复杂的参数。下一节将描述该命令的完整扩展语法,并讨论一些需要使用该语法的常见场景。
就像术语 “变更集” 通常用于版本控制系统一样,术语 挑选 也是如此。这个词指的是从一个分支中选择 一个 特定的变更集并将其复制到另一个分支。挑选也可以指将一个分支中的特定一组(不一定连续!)变更集复制到另一个分支。这与更典型的合并场景形成对比,在更典型的合并场景中,“下一个” 连续的修订范围会自动复制。
为什么人们想要复制单个更改?这种情况比您想象的要多。例如,让我们回到过去,想象一下您还没有将您的私人功能分支合并回主干。在饮水机旁,您得知 Sally 在主干上对 integer.c
做了一个有趣的更改。查看主干提交的历史记录,您会发现她在修订版 355 中修复了一个严重错误,该错误直接影响到您正在开发的功能。您可能还没有准备好将所有主干更改合并到您的分支,但您肯定需要这个特定的错误修复才能继续您的工作。
$ svn diff -c 355 ^/calc/trunk Index: integer.c =================================================================== --- integer.c (revision 354) +++ integer.c (revision 355) @@ -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, "CP/MM"); + 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;
就像您在前面的示例中使用 svn diff 检查修订版 355 一样,您可以将相同的选项传递给 svn merge
$ svn merge -c 355 ^/calc/trunk --- Merging r355 into '.': U integer.c --- Recording mergeinfo for merge of r355 into '.': U . $ svn status M integer.c
您现在可以进行通常的测试程序,然后将此更改提交到您的分支。提交后,Subversion 将 r355 标记为已合并到分支,以便将来同步您的分支与主干的 “魔法” 合并知道跳过 r355。(将相同的更改合并到同一个分支几乎总是会导致冲突!)
$ cd my-calc-branch $ svn propget svn:mergeinfo . /trunk:341-349,355 # Notice that r355 isn't listed as "eligible" to merge, because # it's already been merged. $ svn mergeinfo ^/calc/trunk --show-revs eligible r350 r351 r352 r353 r354 r356 r357 r358 r359 r360 $ svn merge ^/calc/trunk --- Merging r350 through r354 into '.': U . U integer.c U Makefile --- Merging r356 through r360 into '.': U . U integer.c U button.c --- Recording mergeinfo for merge of r350 through r360 into '.': U .
这种从一个分支复制(或 回溯)错误修复到另一个分支的用例可能是挑选更改最流行的原因;它经常出现,例如,当一个团队维护软件的 “发布分支” 时。(我们在 名为“发布分支”的部分 中讨论了这种模式。)
![]() |
警告 |
---|---|
您是否注意到,在最后一个示例中,合并调用合并了两个不同的范围?svn merge 命令将两个独立的补丁应用到您的工作副本,以跳过变更集 355,您的分支已经包含了该变更集。这本身并没有什么错,只是它有可能使冲突解决变得更加复杂。如果第一个更改范围创建了冲突,您必须以交互方式解决它们,以便合并过程继续并应用第二个更改范围。如果您推迟了第一波更改的冲突,整个合并命令将以错误消息退出。 [32] |
警告:虽然 svn diff 和 svn merge 在概念上非常相似,但在许多情况下它们的语法有所不同。请务必阅读 第 9 章,Subversion 完整参考 中的有关内容以了解详细信息,或询问 svn help。例如,svn merge 需要工作副本路径作为目标,即它应该应用生成的补丁的位置。如果未指定目标,它将假定您正在尝试执行以下常见操作之一
您想将目录更改合并到当前工作目录中。
您想将特定文件中的更改合并到当前工作目录中存在的同名文件中。
如果您正在合并目录并且没有指定目标路径,svn merge 将假定第一种情况并尝试将更改应用到当前目录。如果您正在合并文件,并且该文件(或同名文件)存在于当前工作目录中,svn merge 将假定第二种情况并尝试将更改应用到本地同名文件。
您现在已经看到了一些 svn merge 命令的示例,您将看到更多示例。如果您对合并的具体工作方式感到困惑,您并不孤单。许多用户(尤其是那些刚接触版本控制的用户)最初对该命令的正确语法以及何时以及如何使用该功能感到困惑。但不要害怕,这个命令实际上比您想象的要简单得多!有一个非常简单的技巧可以帮助您理解 svn merge 的行为方式。
混淆的主要来源是该命令的名称。术语“合并” 某种程度上表示分支被合并在一起,或者正在进行某种神秘的数据混合。事实并非如此。该命令的更好名称可能是 svn diff-and-apply,因为这就是发生的事情:比较两个存储库树,并将差异应用到工作副本。
如果您使用 svn merge 在分支之间进行基本的更改复制,它通常会自动执行正确操作。例如,以下命令
$ svn merge ^/calc/branches/some-branch
将尝试将 some-branch
上所做的任何更改复制到您的当前工作目录中,该目录可能是一个与该分支共享一些历史连接的工作副本。该命令足够智能,可以只复制您的工作副本中尚未存在的更改。如果您每周重复此命令一次,它只会复制自上次合并以来发生的 “最新” 分支更改。
如果您选择通过提供要复制的特定修订版本范围来充分利用 svn merge 命令,该命令将接受三个主要参数
初始存储库树(通常称为比较的 左侧)
最终存储库树(通常称为比较的 右侧)
一个工作副本,用于接受差异作为本地更改(通常称为合并的 目标)
指定完这三个参数后,将比较这两棵树,并将差异应用于目标工作副本作为本地修改。命令完成后,结果与您手动编辑文件或运行各种 svn add 或 svn 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 的两个不同修订版本情况的简写。最后一个语法显示了工作副本参数是可选的;如果省略,它将默认为当前目录。
虽然第一个示例显示了 svn merge 的 “完整” 语法,但请非常小心地使用它;它可能导致合并,这些合并根本不记录任何 svn:mergeinfo
元数据。下一节将对此进行更多讨论。
Subversion 尝试在可能的情况下生成合并元数据,以使 svn merge 的未来调用更智能。但是,仍然存在 svn:mergeinfo
数据未创建或更改的情况。请记住,要对这些情况保持警惕
如果您要求 svn merge 比较两个彼此不相关的 URL,则仍然会生成并应用补丁到您的工作副本,但不会创建任何合并元数据。这两个源之间没有共同的历史记录,而未来的 “智能” 合并依赖于该共同历史记录。
虽然可以运行类似于 svn merge -r 100:200
的命令,但生成的补丁也缺少任何历史合并元数据。在撰写本文时,Subversion 无法在 http://svn.foreignproject.com/repos/trunk
svn:mergeinfo
属性中表示不同的存储库 URL。
--ignore-ancestry
如果将此选项传递给 svn merge,它会导致合并逻辑无脑地生成差异,就像 svn diff 一样,忽略任何历史关系。我们将在本章后面的 名为“注意或忽略祖先”的部分 中讨论这一点。
在本章前面 (名为“撤消更改”的部分),我们讨论了如何使用 svn merge 应用 “反向补丁” 作为回滚更改的一种方式。如果使用此技术来撤消对对象个人历史记录的更改(例如,将 r5 提交到主干,然后立即使用 svn merge . -c -5
回滚 r5),这种合并不会影响记录的 mergeinfo。[33]
就像 svn update 命令一样,svn merge 会将更改应用到你的工作副本。因此,它也能够创建冲突。但是,由 svn merge 生成的冲突有时会有所不同,本节将解释这些差异。
首先,假设你的工作副本没有本地编辑。当你 svn update 到特定修订版时,服务器发送的更改始终会 “干净地” 应用到你的工作副本。服务器通过比较两棵树来生成增量:你的工作副本的虚拟快照,以及你感兴趣的修订版树。因为比较的左侧完全等于你已经拥有的内容,所以增量保证可以正确地将你的工作副本转换为右侧的树。
但是 svn merge 没有这样的保证,而且可能会更加混乱:高级用户可以要求服务器比较 任何 两个树,即使它们与工作副本无关!这意味着存在很大的潜在人为错误。用户有时会比较错误的两个树,从而创建无法干净地应用的增量。 svn merge 子命令会尽力应用尽可能多的增量,但有些部分可能无法应用。合并错误增量的常见迹象是意外的树冲突。
$ svn merge -r 1288:1351 http://svn.example.com/myrepos/branch --- Merging r1289 through r1351 into '.': C bar.c C foo.c C docs --- Recording mergeinfo for merge of r1289 through r1351 into '.': U . Summary of conflicts: Tree conflicts: 3 $ svn st ! C bar.c > local missing, incoming edit upon merge ! C foo.c > local missing, incoming edit upon merge ! C docs > local delete, incoming edit upon merge
在前面的示例中,可能是 bar.c
、foo.c
和 docs
都存在于正在比较的分支的两个快照中。生成的增量想要更改工作副本中相应路径的内容,但这些路径不存在于工作副本中。无论如何,树冲突的普遍存在很可能意味着用户比较了错误的两个树;这是用户错误的典型迹象。发生这种情况时,可以轻松地递归地还原合并创建的所有更改(svn revert . --recursive
),删除还原后留下的任何未版本化的文件或目录,并使用正确的参数重新运行 svn merge。
还要记住,合并到没有本地编辑的工作副本中仍然可能产生文本冲突。
$ svn merge -c 1701 http://svn.example.com/myrepos/branchX --accept postpone --- Merging r1701 into '.': C glub.c C sputter.c --- Recording mergeinfo for merge of r1701 into '.': U . Summary of conflicts: Text conflicts: 2 C:\SVN\src-branch-1.7.x>svn st M . ? glub.c.merge-left.r1700 ? glub.c.merge-right.r1701 C glub.c ? glub.c.working ? sputter.c.merge-left.r1700 ? sputter.c.merge-right.r1701 C sputter.c ? sputter.c.working Summary of conflicts: Text conflicts: 2
冲突怎么可能发生?同样,因为用户可以请求 svn merge 将任何旧增量定义并应用到工作副本,所以该增量可能包含无法干净地应用于工作文件的文本更改,即使该文件没有本地修改。
在 svn update 和 svn merge 之间,另一个细微的差别在于发生冲突时创建的完整文本文件的名称。在 名为“解决任何冲突”的部分 中,我们看到更新会生成名为 filename.mine
、filename.rOLDREV
和 filename.rNEWREV
的文件。但是,当 svn merge 产生冲突时,它会创建三个名为 filename.working
、filename.merge-left.rOLDREV
和 filename.merge-right.rNEWREV
的文件。在这种情况下,术语 “merge-left” 和 “merge-right” 描述了文件来自双树比较的哪一侧,“rOLDREV” 描述了左侧的修订版,而 “rNEWREV” 描述了右侧的修订版。无论如何,这些不同的名称可以帮助您区分由于更新而发生的冲突和由于合并而发生的冲突。
有时您可能不希望自动合并特定的变更集。例如,您的团队的策略可能是将新的开发工作放在 /trunk
上,但在将更改回传到用于向公众发布的稳定分支时更加保守。在一种极端情况下,您可以手动从主干挑选单个变更集到分支——只有足够稳定的更改才能通过审查。但是,也许情况并不那么严格;也许大多数时候您只是让 svn merge 自动将大多数更改从主干合并到分支。在这种情况下,您需要一种方法来屏蔽一些特定的更改,也就是说,阻止它们被自动合并。
在 Subversion 1.7 中,阻止变更集的唯一方法是让系统相信该更改已经 合并 了。为此,请使用 --record-only
选项调用 merge 子命令
$ cd my-calc-branch $ svn propget svn:mergeinfo . /trunk:1680-3305 # Let's make the metadata list r3328 as already merged. $ svn merge -c 3328 --record-only ^/calc/trunk --- Recording mergeinfo for merge of r3328 into '.': U . $ svn status M . $ svn propget svn:mergeinfo . /trunk:1680-3305,3328 $ svn commit -m "Block r3328 from being merged to the branch." …
从 Subversion 1.7 开始,--record-only
合并是可传递的。这意味着除了记录描述被阻止修订版本的 mergeinfo 之外,合并源中的任何 svn:mergeinfo
属性差异也会被应用。例如,假设我们想要阻止将“frazzle”功能从 ^/trunk
合并到我们的 ^/branches/proj-X
分支。我们知道所有 frazzle 工作都是在它自己的分支上完成的,该分支在修订版 1055 中被重新集成到 trunk
中。
$ svn log -v ^/trunk -r 1055 ------------------------------------------------------------------------ r1055 | francesca | 2011-09-22 07:40:06 -0400 (Thu, 22 Sep 2011) | 3 lines Changed paths: M /trunk M /trunk/src/frazzle.c Reintegrate the frazzle-feature-branch to trunk.
因为修订版 1055 是一个重新集成合并,所以我们知道记录了描述合并的 mergeinfo。
$ svn diff ^/trunk -c 1055 --depth empty Index: . =================================================================== --- . (revision 1054) +++ . (revision 1055) Property changes on: . ___________________________________________________________________ Modified: svn:mergeinfo Merged /branches/frazzle-feature-branch:r997-1003
现在,仅仅阻止从 ^/trunk
合并修订版 1055 并不是万无一失的,因为有人可能会直接从 ^/branches/frazzle-feature-branch
合并 r996:1003。幸运的是,Subversion 1.7 中 --record-only
合并的可传递性阻止了这种情况;--record-only
合并应用了修订版 1055 中的 svn:mergeinfo
差异,从而阻止了直接从 frazzle 分支合并 以及 与 Subversion 1.7 之前一直以来的方式一样,它阻止了直接从 ^/trunk
合并修订版 1055。
$ cd branches/proj-X $ svn merge ^/trunk . -c 1055 --record-only --- Merging r1055 into '.': G . --- Recording mergeinfo for merge of r1055 into '.': G . $ svn diff --depth empty . Index: . =================================================================== --- . (revision 1070) +++ . (working copy) Property changes on: . ___________________________________________________________________ Modified: svn:mergeinfo Merged /trunk:r1055 Merged /branches/frazzle-feature-branch:r997-1003
使用 --record-only
阻止更改是可行的,但它也有一些危险。主要问题是我们没有明确区分““我已经有了这个更改””和““我没有这个更改,但目前不想拥有它。””这两个概念。我们实际上是在欺骗系统,让它认为更改之前已经合并了。这将责任推给了你——用户——去记住更改实际上并没有合并,只是你不想拥有它。没有办法向 Subversion 请求““被阻止的变更列表。””如果你想跟踪它们(以便将来可以解除阻止),你需要将它们记录在某个文本文件中,或者可能是在一个自定义属性中。
在重新整合分支后,除了销毁并重新创建分支,还有另一种方法。要理解为什么这种方法有效,你需要了解为什么分支在重新整合后最初不适合继续使用。
假设你在修订版 A 中创建了你的分支。在你的分支上工作时,你创建了一个或多个修订版,这些修订版对分支进行了更改。在将你的分支重新整合回主干之前,你从主干到你的分支进行了最后一次合并,并将这次合并的结果提交为修订版 B。
当将你的分支重新整合到主干时,你创建了一个新的修订版 X,它更改了主干。在修订版 X 中对主干进行的更改在语义上等同于你在修订版 A 和 B 之间对你的分支进行的更改。
如果你现在尝试将主干上的未完成更改合并到你的分支,Subversion 将认为修订版 X 中的更改可以合并到分支中。但是,由于你的分支已经包含了修订版 X 中的所有更改,合并这些更改会导致虚假冲突!这些冲突通常是树冲突,尤其是在分支或主干在开发期间进行了重命名的情况下。
那么如何解决这个问题呢?我们需要确保 Subversion 不会尝试将修订版 X 合并到分支中。这可以通过使用 --record-only
合并选项来实现,该选项是在 名为“阻止更改”的部分 中引入的。
要执行仅记录合并,请获取在修订版 X 中刚刚重新整合的分支的工作副本,并将修订版 X 从主干合并到你的分支,确保使用 --record-only
选项。
此合并使用樱桃式合并语法,该语法是在 名为“樱桃式合并”的部分 中引入的。继续使用 名为“重新整合分支”的部分 中的运行示例,其中修订版 X 是修订版 391
$ cd my-calc-branch $ svn update Updating '.': Updated to revision 393. $ svn merge --record-only -c 391 ^/calc/trunk --- Recording mergeinfo for merge of r391 into '.': U . $ svn commit -m "Block revision 391 from being merged into my-calc-branch." Sending . Committed revision 394.
现在你的分支已准备好再次吸收来自主干的更改。在将你的分支与主干同步后,你甚至可以第二次重新整合分支。如果需要,你可以执行另一个仅记录合并以保持分支处于活动状态。重复此过程。
现在应该很清楚为什么删除分支并重新创建它与执行上述仅记录合并具有相同的效果。因为修订版 X 是新创建分支的自然历史的一部分(参见侧边栏 自然历史和隐式合并信息),Subversion 永远不会尝试将修订版 X 合并到分支中,从而避免了虚假冲突。
任何版本控制系统的主要功能之一是跟踪谁更改了什么以及他们何时更改。 svn log 和 svn blame 子命令只是为此目的的工具:当在单个文件上调用时,它们不仅显示影响该文件的变更集的历史记录,还显示确切的用户编写了哪一行代码,以及她何时编写。
但是,当更改开始在分支之间复制时,事情就开始变得复杂起来。例如,如果你要询问 svn log 关于你的功能分支的历史记录,它将显示曾经影响该分支的每个修订版。
$ cd my-calc-branch $ svn log -q ------------------------------------------------------------------------ r390 | user | 2002-11-22 11:01:57 -0600 (Fri, 22 Nov 2002) ------------------------------------------------------------------------ r388 | user | 2002-11-21 05:20:00 -0600 (Thu, 21 Nov 2002) ------------------------------------------------------------------------ r381 | user | 2002-11-20 15:07:06 -0600 (Wed, 20 Nov 2002) ------------------------------------------------------------------------ r359 | user | 2002-11-19 19:19:20 -0600 (Tue, 19 Nov 2002) ------------------------------------------------------------------------ r357 | user | 2002-11-15 14:29:52 -0600 (Fri, 15 Nov 2002) ------------------------------------------------------------------------ r343 | user | 2002-11-07 13:50:10 -0600 (Thu, 07 Nov 2002) ------------------------------------------------------------------------ r341 | user | 2002-11-03 07:17:16 -0600 (Sun, 03 Nov 2002) ------------------------------------------------------------------------ r303 | sally | 2002-10-29 21:14:35 -0600 (Tue, 29 Oct 2002) ------------------------------------------------------------------------ r98 | sally | 2002-02-22 15:35:29 -0600 (Fri, 22 Feb 2002) ------------------------------------------------------------------------
但这真的是对分支上发生的所有更改的准确描述吗?这里遗漏的是,修订版 390、381 和 357 实际上是合并来自主干的更改的结果。如果你详细查看其中一个日志,则构成分支更改的多个主干变更集将无处可寻。
$ svn log -v -r 390 ------------------------------------------------------------------------ r390 | user | 2002-11-22 11:01:57 -0600 (Fri, 22 Nov 2002) | 1 line Changed paths: M /branches/my-calc-branch/button.c M /branches/my-calc-branch/README Final merge of trunk changes to my-calc-branch.
我们碰巧知道,这次合并到分支只是主干更改的合并。我们如何才能看到这些主干更改呢?答案是使用 --use-merge-history
(-g
) 选项。此选项扩展了作为合并一部分的那些 “子” 更改。
$ svn log -v -r 390 -g ------------------------------------------------------------------------ r390 | user | 2002-11-22 11:01:57 -0600 (Fri, 22 Nov 2002) | 1 line Changed paths: M /branches/my-calc-branch/button.c M /branches/my-calc-branch/README Final merge of trunk changes to my-calc-branch. ------------------------------------------------------------------------ r383 | sally | 2002-11-21 03:19:00 -0600 (Thu, 21 Nov 2002) | 2 lines Changed paths: M /branches/my-calc-branch/button.c Merged via: r390 Fix inverse graphic error on button. ------------------------------------------------------------------------ r382 | sally | 2002-11-20 16:57:06 -0600 (Wed, 20 Nov 2002) | 2 lines Changed paths: M /branches/my-calc-branch/README Merged via: r390 Document my last fix in README.
通过使日志操作使用合并历史记录,我们不仅看到了我们查询的修订版 (r390),还看到了与它一起出现的两个修订版——Sally 对主干进行的一些更改。这是一个更完整的历史记录!
svn blame 命令也接受 --use-merge-history
(-g
) 选项。如果忽略此选项,查看 button.c
的逐行注释的人可能会错误地认为你负责修复某个错误的那些行。
$ svn blame button.c … 390 user retval = inverse_func(button, path); 390 user return retval; 390 user } …
虽然你确实在版本 390 中提交了这三行代码,但其中两行实际上是 Sally 在版本 383 中写的。
$ svn blame button.c -g … G 383 sally retval = inverse_func(button, path); G 383 sally return retval; 390 user } …
现在我们知道是谁应该为这两行代码负责了!
与 Subversion 开发人员交谈时,你很可能会听到“祖先”这个词。这个词用来描述存储库中两个对象之间的关系:如果它们彼此相关,则一个对象被称为另一个对象的祖先。
例如,假设你在版本 100 中提交了一个对文件 foo.c
的更改。那么 foo.c@99
是 foo.c@100
的“祖先”。另一方面,假设你在版本 101 中提交了删除 foo.c
的操作,然后在版本 102 中添加了一个同名的新文件。在这种情况下,foo.c@99
和 foo.c@102
似乎是相关的(它们具有相同的路径),但实际上它们是存储库中完全不同的对象。它们没有共享任何历史记录或“祖先”。
提出这个问题的原因是为了指出 svn diff 和 svn merge 之间的一个重要区别。前者忽略祖先,而后者对祖先非常敏感。例如,如果你要求 svn diff 比较 foo.c
的版本 99 和 102,你会看到基于行的差异;diff 命令只是盲目地比较两个路径。但如果你要求 svn merge 比较相同的两个对象,它会注意到它们是无关的,并首先尝试删除旧文件,然后添加新文件;输出将显示删除后添加的操作。
D foo.c A foo.c
大多数合并操作涉及比较彼此具有祖先关系的树;因此,svn merge 默认采用这种行为。但是,有时您可能希望 merge 命令比较两个无关的树。例如,您可能已经导入了两个源代码树,它们代表同一个软件项目的不同供应商版本(参见 名为“供应商分支”的部分)。如果您要求 svn merge 比较这两个树,您将看到整个第一棵树被删除,然后是整个第二棵树的添加!在这些情况下,您希望 svn merge 只进行基于路径的比较,忽略文件和目录之间的任何关系。在您的 merge 命令中添加 --ignore-ancestry
选项,它将像 svn diff 一样工作。(反之,--notice-ancestry
选项将导致 svn diff 像 svn merge 命令一样工作。)
![]() |
提示 |
---|---|
|
一个常见的愿望是重构源代码,尤其是在基于 Java 的软件项目中。文件和目录被四处移动和重命名,这往往会对项目中的每个人造成很大的干扰。听起来像是使用分支的完美案例,不是吗?只需创建一个分支,将东西四处移动,然后将分支合并回主干,对吧?
唉,这种情况现在并不奏效,被认为是 Subversion 当前的弱点之一。问题是 Subversion 的 svn update 命令不像它应该的那样健壮,尤其是在处理复制和移动操作时。
当您使用 svn copy 复制文件时,存储库会记住新文件来自哪里,但它无法将该信息传输到正在运行 svn update 或 svn merge 的客户端。它不会告诉客户端 “将您已经拥有的那个文件复制到这个新位置,”” 而是发送一个全新的文件。这会导致问题,尤其是因为重命名文件也会发生同样的事情。Subversion 的一个鲜为人知的事实是,它缺乏 “真正的重命名”——svn move 命令只不过是 svn copy 和 svn delete 的聚合。
例如,假设在您私有分支上工作时,您将 integer.c
重命名为 whole.c
。实际上,您在分支中创建了一个新文件,它是原始文件的副本,并删除了原始文件。同时,回到 trunk
上,Sally 已经提交了对 integer.c
的一些改进。现在您决定将您的分支合并到主干
$ cd calc/trunk $ svn merge --reintegrate ^/calc/branches/my-calc-branch --- Merging differences between repository URLs into '.': D integer.c A whole.c U . --- Recording mergeinfo for merge between repository URLs into '.': U .
乍一看,这似乎没什么问题,但可能也并非你和 Sally 预期的那样。合并操作删除了 integer.c
文件的最新版本(包含 Sally 的最新更改),并盲目添加了你的新 whole.c
文件,而该文件是 integer.c
文件的旧版本的副本。最终结果是,将你的“重命名”合并到主干中删除了 Sally 在最新版本中的更改!
这并不意味着数据丢失。Sally 的更改仍然存在于仓库的历史记录中,但可能不会立即发现这一点。这个故事的寓意是,在 Subversion 改善之前,要非常小心地将副本和重命名从一个分支合并到另一个分支。
如果你刚刚将服务器升级到 Subversion 1.5 或更高版本,那么旧版 Subversion 客户端可能会导致 合并跟踪 出现问题。这是因为旧版客户端不支持此功能;当这些旧版客户端执行 svn merge 时,它们不会修改 svn:mergeinfo
属性的值。因此,后续提交尽管是合并的结果,但不会告诉仓库关于重复更改的信息——该信息丢失了。之后,当“合并感知”客户端尝试自动合并时,它们很可能会遇到由于重复合并而导致的各种冲突。
如果你和你的团队依赖于 Subversion 的合并跟踪功能,你可能需要配置你的仓库以防止旧版客户端提交更改。最简单的方法是检查开始提交钩子脚本中的“功能”参数。如果客户端报告自己具有 mergeinfo
功能,则钩子脚本可以允许提交开始。如果客户端没有报告该功能,则让钩子拒绝提交。示例 4.1. “合并跟踪守门员开始提交钩子脚本” 给出了一个这样的钩子脚本示例
示例 4.1. 合并跟踪守门员开始提交钩子脚本
#!/usr/bin/env python import sys # The start-commit hook is invoked before a Subversion txn is created # in the process of doing a commit. Subversion runs this hook # by invoking a program (script, executable, binary, etc.) named # 'start-commit' (for which this file is a template) # with the following ordered arguments: # # [1] REPOS-PATH (the path to this repository) # [2] USER (the authenticated user attempting to commit) # [3] CAPABILITIES (a colon-separated list of capabilities reported # by the client; see note below) capabilities = sys.argv[3].split(':') if "mergeinfo" not in capabilities: sys.stderr.write("Commits from merge-tracking-unaware clients are " "not permitted. Please upgrade to Subversion 1.5 " "or newer.\n") sys.exit(1) sys.exit(0)
有关钩子脚本的更多信息,请参阅 名为“实现仓库钩子”的部分。
底线是 Subversion 的合并跟踪功能具有极其复杂的内部实现,而 svn:mergeinfo
属性是用户了解该机制的唯一窗口。
有时,mergeinfo 会出现在您预计不会被操作触及的路径上。有时,mergeinfo 根本不会生成,而您却期望它生成。此外,mergeinfo 元数据的管理有一整套分类法和行为,例如 “显式” 与 “隐式” mergeinfo,“有效” 与 “无效” 修订版,mergeinfo 的特定机制 “省略”,甚至从父目录到子目录的 “继承”。
我们选择只简要介绍这些详细主题,如果有的话,原因有二。首先,对于普通用户来说,细节级别绝对让人难以招架。其次,更重要的是,普通用户 不应该 理解这些概念;它们通常应该作为烦人的实现细节保留在后台。尽管如此,如果您喜欢这种东西,您可以在 CollabNet 网站(现在镜像在 Subversion 网站上)上发布的一篇论文中获得极好的概述:https://subversion.org.cn/blog/2008-05-06-merge-info.html。
目前,如果您想避开合并跟踪的复杂性,我们建议您遵循以下简单最佳实践
对于短期功能分支,请遵循 名为“基本合并”的部分 中描述的简单过程。
避免子树合并和子树 mergeinfo,仅在分支的根目录上执行合并,而不是在子目录或文件上(请参阅 名为“子树合并和子树 mergeinfo”的部分)。
不要直接编辑 svn:mergeinfo
属性;使用 svn merge 以及 --record-only
选项来实现对元数据的预期更改(如 名为“阻止更改”的部分 中所示)。
您的合并目标应该是工作副本,它代表一个 完整 树的根目录,该树代表存储库中 单个 位置在某个时间点的状态。
不要使用 --allow-mixed-revisions
选项合并到混合修订版工作副本中。
不要合并到具有 “切换” 子目录的目标(如下一节 名为“遍历分支”的部分 中所述)。
避免合并到具有稀疏目录的目标。同样,不要合并到除 --depth=infinity
之外的深度。
确保您对所有合并源具有读取权限,并对所有合并目标具有读写权限。