本文本尚在编写中,内容可能随时更改,可能无法准确描述 Apache™ Subversion® 软件的任何已发布版本。为本页添加书签或以其他方式将他人引导至本页可能并非明智之举。请访问 https://svnbook.subversion.org.cn/ 获取此书的稳定版本。

高级合并

自动魔法到此结束。迟早,一旦你掌握了分支和合并的基本操作,你将不得不要求 Subversion 从一个地方合并 特定 的更改到另一个地方。为此,你将不得不开始向 svn merge 传递更复杂的参数。下一节描述了该命令的完全扩展语法,并讨论了需要使用该语法的几种常见场景。

Cherrypicking

就像术语 变更集 常用于版本控制系统一样,术语 Cherrypicking 也是如此。此词指的是从一个分支中选择 一个 特定的变更集并将其复制到另一个分支的操作。Cherrypicking 也可能指从一个分支复制一组特定的变更集(不一定连续!)到另一个分支的操作。这与更典型的合并场景形成对比,在更典型的合并场景中,下一个 连续的修订范围会自动复制。

人们为什么要只复制一个更改?这种情况比你想象的要多。例如,假设你创建了一个新的功能分支 /calc/branches/my-calc-feature-branch,它从 /calc/trunk 复制而来。

$ svn log ^/calc/branches/new-calc-feature-branch -v -r403
------------------------------------------------------------------------
r403 | user | 2013-02-20 03:26:12 -0500 (Wed, 20 Feb 2013) | 1 line
Changed paths:
   A /calc/branches/new-calc-feature-branch (from /calc/trunk:402)

Create a new calc branch for Feature 'X'.
------------------------------------------------------------------------

在饮水机旁,你听到有人说 Sally 在主干上对 main.c 做了一个有趣的更改。查看主干提交的历史记录,你会发现她在修订版 413 中修复了一个严重错误,该错误直接影响到你正在进行的功能开发。你可能还没有准备好将所有主干更改合并到你的分支,但你肯定需要这个特定的错误修复才能继续你的工作。

$ svn log ^/calc/trunk -r413 -v
------------------------------------------------------------------------
r413 | sally | 2013-02-21 01:57:51 -0500 (Thu, 21 Feb 2013) | 3 lines
Changed paths:
   M /calc/trunk/src/main.c

Fix issue #22 'Passing a null value in the foo argument
of bar() should be a tolerated, but causes a segfault'.
------------------------------------------------------------------------

$ svn diff ^/calc/trunk -c413
Index: src/main.c
===================================================================
--- src/main.c  (revision 412)
+++ src/main.c  (revision 413)
@@ -34,6 +34,7 @@
…
# Details of the fix
…

就像你在前面的例子中使用 svn diff 检查修订版 413 一样,你也可以将相同的选项传递给 svn merge

$ cd new-calc-feature-branch

$ svn merge ^/calc/trunk -c413
--- Merging r413 into '.':
U    src/main.c
--- Recording mergeinfo for merge of r413 into '.':
 U   .

$ svn st
 M      .
M       src/main.c

你现在可以按照通常的测试步骤进行操作,然后再将此更改提交到你的分支。提交后,Subversion 会更新你的分支上的 svn:mergeinfo 以反映 r413 已合并到分支中。这会阻止将来的自动同步合并尝试再次合并 r413。(将相同的更改合并到同一个分支几乎总是会导致冲突!)还要注意 mergeinfo /calc/branches/my-calc-branch:341-379。这是在我们进行 r380 的 /calc/branches/my-calc-branch 分支到 /calc/trunk 的重新整合合并期间记录的。当我们在 r403 创建 my-calc-branch 分支时,此 mergeinfo 会随着复制操作一起保存。

$ svn pg svn:mergeinfo -v
Properties on '.':
  svn:mergeinfo
    /calc/branches/my-calc-branch:341-379
    /calc/trunk:413

