This text is a work in progress—highly subject to change—and may not accurately describe any released version of the Apache™ Subversion® software. Bookmarking or otherwise referring others to this page is probably not such a smart idea. Please visit http://www.svnbook.com/ for stable versions of this book.

高级合并

一旦用户开始频繁地使用分支与合并, 很快就会要求 Subversion 把一个 特定的 的修改从一个地方合并到另一个地方. 为了完成 这项工作, 用户要给 svn merge 传递更多的参数, 下一节 将对命令的语法进行完整地介绍, 同时还将讨论它们的典型应用场景.

精选

和术语 变更集 一样, 术语 精选 (cherrypicking) 也经常出现在版本控制系统中. 精选指的是这样一种操作: 从分支中挑选 一个 特定的 变更集, 将其复制到其他地方. 精选也可以指这样一种操作: 将一个特定的变更集 集合 (不一定是连续的) 从一个分支复制到另一个分支上. 这和典型的合并 场景相反 (典型的合并场景是自动合并下一段版本号范围).

为什么会有人只想复制单独的一个修改? 这种情况要比你想像的更常发生, 假设你从 /calc/trunk 创建了一个特性分支 /calc/branches/my-calc-feature-branch:

$ 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, Sally 修正了一个很严重的错误, 而这个错误也会影响你正在开发的新特性. 你的分支可能还没有准备好合并主干上的所有修改, 但是为了能让工作继续 下去, 你确实需要 r413 的修改.

$ 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 查看 r413 那样, 你也可以向 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 (在同一分支内多次 合并同一修改通常会导致冲突). 还要注意合并信息 /calc/branches/my-calc-branch:341-379, 这条信息 是早先 /calc/trunk 在 r380 再整合合并 /calc/branches/my-calc-branch 时记录的, 当我们 在 r403 创建分支 my-calc-feature-branch 时, 这条合并信息也被一并复制.

$ 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 将把合并分成 两步进行, 第一步是合并所有可合并的修改, 直到 r412, 第二步是从 r414 开始 合并所有可合并的修改, 直到 HEAD. 因为我们已经合并了 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   .

将一个新版本上的修改 回植 ( backporting) 到另一个分支可能是精选修改最常见的需求, 例如, 当开发团队在维护软件的 发布分支 时, 就会经常遇到 这种情况 (见 “发布分支”一节.)

[警告] 警告

在上面的例子里, 读者是否注意到了 Subversion 如何合并两个不同的版 本号范围? svn merge 为了跳过变更集 413 (分支已经包 含了变更集 413), 向工作副本应用了两个独立的补丁, 这么做除了可能会使 冲突更难解决之外, 本身并没有什么不对的地方. 如果第一段修改范围产生了 冲突, 那么只有在冲突解决后才能接着应用第二段修改范围. 如果在出现冲突 后, 用户选择推迟解决冲突, 那么命令 svn merge 将会报错退出, 在第二次运行合并命令之前, 用户必须解决冲突.

提醒一句: svn diffsvn merge 在概念上非常类似, 但在很多情况下它们使用不同的语法, 详情 见 svn 参考手册—Subversion 命令行客户端. 比如说 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 上的修改合并到当前 的工作目录上 (假定当前目录是工作副本或工作副本的一部分, 而且和 some-branch 有历史上的联系), 命令只会合并当前 目录还没有的修改. 如果用户在一周后再执行相同的命令, 命令就只会复制在 上一次合并后新出现的修改.