还要注意,mergeinfo 中没有将 r413 列为“可合并”,因为它已经合并了。

$ svn mergeinfo ^/calc/trunk --show-revs eligible
r404
r405
r406
r407
r409
r410
r411
r412
r414
r415
r416
…
r455
r456
r457

前面的说明意味着,当最终需要进行自动同步合并时,Subversion 会将合并分成两部分。首先,它会合并到修订版 412 之前的所有可合并的更改。然后,它会合并从修订版 414 到 HEAD 修订版的全部可合并修订。由于我们已经 cherrypicked 了 r413,因此该更改会被跳过。

$ svn merge ^/calc/trunk
--- Merging r403 through r412 into '.':
U    doc/INSTALL
U    src/main.c
U    src/button.c
U    src/integer.c
U    Makefile
U    README
--- Merging r414 through r458 into '.':
G    doc/INSTALL
G    src/main.c
G    src/integer.c
G    Makefile
--- Recording mergeinfo for merge of r403 through r458 into '.':
 U   .

这种从一个分支复制(或 回溯)错误修复到另一个分支的用例可能是 cherrypicking 更改最流行的原因;它经常出现,例如,当团队维护软件的 发布分支 时。(我们在 名为“发布分支”的部分 中讨论了这种模式。)

[Warning] 警告

你是否注意到,在最后一个例子中,合并调用合并了两个不同的范围?svn merge 命令将两个独立的补丁应用到你的工作副本,以跳过变更集 413,因为你的分支已经包含了该变更集。这样做本身没有什么问题,只是它有可能使冲突解决变得更加棘手。如果第一范围的更改产生了冲突,你 必须 以交互方式解决这些冲突才能继续合并过程并应用第二范围的更改。如果你推迟解决第一波更改产生的冲突,整个合并命令将以错误消息退出,并且你必须解决冲突才能再次运行合并以获取剩余的更改。

提醒一下:虽然 svn diffsvn merge 在概念上非常相似,但在许多情况下它们确实具有不同的语法。请务必阅读 svn 参考——Subversion 命令行客户端 中关于它们的详细内容,或者询问 svn help。例如,svn merge 需要一个工作副本路径作为目标,也就是说,它应该将生成的补丁应用到哪个位置。如果没有指定目标,它会假设你正在尝试执行以下常见操作之一。

  • 你想将目录更改合并到当前工作目录中。

  • 你想将特定文件的更改合并到当前工作目录中同名文件。

如果你正在合并一个目录并且没有指定目标路径,svn merge 会假设第一种情况,并尝试将更改应用到当前目录。如果你正在合并一个文件,并且该文件(或同名文件)存在于当前工作目录中,svn merge 会假设第二种情况,并尝试将更改应用到同名的本地文件。

合并语法:完全公开

你现在已经看到了一些 svn merge 命令的示例,你将看到更多示例。如果你对合并的具体工作原理感到困惑,你并不孤单。许多用户(尤其是刚接触版本控制的用户)最初会对该命令的正确语法以及何时何地使用该功能感到困惑。但不要害怕,这个命令实际上比你想象的要简单得多!有一个非常简单的方法可以理解 svn merge 的行为。

混淆的主要来源是该命令的 名称。术语 merge 某种程度上表示分支被合并在一起,或者正在进行某种神秘的数据混合。情况并非如此。该命令更好的名称可能是 svn diff-and-apply,因为这正是它的作用:比较两个存储库树,并将差异应用到工作副本。

如果你使用 svn merge 来执行分支之间基本的更改复制,自动合并通常会执行正确操作。例如,以下命令:

$ svn merge ^/calc/branches/some-branch

会尝试将 some-branch 上所做的任何更改复制到你的当前工作目录中,该目录很可能是一个与该分支共享某些历史记录的工作副本。该命令足够智能,可以只复制你的工作副本中尚未存在的更改。如果你每周重复此命令一次,它只会复制自你上次合并以来 最新 的分支更改。

如果你选择通过向它提供要复制的特定修订范围来使用 svn merge 命令的所有功能,该命令将接受三个主要参数。

  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 的两个不同修订版的情况。 这种类型的合并被称为(出于显而易见的原因)2-URL 合并。最后一个语法显示工作副本参数是可选的;如果省略,它默认为当前目录。

虽然第一个示例显示了 svn merge完整 语法,但要谨慎使用它;它可能导致不记录任何 svn:mergeinfo 元数据的合并。下一节将更详细地讨论这一点。

没有 mergeinfo 的合并

Subversion 尝试尽可能生成合并元数据,以使 svn merge 的未来调用更加智能。但是,仍然有一些情况不会创建或更改 svn:mergeinfo 数据。请记住,要对这些情况稍微谨慎一些。

合并不相关的源

如果你要求 svn merge 比较两个彼此无关的 URL,仍会生成一个补丁并将其应用到你的工作副本,但不会创建任何合并元数据。这两个源之间没有共同的历史记录,而将来的 智能 合并依赖于这种共同历史记录。

从外部存储库合并

虽然可以运行 svn merge -r 100:200 http://svn.foreignproject.com/repos/trunk 等命令,但由此生成的补丁也缺少任何历史合并元数据。在撰写本文时,Subversion 无法在 svn:mergeinfo 属性中表示不同的存储库 URL。

使用 --ignore-ancestry

如果将此选项传递给 svn merge,它会使合并逻辑无脑地生成差异,就像 svn diff 一样,忽略任何历史关系。我们将在本章后面 名为“注意或忽略血统”的部分 中讨论这个问题。

从目标的自然历史中应用反向合并

在本章的前面 (名为“撤销更改”的部分),我们讨论了如何使用 svn merge 来应用 反向补丁 作为回滚更改的一种方式。如果这种技术用于撤销对对象个人历史记录的更改(例如,将提交 r5 到主干,然后立即使用 svn merge . -c -5 回滚 r5),这种类型的合并不会影响记录的 mergeinfo。[41]

关于合并冲突的更多信息

就像 svn update 命令一样,svn merge 将更改应用于你的 working copy。因此,它也可能创建冲突。然而,由 svn merge 生成的冲突有时不同,本节将解释这些差异。

首先,假设你的 working copy 没有本地编辑。当你 svn update 到某个特定修订版时,服务器发送的更改始终 干净地 应用于你的 working copy。服务器通过比较两棵树来生成 delta:你的 working copy 的虚拟快照,以及你感兴趣的修订版树。因为比较的左侧与你已经拥有的内容完全相同,所以 delta 能够保证将你的 working copy 正确地转换为右侧树。

svn merge 没有这样的保证,并且可能更加混乱:高级用户可以要求服务器比较 任何 两棵树,即使它们与 working copy 无关!这意味着存在很大的潜在人为错误。用户有时会比较错误的两棵树,创建无法干净应用的 delta。 svn merge 子命令尽其所能应用尽可能多的 delta,但有些部分可能无法应用。合并错误的 delta 的一个常见迹象是意外的树冲突

$ svn merge ^/calc/trunk -r104:115
--- Merging r105 through r115 into '.':
   C doc
   C src/button.c
   C src/integer.c
   C src/real.c
   C src/main.c
--- Recording mergeinfo for merge of r105 through r115 into '.':
 U   .
Summary of conflicts:
  Tree conflicts: 5

$ svn st
 M      .
!     C doc
      >   local dir missing, incoming dir edit upon merge
!     C src/button.c
      >   local file missing, incoming file edit upon merge
!     C src/integer.c
      >   local file missing, incoming file edit upon merge
!     C src/main.c
      >   local file missing, incoming file edit upon merge
!     C src/real.c
      >   local file missing, incoming file edit upon merge
Summary of conflicts:
  Tree conflicts: 5