如果用户想通过指定被复制的版本号范围, 最大程度地使用 svn merge, 则命令接受三个参数:

  1. 一个初始的仓库目录 (通常被叫作比较的 左侧 (left side))

  2. 一个最终的仓库目录 (通常被叫作比较的 右侧 (right side)

  3. 接受差异的工作副本 (通常被叫作合并的 目标 (target))

这三个参数一旦指定, Subversion 就比较两个仓库目录, 将比较产生的差异 作为本地修改应用到目标工作副本. 命令执行结束后, 得到的结果和用户手工编 辑文件或执行各种命令 (例如 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 的两个不同 版本号, 可以像第二种语法那样简写, 这种类型的合并称为 二路 URL 合并 (原因显而易见); 第三种语法说明工作副本参数是可选的, 如果省略工作副本参数, 默认是当前工作目录.

虽然第一个例子展示了 svn merge完整 语法, 但使用时要小心, 它会导致 Subversion 不去更新元数据 svn:mergeinfo, 下一节将对此进行更详细的介绍.

没有合并信息的合并

如果可能的话, Subversion 都会去尝试生成合并的元数据, 从而帮助后面 调用的 svn merge 更加智能, 但在某些情况下, svn:mergeinfo 既不会被创建, 也不会被更新, 对这些情况要稍 微注意一点:

合并不相关的源

如果用户要求 Subversion 去比较两个完全不相关的 URL, 那么 Subversion 仍然会生成补丁并应用到工作副本上, 但不会创建或更新 合并元数据. 因为两个源之间没有公共的历史, 而将来的 智能 合并需要这些公共历史.

合并外部仓库

虽然执行这样一条命令—svn merge -r 100:200 http://svn.foreignproject.com/repos/trunk —是可以的, 但生成的补丁依然缺少 合并元数据. 在撰写本书时, Subversion 还不支持在属性 svn:mergeinfo 内表示多个不同仓库的 URL.

使用--ignore-ancestry

如果向命令 svn merge 传递选项 --ignore-ancestry, 这将导致 svn merge 按照和 svn diff 相同的方式生成 不含有历史的差异, 更多的内容将在 “关注或忽略祖先”一节 介绍.

反向合并目标的修改历史

在本章的 “撤消修改”一节 我们 介绍了如何使用 svn merge 应用一个 逆补丁 , 从而回滚已提交的修改. 如果使用这项技术撤消某个对象的已 提交的修改 (例如在主干上提交了 r5, 之后又马上用 svn merge . -c -5 撤消 r5 的修改), 这种类型的合并也不会更新 合并信息.[38]

关于合并冲突的更多内容

svn update 类似, svn merge 也是向工作副本应用修改, 因此难免会产生冲突. 然而与 svn update 相比, 由 svn merge 产生的冲突有 点不同, 本节就是介绍这些不同之处.

在开始前假设用户的工作副本不含有本地修改, 当用户执行 svn update, 把工作副本更新到某个特定的版本号时, 从服务器接收 的修改总能 干净地 应用到工作副本上. 服务器生成差异的 方式是比较两棵目录树: 一个是工作副本的虚拟快照, 另一个是用户指定的版本 号所对应的目录树. 因为比较的左侧等价于工作副本, 所以生成的差异总能保证 正确地把工作副本更新到右侧.

但是 svn merge 没有这种保证, 而且冲突可能会更 混乱: 高级用户可以要求服务器比较 任意 两个目录树, 即使目录树和工作副本并不相关! 这就意味着有很大的可能产生人为错误. 用户有时候会比较两个错误的目录树, 导致生成的差异不能被干净地应用到工 作副本上. 命令 svn merge 会尽可能多地把修改应用到 工作副本, 但某些修改可能根本就无法应用成功. 合并错误的常见现象是出现了 意想不到的目录冲突:

$ 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 和 4 个 *.c 文件在分支的两个快照中都存在, 生成的 差异想去修改工作副本中对应路径上的文件内容. 但这些路径在工作副本中都 不存在. 无论真实的情况是什么, 产生目录冲突最可能的原因是用户比较了两 个错误的目录树, 或者是差异被应用到了错误的工作副本—这两种都是 用户最常犯的错误. 当错误发生时, 最简单的办法就是递归地撤消由合并产生 的所有本地修改 (svn revert . --recursive), 删除可能残留的未被版本控制的文件和目录, 然后再用正确的参数执行 svn merge.

还要注意, 即使在向不含有本地修改的工作副本合并, 仍有可能产生 内容冲突.

$ 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 定义并应用任意一个老差异到工作副本中, 而这个差异所包含的 修改可能不能被干净地应用到文件中, 即使这个文件不含有本地修改.

svn updatesvn merge 的另 一个不同点是当冲突发生时, 新创建的文件的名字. 在 “解决冲突”一节 我们已经看到更新操作可能会 创建形如 filename.mine, filename.rOLDREVfilename.rNEWREV . 的新文件. 当 svn merge 发生冲突时, 它会 创建 3 个形如 filename.working, filename.merge-left.rOLDREVfilename.merge-right.rNEWREV 的新文件. 模式中的 merge-leftmerge-right 分别指出了文件 来自比较的左侧和右侧, rOLDREV 描述了左侧的版本号, 而 rNEWREV 描述了右侧的版本号. 无论是 svn update , 还是 svn merge, 这些文件名都可以帮助 用户分辨冲突的来源.

拦截修改

有时候, 用户可能不想让某个特定的变更集被自动合并, 比如说你所在的 团队的开发策略是在 /trunk 完成新的开发工作, 但 是, 在向稳定分支回植修改时非常保守, 因为稳定分支是面向发布的分支. 在比较极端的情况下, 你可以手动地从主干精选修改—只精选那些足够 稳定的修改—再合并到分支上. 不过实际做起来可能没这么严格, 大多数时 候你只想让 svn merge 把主干的大多数修改自动合并 到分支上, 这时候就需要一种方法能够屏蔽掉一些特定的变更集, 阻止它们 被自动合并.

为了拦截一个变更集, 必须让 Subversion 认为变更集 已经 被合并了. 为了实现这点, 在执行 svn merge 时添加选项 --record-only, 该选项使得 Subversion 更新 合并信息, 就好像它真得执行了合并, 但实际上文件内容并没有被修改.

$ 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 的合并是传递的, 这就意味着除了在被合并的目标上记录被拦截的合并信息外, 源的 svn:mergeinfo 属性上的任意修改都会被应用到目 标的 svn:mergeinfo 属性上. 举例来说, 我们想要拦截 ^/paint/trunk 上与特性 'paint-python-wrapper' 有关的修改被合并到分支 ^/paint/branches/paint-1.0.x 上. 我们已经知道特性 'paint-python-wrapper' 已经在自己的分支上开发完成, 并且在 r465 合并到了 /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.
------------------------------------------------------------------------

因为 r465 是一个再整合合并, 所以我们知道描述合并的信息被 记录了下来:

$ 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 的 r465 并 不能确保万无一失, 因为其他人可能会直接从 /paint/branches/paint-python-wrapper 合并 r462:464, 幸运的是选项 --record-only 的传递性质 可以防止这种情况发生. 选项 --record-only 把 r465 生成的 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!

$

如果以后用户意识到自己实际上 需要 被拦截的 修改, 那么有两种选择. 一种是用户可以撤消 r466, 撤消的方法见 “撤消修改”一节, 把撤消 r466 的修 改提交后, 用户就可以再次从 /paint/trunk 合并 r465. 另一种是在合并 r465 时带上选项 --ignore-ancestry, 这将导致命令 svn merge 忽略合并信息, 直接应用所 请求的差异, 见 “关注或忽略祖先”一节.

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

使用 --record-only 有一点危险, 因为我们经常无 法分辨什么时候是 我已经包含了这个修改, 什么时候是 我没有这个修改, 但目前还不想要它. 使用 --record-only 实际上是在向 Subversion 撒谎, 让它以为修改 已经被合并了. 记住修改是否被真正地合并是用户的责任, Subversion 无法 回答 有哪些修改被拦截了 这样的问题. 如果用户想跟踪 被拦截的修改 (以后可能会放开对它们的拦截), 就要自己找地方记录, 例如 记在某个文本文件上, 或自定义的属性里.

对合并敏感的日志与注释

任意一个版本控制系统都需要支持的一项特性是能够查看是谁, 在什么时 候, 修改了什么地方, 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)
------------------------------------------------------------------------

但是这些日志完整地刻画了分支上的所有修改吗? 输出中没有明确指出的是 r352, r362, r372 和 r379 其实是从主干合并修改的结果. 如果你详细地查看 这几个日志将会发现我们没办法看到构成分支修改的多个主干变更集:

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

svn log 增加选项 --use-merge-history (-g), 我们不仅可以看到 r352, 还可以看到通过 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    }
…