在前面的例子中,可能是 doc 和四个 *.c 文件在正在比较的分支的两个快照中都存在。生成的 delta 想要改变你的 working copy 中对应路径的内容,但这些路径不存在于 working copy 中。无论如何,树冲突的普遍存在很可能意味着用户比较了错误的两棵树,或者你正在合并到错误的 working copy 目标;两者都是用户错误的典型迹象。当这种情况发生时,可以轻松地递归地撤销合并创建的所有更改 (svn revert . --recursive),删除撤销后留下的任何未版本化的文件或目录,并使用正确的参数重新运行 svn merge

还要记住,合并到没有本地编辑的 working copy 仍然可能产生文本冲突。

$ svn st

$ svn merge ^/paint/trunk -r289:291
--- Merging r290 through r291 into '.':
C    Makefile
--- Recording mergeinfo for merge of r290 through r291 into '.':
 U   .
Summary of conflicts:
  Text conflicts: 1
Conflict discovered in file 'Makefile'.
Select: (p) postpone, (df) diff-full, (e) edit, (m) merge,
        (mc) mine-conflict, (tc) theirs-conflict, (s) show all options: p

$ svn st
 M      .
C       Makefile
?       Makefile.merge-left.r289
?       Makefile.merge-right.r291
?       Makefile.working
Summary of conflicts:
  Text conflicts: 1

冲突怎么可能发生?同样,因为用户可以请求 svn merge 将任何旧的 delta 定义并应用于 working copy,所以该 delta 可能包含无法干净地应用于 working 文件的文本更改,即使该文件没有本地修改。

svn updatesvn merge 之间的另一个细微差别是,在发生冲突时创建的全文文件的名称。在 名为“解决任何冲突”的部分 中,我们看到更新会生成名为 filename.minefilename.rOLDREVfilename.rNEWREV 的文件。当 svn merge 产生冲突时,它会创建三个名为 filename.workingfilename.merge-left.rOLDREVfilename.merge-right.rNEWREV 的文件。在这种情况下,术语 merge-leftmerge-right 描述了文件来自双树比较的哪一侧,rOLDREV 描述了左侧的修订版,而 rNEWREV 描述了右侧的修订版。无论如何,这些不同的名称可以帮助你区分因更新而发生的冲突和因合并而发生的冲突。

阻止更改

有时,你不想自动合并某个特定的更改集。例如,也许你团队的策略是在 /trunk 上进行新的开发工作,但对于将更改回传到用于向公众发布的稳定分支更加保守。在一种极端情况下,你可以手动从主干挑选单个更改集到分支——只有足够稳定的更改才能通过测试。也许情况并不那么严格;也许大部分时间你只是让 svn merge 自动将大多数更改从主干合并到分支。在这种情况下,你需要一种方法来屏蔽一些特定的更改,也就是说,阻止它们被自动合并。

要阻止更改集,你必须让 Subversion 相信该更改已经 合并。为此,使用 --record-only 选项调用 merge 子命令。该选项使 Subversion 记录 mergeinfo,就好像它实际上执行了合并一样,但实际上没有任何差异被应用

$ cd my-calc-branch

$ svn merge ^/calc/trunk -r386:388 --record-only
--- Recording mergeinfo for merge of r387 through r388 into '.':
 U   .

# Only the mergeinfo is changed
$ svn st
 M      .

$ svn pg svn:mergeinfo -vR
Properties on '.':
  svn:mergeinfo
    /calc/trunk:341-378,387-388

$ svn commit -m "Block r387-388 from being merged to my-calc-branch."
Sending        .

Committed revision 461.

从 Subversion 1.7 开始,--record-only 合并是可传递的。这意味着,除了记录描述被阻止的修订版的 mergeinfo 之外,还会应用合并源中的任何 svn:mergeinfo 属性差异。例如,假设我们想要阻止 'paint-python-wrapper' 特性永远被从 ^/paint/trunk 合并到 ^/paint/branches/paint-1.0.x 分支。我们知道这个特性的工作是在它自己的分支上完成的,该分支在修订版 465 中被重新集成到 /paint/trunk

$ svn log -v -r465 ^/paint/trunk
------------------------------------------------------------------------
r465 | joe | 2013-02-25 14:05:12 -0500 (Mon, 25 Feb 2013) | 1 line
Changed paths:
   M /paint/trunk
   A /paint/trunk/python (from /paint/branches/paint-python-wrapper/python:464)

Reintegrate Paint Python wrapper.
------------------------------------------------------------------------

因为修订版 465 是一个重新集成合并,所以我们知道 mergeinfo 会被记录下来,以描述这个合并

$ svn diff ^/paint/trunk --depth empty -c465
Index: .
===================================================================
--- .   (revision 464)
+++ .   (revision 465)

Property changes on: .
___________________________________________________________________
Added: svn:mergeinfo
   Merged /paint/branches/paint-python-wrapper:r463-464

现在,仅仅阻止从 /paint/trunk 合并修订版 465 并不保险,因为有人可能会从 /paint/branches/paint-python-wrapper 直接合并 r462:464。幸运的是,--record-only 合并的可传递性阻止了这种情况;--record-only 合并会应用修订版 465 中的 svn:mergeinfo 差异,从而阻止 直接/paint/trunk间接/paint/branches/paint-python-wrapper 合并该更改

$ cd paint/branches/paint-1.0.x

$ svn merge ^/paint/trunk --record-only -c465
--- Merging r465 into '.':
 U   .
--- Recording mergeinfo for merge of r465 into '.':
 G   .

$ svn diff --depth empty
Index: .
===================================================================
--- .   (revision 462)
+++ .   (working copy)

Property changes on: .
___________________________________________________________________
Added: svn:mergeinfo
   Merged /paint/branches/paint-python-wrapper:r463-464
   Merged /paint/trunk:r465

$ svn ci -m "Block the Python wrappers from the first release of paint."
Sending        .

Committed revision 466.

现在,任何随后尝试将该特性合并到 /paint/trunk 的尝试都将失效

$ svn merge ^/paint/trunk -c465
--- Recording mergeinfo for merge of r465 into '.':
 U   .

$ svn st # No change!

$ svn merge ^/paint/branches/paint-python-wrapper -r462:464
--- Recording mergeinfo for merge of r463 through r464 into '.':
 U   .

$ svn st  # No change!

$

如果你在以后意识到你实际上 需要 将被阻止的特性合并到 /paint/trunk ,你有两个选择。你可以像我们在 名为“撤销更改”的部分 中讨论的那样,反向合并 r466(你阻止该特性的修订版)。一旦你提交了这个更改,你就可以重复从 /paint/trunk 合并 r465。或者,你可以简单地使用 --ignore-ancestry 选项重复从 /paint/trunk 合并 r465,这将使合并忽略任何 mergeinfo,只应用请求的差异,参见 名为“注意到或忽略祖先”的部分

$ svn merge ^/paint/trunk -c465 --ignore-ancestry
--- Merging r465 into '.':
A    python
A    python/paint.py
 G   .

使用 --record-only 阻止更改有效,但它也有一点危险。主要问题是我们没有清楚地区分 我已经有了这个更改我没有这个更改,但我目前不想要它。 我们实际上是在欺骗系统,让它认为该更改以前已经被合并。这将责任推卸给你——用户——让你记住该更改实际上并没有被合并,只是你目前不想要它。无法向 Subversion 询问 被阻止的更改列表。 如果你想跟踪它们(以便将来可以解除阻止),你需要将它们记录在某个文本文件中,或者可能是某个虚构的属性中。

合并敏感的日志和注释

任何版本控制系统的主要功能之一是跟踪谁更改了什么,以及他们何时更改的。 svn logsvn blame 子命令正是为此目的:当它们在单个文件上调用时,它们不仅显示影响该文件的更改集历史记录,还准确地显示哪个用户写了哪一行代码,以及她何时写下的。