用户 user 的确在 r352 提交了这 3 行修改, 但其中 2 行实际上来自 Sally 在 r348 的修改, 它们通过同步合并被合并到了分支中:

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

现在我们知道了是谁应该 真正地 为这 2 行修改 负责!

关注或忽略祖先

如果与 Subversion 开发人员交谈, 你可能会经常听到一个术语: 祖先 (ancestry). 这个术语描述了 仓库中两个对象间的一种关系: 如果它们之间是相关的, 那么其中一个对象就 是另一个对象的祖先.

比如说你在 r100 提交了文件 foo.c 的修改, 那么 foo.c@99 就是 foo.c@100 的一个 祖先. 另一方面, 如果你在 r101 删除了文件 foo.c, 然后在 r102 又提交了一个具有相同名字的文件, 虽然从名字上看, foo.c@99foo.c@102 是相关的, 但实际上它们是两个完全不相关的对象, 只是碰巧名 字相同罢了, 它们之间不共享历史或 祖先.

介绍 祖先 是为了说明 svn diffsvn merge 之间的一个重要区别. svn diff 会忽略祖先, 而 svn merge 对祖先非常敏感. 举例来说, 如果用户要求 svn diff 去比较 foo.c 在 r99 和 r102 时的版本, 命令将会盲目地比较这两个 版本, 并输出以行为单位的差异. 但是如果用户要求 svn merge 去比较相同的两个对象, 它将会注意到两个对象之间是不相关的, 于是先删除旧文件, 再添加新文件, 从命令的输出信息可以看得很清楚:

D    foo.c
A    foo.c

大多数合并操作都会涉及比较两个在历史上相关的目录树, 因此 svn merge 就把这种情况当成默认条件. 然而, 在少数情况下用户 可能希望 svn merge 去比较两个不相关的目录树. 比如说 用户导入了两份源代码, 分别表示软件的两个不同的供应商发布版 (见 “供方分支”一节), 如果用户要求 svn merge 去比较这两个目录树, 将会看到第一个目录树被整 体删除, 然后再整体添加第二个目录树. 对于这种情况, 用户其实是希望 svn merge 只做基于路径的比较, 完全忽略文件和目录 之间的任何关系. 添加选项 --ignore-ancestry 后, svn merge 的行为将变得和 svn diff 一样. (反之, 添加 --notice-ancestry 后, 命令 svn diff 的行为将变得和 svn merge 一样)