然而,当更改开始在分支之间复制时,事情就开始变得复杂。例如,如果你要询问 svn log 关于你的特性分支的历史记录,它将显示曾经影响该分支的每个修订版

$ cd my-calc-branch

$ svn log -q
------------------------------------------------------------------------
r461 | user | 2013-02-25 05:57:48 -0500 (Mon, 25 Feb 2013)
------------------------------------------------------------------------
r379 | user | 2013-02-18 10:56:35 -0500 (Mon, 18 Feb 2013)
------------------------------------------------------------------------
r378 | user | 2013-02-18 09:48:28 -0500 (Mon, 18 Feb 2013)
------------------------------------------------------------------------
…
------------------------------------------------------------------------
r8 | sally | 2013-01-17 16:55:36 -0500 (Thu, 17 Jan 2013)
------------------------------------------------------------------------
r7 | bill | 2013-01-17 16:49:36 -0500 (Thu, 17 Jan 2013)
------------------------------------------------------------------------
r3 | bill | 2013-01-17 09:07:04 -0500 (Thu, 17 Jan 2013)
------------------------------------------------------------------------

但这真的是对分支上发生的所有更改的准确描述吗?这里遗漏的是,修订版 352、362、372 和 379 实际上是从主干合并更改的结果。如果你仔细查看这些日志,构成分支更改的多个主干更改集将无处可见

$ svn log ^/calc/branches/my-calc-branch -r352 -v
------------------------------------------------------------------------
r352 | user | 2013-02-16 09:35:18 -0500 (Sat, 16 Feb 2013) | 1 line
Changed paths:
   M /calc/branches/my-calc-branch
   M /calc/branches/my-calc-branch/Makefile
   M /calc/branches/my-calc-branch/doc/INSTALL
   M /calc/branches/my-calc-branch/src/button.c
   M /calc/branches/my-calc-branch/src/real.c

Sync latest trunk changes to my-calc-branch.
------------------------------------------------------------------------

我们碰巧知道,这次合并到分支只不过是主干更改的合并。我们如何才能看到这些主干更改呢?答案是使用 --use-merge-history (-g) 选项。该选项会扩展那些作为合并的一部分的 更改。

$ svn log ^/calc/branches/my-calc-branch -r352 -v -g
------------------------------------------------------------------------
r352 | user | 2013-02-16 09:35:18 -0500 (Sat, 16 Feb 2013) | 1 line
Changed paths:
   M /calc/branches/my-calc-branch
   M /calc/branches/my-calc-branch/Makefile
   M /calc/branches/my-calc-branch/doc/INSTALL
   M /calc/branches/my-calc-branch/src/button.c
   M /calc/branches/my-calc-branch/src/real.c

Sync latest trunk changes to my-calc-branch.
------------------------------------------------------------------------
r351 | sally | 2013-02-16 08:04:22 -0500 (Sat, 16 Feb 2013) | 2 lines
Changed paths:
   M /calc/trunk/src/real.c
Merged via: r352

Trunk work on calc project.
------------------------------------------------------------------------
…
------------------------------------------------------------------------
r345 | sally | 2013-02-15 16:51:17 -0500 (Fri, 15 Feb 2013) | 2 lines
Changed paths:
   M /calc/trunk/Makefile
   M /calc/trunk/src/integer.c
Merged via: r352

Trunk work on calc project.
------------------------------------------------------------------------
r344 | sally | 2013-02-15 16:44:44 -0500 (Fri, 15 Feb 2013) | 1 line
Changed paths:
   M /calc/trunk/src/integer.c
Merged via: r352

Refactor the bazzle functions.
------------------------------------------------------------------------

通过使日志操作使用合并历史记录,我们不仅看到了我们查询的修订版(r352),还看到了其他与它一起进行的修订版——Sally 对主干的修改。这更完整地展现了历史记录!

svn blame 命令也接受 --use-merge-history (-g) 选项。如果忽略了此选项,查看 src/button.c 的逐行注释的人可能会误以为你对某个特定更改负责。

$ svn blame src/button.c
…
   352    user    retval = inverse_func(button, path);
   352    user    return retval;
   352    user    }
…

虽然你确实在修订版 352 中提交了这三行代码,但其中两行实际上是 Sally 在修订版 348 中编写的,并通过同步合并引入你的分支。

$ svn blame button.c -g
…
G    348    sally   retval = inverse_func(button, path);
G    348    sally   return retval;
     352    user    }
…

现在我们知道应该责怪谁才是那两行代码的真正“罪魁祸首”!

注意到或忽略祖先

在与 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 仅进行基于路径的比较,忽略文件和目录之间的任何关系。在你的 merge 命令中添加 --ignore-ancestry 选项,它将像 svn diff 一样工作。(反过来,--notice-ancestry 选项将使 svn diffsvn merge 命令一样工作。)

[Tip] 提示

--ignore-ancestry 选项还会禁用 合并跟踪。这意味着 svn:mergeinfosvn merge 确定要合并哪些修订版时不会被考虑,也不会记录 svn:mergeinfo 来描述合并。

合并和移动

一个常见的愿望是重构源代码,尤其是在基于 Java 的软件项目中。文件和目录被重新排列和重命名,这往往会对所有参与项目的人造成很大的干扰。听起来像是使用分支的完美案例,不是吗?只需创建一个分支,重新排列东西,然后将分支合并回主干,对吧?

唉,这种情况现在并不适用,被认为是 Subversion 目前的一个弱点。问题在于 Subversion 的 svn merge 命令不像它应该的那样强大,尤其是在处理复制和移动操作时。

当你使用 svn copy 复制文件时,存储库会记住新文件来自哪里,但它无法将这些信息传递给正在运行 svn updatesvn merge 的客户端。它没有告诉客户端“将你已经拥有的那个文件复制到这个新位置”,而是发送了一个全新的文件。这会导致问题,尤其是在重命名的情况下出现树冲突,因为重命名不仅涉及新副本,还涉及删除旧路径——Subversion 的一个鲜为人知的事实是,它缺乏“真正的重命名”——svn move 命令仅仅是 svn copysvn delete 的集合。

例如,假设你想在你的私有分支 /calc/branch/my-calc-branch 上做一些修改。首先,你使用 /calc/trunk 执行自动同步合并,并在 r470 中提交。

$ cd calc/trunk

$ svn merge ^/calc/trunk
--- Merging differences between repository URLs into '.':
U    doc/INSTALL
A    FAQ
U    src/main.c
U    src/button.c
U    src/integer.c
U    Makefile
U    README
 U   .
--- Recording mergeinfo for merge between repository URLs into '.':
 U   .

$ svn ci -m "Sync all changes from ^/calc/trunk through r469."
Sending        .
Sending        Makefile
Sending        README
Sending        FAQ
Sending        doc/INSTALL
Sending        src/main.c
Sending        src/button.c
Sending        src/integer.c
Transmitting file data ....
Committed revision 470.

然后你在 r471 中将 integer.c 重命名为 whole.c,然后在 r473 中对同一个文件进行一些编辑。实际上,你在你的分支中创建了一个新文件(即原始文件的副本加上一些编辑),并删除了原始文件。与此同时,在 /calc/trunk 上,Sally 在 r472 中提交了她自己的 integer.c 的一些改进。

$ svn log -v -r472 ^/calc/trunk
------------------------------------------------------------------------
r472 | sally | 2013-02-26 07:05:18 -0500 (Tue, 26 Feb 2013) | 1 line
Changed paths:
   M /calc/trunk/src/integer.c

Trunk work on integer.c.
------------------------------------------------------------------------

现在你决定将你的分支合并回主干。Subversion 如何将你所做的重命名和编辑与 Sally 所做的编辑结合起来?