[提示] 提示

属性 --ignore-ancestry 还会禁止 合并跟踪, 这就 意味着当 svn 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 修改了 whole.c . 从效果上来看等价于创建了一个新文件 (原文件的副本再加上 一些修改), 再删除原文件. 同时在 /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 不会 把这些修改组合起来, 而是产生一个目录冲突[39] 因为 Subversion 需要用户帮它算出你和 Sally 的哪些修改应该留在 whole.c 上, 或者是重命 名操作是否应该保留.

用户解决完目录冲突后才能提交, 这可能需要人工介入, 见 “处理目录冲突”一节. 我们举这个例子的目的是提醒 用户, 在 Subversion 改良之前, 要小心对待从一个分支合并复制和重命名操 作到另一个分支, 如果确实这样做了, 要做好解决目录冲突的准备.

禁止不支持合并跟踪的客户端

如果用户只是把服务器端升级到 Subversion 1.5 及以后的版本, 那么 1.5 版之前的客户端在 合并跟踪 方面会产生 问题, 这是因为 1.5 版之前的客户端不支持这项特性. 当旧版客户端执行 svn merge 时, 命令不会去更新属性 svn:mergeinfo, 因此, 随后的提交虽然是合并的结果, 但 关于被复制的修改的信息不会告诉给服务器—这些信息就此丢失. 以后, 如果新版客户端执行自动合并, 很可能会因为合并重复的修改而产生大量冲突.

如果你和你的团队非常依赖 Subversion 的合并跟踪特性, 你可能需要对 仓库进行配置, 使得仓库禁止旧客户端提交修改. 最简单的配置方法是在 start-commit 钩子脚本里检查参数 capabilities , 如果客户端反映它支持 mergeinfo 功能, 钩子脚本就允许客户端提交, 否则的话就禁止该客户端提交修改, 例 4.1 “合并跟踪的看门狗—钩子脚本 start-commit” 给出了 钩子脚本 start-commit 的一个示例:

例 4.1. 合并跟踪的看门狗—钩子脚本 start-commit

#!/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 是用户了解合并跟踪内部机制的 唯一窗口.

记录合并信息的时机和方式有时候会很难理解, 另外, 合并信息元数据的 管理也分成了很多种类型, 例如 显式隐式 的合并信息, 可实施不可实施 的版本号, 省略 合并信息的特定机制, 以及从父目录到子目录的 继承.

我们决定只对这些主题进行简单的介绍, 原因有以下几点. 首先对于一个普 通用户来说, 细节过于复杂; 第二, 普通用户 不需要 完全理解这些概念, 实现上的细节对他们而言是透明的. 如果读者有兴趣, 可以 阅读 CollabNet 的一篇文章: http://www.open.collab.net/community/subversion/articles/merge-info.html.

如果读者只想尽量避开合并跟踪的复杂性, 我们有以下建议:

  • 如果是短期的特性分支, 遵循 “基本合并”一节 描述的步骤.

  • 避免子目录合并与子目录合并信息, 只在分支的根目录执行合并, 而不是在分支的子目录或文件上执行合并 (见 “子目录合并与子目录合并信息”一节).

  • 不要直接修改属性 svn:mergeinfo, 而是用 带有选项 --record-only 的命令 svn merge 向属性施加期望的修改, 见 “拦截修改”一节).

  • 被合并的目标应该是一个工作副本, 代表了一个 完整的 目录的根, 这个目录则代表了某一时刻, 仓库的一个单一 位置:

    • 在合并前更新! 不要使用选项 --allow-mixed-revisions 去合并含有混合版本号的工作副本.

    • 不要合并带有 已切换的 子目录的目标 (在 “遍历分支”一节 介绍).

    • 避免合并含有稀疏目录的目标, 类似地, 也不要合并深度不是 --depth=infinity 的目标.

    • 确保你对合并的源具有读权限, 对被合并的目标具有读写权限.

当然, 有时候你并不能完全按照上面所说的要求去做, 此时也不用担心, 只 要你知道这样做的后果就行.



[38] 有趣的是, 像这样撤消一个版本号的修改后, 我们就不能再用 svn merge . -c 5 重新合并 r5, 因为合并信息记录的是 r5 已经合并过了. 为了忽略合并信息, 必须加上选项 --ignore-ancestry

[39] 如果 Sally 没有提交 r472 的修改, 那么 Subversion 将会注意到目标工作副本的 integer.c 和合并左端的 whole.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   .