$ svn merge ^/calc/branches/my-calc-branch
--- Merging differences between repository URLs into '.':
   C src/integer.c
 U   src/real.c
A    src/whole.c
--- Recording mergeinfo for merge between repository URLs into '.':
 U   .
Summary of conflicts:
  Tree conflicts: 1

$ svn st
 M      .
      C src/integer.c
      >   local file edit, incoming file delete upon merge
 M      src/real.c
A  +    src/whole.c
Summary of conflicts:
  Tree conflicts: 1

答案是 Subversion 不会将这些更改结合起来,而是会引发树冲突[42],因为它需要你的帮助来弄清楚你的更改和 Sally 的更改的哪些部分最终应该出现在 whole.c 中,或者是否应该进行重命名!

你需要在提交合并之前解决此树冲突,这可能需要你手动干预,参见 名为“处理结构冲突”的部分。这个故事的寓意是,在 Subversion 改进之前,要小心地将副本和重命名从一个分支合并到另一个分支,并且在这样做时,要准备好手动解决。

防止天真的客户端提交合并

如果你刚刚将你的服务器升级到 Subversion 1.5 或更高版本,则存在 1.5 之前的 Subversion 客户端可能会导致 合并跟踪 出现问题的风险。这是因为 1.5 之前的客户端不支持此功能;当其中一个较旧的客户端执行 svn merge 时,它根本不修改 svn:mergeinfo 属性的值。因此,随后的提交,尽管是合并的结果,但不会告诉存储库关于重复更改的信息——这些信息丢失了。稍后,当“合并感知”客户端尝试自动合并时,它们很可能会遇到由于重复合并而导致的各种冲突。

如果你和你的团队依赖 Subversion 的合并跟踪功能,你可能希望配置你的存储库以防止较旧的客户端提交更改。执行此操作的简单方法是检查开始提交钩子脚本中的“功能”参数。如果客户端报告它具有 mergeinfo 功能,则钩子脚本可以允许提交开始。如果客户端没有报告该功能,则让钩子拒绝提交。 示例 4.1,“合并跟踪守门员开始提交钩子脚本” 给出了此类钩子脚本的示例。

示例 4.1. 合并跟踪守门员开始提交钩子脚本

#!/usr/bin/env python
import sys

# The start-commit hook is invoked immediately after a Subversion txn is
# created and populated with initial revprops 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)
#   [4] TXN-NAME     (the name of the commit txn just created)

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 “省略”的特定机制,甚至从父目录到子目录的“继承”。

我们选择仅简要介绍这些详细主题(如果有的话),原因有以下两个。首先,对于一般用户来说,细节的程度令人难以接受。其次,更重要的是,一般用户不需要理解这些概念;它们通常作为实现细节保持在后台。尽管如此,如果你喜欢这种东西,你可以在 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 以外的深度。

    • 确保你对所有合并源具有读取权限,并且对所有合并目标具有读写权限。

当然,有时你可能需要违反一些最佳实践。如果你需要这样做,不要担心,只要确保你理解这样做会造成的影响。



[41] 有趣的是,在回滚这样的修订后,我们无法使用 svn merge . -c 5 重新应用该修订,因为 mergeinfo 已经列出了 r5 已经被应用。我们必须使用 --ignore-ancestry 选项,让 merge 命令忽略现有的 mergeinfo!

[42] 如果 Sally 没有在 r472 中进行更改,那么 Subversion 会注意到目标工作副本中的 integer.c 与合并左侧的 integer.c 相同,并允许您的重命名成功,而不会出现树冲突。

$ svn merge ^/calc/branches/my-calc-branch
--- Merging differences between repository URLs into '.':
 U   src/real.c
A    src/whole.c
D    src/integer.c
--- Recording mergeinfo for merge between repository URLs into '.':
 U   .
TortoiseSVN 官方中文版 1.14.7 发布