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.

基本合并

现在你和 Sally 并行地在两个分支上进行开发: 你在自己的私有分支上工作, Sally 在项目的主干 (开发主线) 上工作.

如果项目有很多开发人员, 大多数人都会检出主干的工作副本. 如果有人需要 完成一个长期的修改, 而这个修改的中间成果很可能会扰乱主干, 那么比较标准 的做法是为它创建一个私有分支, 把修改都提交到这个分支上, 直到所有的相关 工作都完成为止.

有了分支后, 好消息是你和 Sally 的工作不会互相干扰, 但坏消息是分支 容易偏离主干过远. 记住, 缓慢爬行 策略的问题是当你完成 分支上的工作时, 把分支上的修改合并到主干上而不产生大量的冲突, 几乎是不 可能的.

因此在工作的过程中, 你和 Sally 会继续分享修改, 哪些修改值得分享完全由你 来决定, Subversion 允许用户有选择地在分支之间 复制 修改. 当你在分支上的工作全部完成时, 分支上的整个修改集合就可以被复制到主干上. 用 Subversion 的行话来讲, 把一个分支上的修改复制到其他分支上— 这 种操作称为 合并 (merging), 完成这种操作的命令是 svn merge.

在下面的例子里, 我们假设 Subversion 客户端和服务器端的版本都是 1.8 或更新的版本. 如果客户端或服务器端的版本小于 1.5, 事情就会变得很复杂: 旧版的 Subversion 不会自动跟踪修改, 这就迫使用户必须手工实现类似的效果, 而这种过程相对来说比较痛苦, 具体来说, 用户必须按照合并语法, 详细地指定 被复制的版本号范围 (见本章后面的 “合并语法详解”一节), 而且还要注 意哪些修改已经合并, 哪些没有. 因此, 我们 强烈 建 议用户不要使用 1.5 版本之前的 Subversion 客户端与服务器端.

变更集

在继续之前, 我们需要提醒读者后面的内容会经常讨论到 修改. 对版本控制系统有经验的用户经常混用 修改 (change) 和 变更集 (changeset) 这两个概念, 但我们必须弄清楚 Subversion 是怎么理解 变更集 ( changeset) 的.

每个人对变更集的理解似乎都有所不同, 至少在变更集对版本控制系统的 意义上都有不同的期待. 从我们的角度来说, 变更集只是一个带有独特的名 字的修改集合. 修改可能包括文件的修改, 目录结构的修改, 或元数据的修改. 更一般的说, 变更集只是带有名字的补丁.

在 Subversion 中, 一个全局的版本号 N 确定了仓库中的一棵目录树: 它是仓库在第 N 次提交后的样子. 同时它还确定了一个隐式的变更集: 如果用户对目录树 NN-1 进行 比较, 就可以得到与第 N 次提交对应的补丁. 正因为如此, 版本号 N 不仅可以表示一棵 目录树, 还可以表示一个变更集. 如果用户使用了一个问题跟踪系统来管理 问题, 用户就可以使用版本号指代修复问题的特定补丁—例如, 这个问题在 r9238 中解决, 然后其他人就可以执行 svn log -r 9238 查看修复问题的提交日志, 再用 svn diff -c 9238 查看补丁的具体内容. Subversion 命令 svn merge 也可以使用版本号作为参数 (读者马上就 会看到). 通过指定参数, 用户可以把一个分支上的特定的变更集合并到另一个 分支上: 为 svn merge 添加参数 -c 9238 就可以把变更集 r9238 合并到你的工作副本里.

保持分支同步

继续我们的例子, 假设自从你开始在自己的私有分支上工作后, 时间过了一周, 你要添加的新特性还未完成, 但你知道在你工作的同时, 团队里的其他人会 继续向项目的主干 /trunk 提交修改. 最好把主干上 的修改复制到你自己的分支上, 以便确保他们的修改能够与你的分支契合, 这可 以通过 自动同步合并 (automatic sync merge) 完成, 自动同步合并的目的是为了让分支与祖先 分支上的修改保持同步. 自动 合并的意思是用户只需要提供 合并所需的最小信息 (也就是合并的源以及被合并的工作副本目标), 至于哪些 修改需要合并则交由 Subversion 决定—在自动合并中, 不需要通过 选项 -r-csvn merge 传递变更集.

[提示] 提示

经常保持分支与开发主线同步可以降低分支被合并到主干上时发生冲突 的概率.

Subversion 知道分支的历史, 也知道它是在什么时候从主干上分离出来. 为了执行一个同步合并, 首先要确保分支的工作副本是 干净的 —也就是没有本地修改. 然后只需要执行:

$ pwd
/home/user/my-calc-branch

$ svn merge ^/calc/trunk
--- Merging r341 through r351 into '.':
U    doc/INSTALL
U    src/real.c
U    src/button.c
U    Makefile
--- Recording mergeinfo for merge of r341 through r351 into '.':
 U   .
 $

命令 svn merge URL 告诉 Subversion 把 URL 上的所有未被合并 的修改都合并到当前工作副本上 (在典型的情况下, 也就是你的工作副本的 根目录). 注意到我们用的是带有脱字符 (^) 的语法 [30], 这样我们 就不用输入完整的主干 URL 地址. 还要注意输出信息中的 Recording mergeinfo for merge…, 这是说合并正在 更新属性 svn:mergeinfo, 我们会在本章后面的 “合并信息和预览”一节 介绍 svn:mergeinfo.

[提示] 提示

在本书及其他地方 (包括 Subversion 邮件列表, 讨论合并跟踪的文章等), 你会经常听到一个术语 合并信息 (mergeinfo), 其实它就是 属性 svn:mergeinfo 的缩写.

执行完上面的例子后, 分支的工作副本就包含了本地修改, 而且这些修改 都是创建完分支后, 主干上的修改的副本:

$ svn status
 M      .
M       Makefile
M       doc/INSTALL
M       src/button.c
M       src/real.c

这时候比较明智的操作是使用 svn diff 查看修 改的内容, 并构建测试分支里的代码. 注意当前工作目录 ( .) 也被修改了, svn diff 显示它新增了 svn:mergeinfo 属性.

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

Property changes on: .
___________________________________________________________________
Added: svn:mergeinfo
   Merged /calc/trunk:r341-351

这个属性是非常重要的与合并相关的元数据, 用户 应该直接修改它的值, 因为后面的 svn merge 会用到该 属性 (关于合并元数据的更多内容, 我们稍后就会进行介绍).

执行完合并后, 可能会有冲突需要处理—就像执行完 svn update 那样—或者可能还需要进行一些小修改, 保证 合并的结果是正确的 (记住, 没有 语法 冲突并不表 示没有 语义 冲突!). 如果合并后产生了很多问题, 用户总是可以用 svn revert . -R 撤消本地的所有 修改, 然后就可以和同事讨论 怎么回事. 如果一切都很顺利, 用户就可以把修改提交到仓库里:

$ svn commit -m "Sync latest trunk changes to my-calc-branch."
Sending        .
Sending        Makefile
Sending        doc/INSTALL
Sending        src/button.c
Sending        src/real.c
Transmitting file data ....
Committed revision 352.

现在, 用户的私有分支就和主干 同步 了, 用户也就不用 担心自己的工作和其他人的相差太远.

假设又过去了一周, 你在自己的分支上提交了更多的修改, 而你的同事也 在不断地修改主干. 再一次, 你想把主干上的修改合并到自己的分支上, 于是 执行下面的命令:

$ svn merge ^/calc/trunk
svn: E195020: Cannot merge into mixed-revision working copy [352:357]; try up\
dating first
$

这种情况可能不在用户的预料之中! 在自己的分支了工作了一周后, 你 发现工作副本包含了混合的版本号 (见 “版本号混合的工作副本”一节). 1.7 及之后版本的 svn merge 在默认情况下禁止向含有混合版本号的工作 副本合并, 简单来说, 这是属性 svn:mergeinfo 合并跟踪 方式的限制导致的 (见 “合并信息和预览”一节), 这些限制意味 着向一个含有混合版本号的工作副本合并将导致无法预料的内容与目录冲突 [31]. 我们不希望产生任何不必要的冲突, 所以先更新工作副 本, 然后再尝试合并.

$ svn up
Updating '.':
At revision 361.

$ svn merge ^/calc/trunk
--- Merging r352 through r361 into '.':
U    src/real.c
U    src/main.c
--- Recording mergeinfo for merge of r352 through r361 into '.':
 U   .

Subversion 知道主干上的哪些修改已经合并到了分支上, 所以它只会合并 那些未合并过的主干修改. 如果构建和测试都没有问题, 用户就可以用 svn commit 把分支的修改提交到仓库里.

子目录合并与子目录合并信息

在本章的大部分例子中, 被合并的目标都是分支 (见 “什么是分支”一节) 的根目录, 虽然这是最常见的 情况, 但是偶尔也需要直接合并分支的子目录, 这种类型的合并称为 子目录合并 (subtree merge), 它的合并信息也相应地称为 子目录合并信息 (subtree mergeinfo ). 子目录合并和子目录合并信息其实并没有什么特别的地方, 唯一需要注意的一点是: 一个分支上完整的合并记录可能不仅仅记录在分支根 目录的合并信息里, 可能还要查看子目录的合并信息才能得到完整的合并信息. 幸运的是 Subversion 会替用户完成这些操作, 用户几乎不需要直接参与, 用一个简单的例子解释一下:

# We need to merge r958 from trunk to branches/proj-X/doc/INSTALL,
# but that revision also affects main.c, which we don't want to merge:
$ svn log --verbose --quiet -r 958 ^/
------------------------------------------------------------------------
r958 | bruce | 2011-10-20 13:28:11 -0400 (Thu, 20 Oct 2011)
Changed paths:
   M /trunk/doc/INSTALL
   M /trunk/src/main.c
------------------------------------------------------------------------

# No problem, we'll do a subtree merge targeting the INSTALL file
# directly, but first take a note of what mergeinfo exists on the
# root of the branch:
$ cd branches/proj-X

$ svn propget svn:mergeinfo --recursive
Properties on '.':
  svn:mergeinfo
    /trunk:651-652

# Now we perform the subtree merge, note that merge source
# and target both point to INSTALL:
$ svn merge ^/trunk/doc/INSTALL doc/INSTALL -c 958
--- Merging r958 into 'doc/INSTALL':
U    doc/INSTALL
--- Recording mergeinfo for merge of r958 into 'doc/INSTALL':
 G   doc/INSTALL

# Once the merge is complete there is now subtree mergeinfo on INSTALL:
$ svn propget svn:mergeinfo --recursive
Properties on '.':
  svn:mergeinfo
    /trunk:651-652
Properties on 'doc/INSTALL':
  svn:mergeinfo
    /trunk/doc/INSTALL:651-652,958

# What if we then decide we do want all of r958? Easy, all we need do is
# repeat the merge of that revision, but this time to the root of the
# branch, Subversion notices the subtree mergeinfo on INSTALL and doesn't
# try to merge any changes to it, only the changes to main.c are merged:
$ svn merge ^/subversion/trunk . -c 958
--- Merging r958 into '.':
U    src/main.c
--- Recording mergeinfo for merge of r958 into '.':
 U   .
--- Eliding mergeinfo from 'doc/INSTALL':
 U   doc/INSTALL

你可能会感到奇怪, 为什么上面的例子里我们只合并了 r958, 但 INSTALL 却含有 r651-652 的合并信息, 这是由于合并 信息的继承性, 合并信息的继承性我们会在 合并信息继承 介绍. 另外还要注意 doc/INSTALL 上的子目录合并信息 被移除了, 或者说被 省略 了, 这被称为 合并信息 省略 (mergeinfo elision), 当 Subversion 检测到多余的子目录合并信息时, 就会发生这种现象.

[提示] 提示

在 Subversion 1.7 版之前, 合并操作会无条件地更新目标所有的子 目录合并信息, 对于拥有大量子目录合并信息的用户而言, 即使是相对比较 简单的合并 (例如只合并了一个文件), 也会影响所有子目录的合并信息, 甚至包括那些不是受影响路径的父目录的目录, 在某种程度上会让人感到 困惑和沮丧. Subversion 1.7 以及之后的版本解决了这个问题, 方法是只 更新受影响路径 (也就是通过应用差异而被修改, 添加或删除的路径, 见 “合并语法详解”一节) 的父目录的子目录合并信息, 有一个例外是目标的合并信息 总是会被更新, 即使被应用的差异不会产生任何修改.

重新整合分支

如果用户完成了分支上的所有工作, 也就是说新特性已经完成, 你已经准备好 把分支合并到主干上 (这样的话团队中的其他成员就可以分享你的工作成果), 合并 的步骤很简单, 首先把分支与主干同步, 就像之前做过的那样[32]

$ svn up # (make sure the working copy is up to date)
Updating '.':
At revision 378.

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

$ # build, test, ...

$ svn commit -m "Final merge of trunk changes to my-calc-branch."
Sending        .
Sending        src/main.c
Transmitting file data .
Committed revision 379.

现在, 使用 svn merge 把分支上的修改合并到主干 上, 这种类型的合并称为 自动再整合 (automatic reintegrate) 合并, 在执行合并之前, 用户需要一份 /calc/trunk 的工作 副本, 可以用 svn checkoutsvn switch (见 “遍历分支”一节) 获取.

[提示] 提示

术语 再整合 来自子命令 merge 的选项 --reintegrate. 该选项在 Subversion 1.8 被 废弃 (1.8 可以自动检测什么时候才需要执行再整合合并), 但是 1.5 到 1.7 版的 Subversion 客户端在执行再整合合并时都要求提供该选项.

在合并分支前, 主干的工作副本不能含有本地修改, 已切换的路径, 或混合 的版本号 (见 “版本号混合的工作副本”一节), 这种状 态不仅会带来很多方便, 而且是自动再整合合并所要求的.

一旦准备好了一个整洁的主干工作副本, 用户就可以把分支合并到主干上了:

$ pwd
/home/user/calc-trunk

$ svn update
Updating '.':
At revision 379.

$ svn merge ^/calc/branches/my-calc-branch
--- Merging differences between repository URLs into '.':
U    src/real.c
U    src/main.c
U    Makefile
--- Recording mergeinfo for merge between repository URLs into '.':
 U   .

$ # build, test, verify, ...

$ svn commit -m "Merge my-calc-branch back into trunk!"
Sending        .
Sending        Makefile
Sending        src/main.c
Sending        src/real.c
Transmitting file data ...
Committed revision 380.

恭喜, 你在分支上提交的修改现在都已经合并到了开发主线. 应该注意的 是和你到目前为止所做的合并操作相比, 自动再整合合并所做的工作不太一样. 之前我们是要求 svn merge 从另一条开发线 (主干) 上 抓取下一个变更集, 然后把变更集复制到另一条开发线 (你的私有分支) 上. 这种操作非常直接, Subversion 每一次都知道如何从一次停止的地方开始. 在我们前面讲过的例子里, Subversion 第一次是把 /calc/trunk 的 r341-351 合并到 /calc/branches/my-calc-branch, 后来它就继续合并 下一段范围, r351-361, 在最后一次同步, 它又合并了 r361-378.

然而, 在把 /calc/branches/my-calc-branch 合并到 /calc/trunk 时, 其底层的数学行为是非常 不一样的. 特性分支现在已经是同时包含了主干修改和分支私有修改的大杂烩, 所以没办法简单地复制一段连续的版本号范围. 通过使用自动再整合合并, 你是在要求 Subversion 只复制那些分支特有的修改 (具体的实现方式是比较最新版的分支 与主干, 最终得到的差异就是分支所特有的修改).

始终记住自动再整合合并只支持上面描述的使用案例, 由于这个狭隘的 重点, 除了前面提到的要求 (最新的工作副本 [33] , 不含有混合的版本号, 已切换的路径或本地修改) 外, svn merge 的大部分选项都会使它不能正常工作, 如果用户用到了除 --accept, --dry-run, --diff3-cmd, --extensions, --quiet 之外的其他非全局选项, 将会得到一个错误.

既然你的私有分支已经合并到了主干上, 现在就可以把它删除了:

$ svn delete ^/calc/branches/my-calc-branch \
             -m "Remove my-calc-branch, reintegrated with trunk in r381."
…

不过, 分支的历史不是很重要吗? 如果有人想查看分支的每一次修改, 审查特性的演变怎么办? 不用担心, 虽然你的分支在 /calc/branches 再也看不到了, 但是它在仓库的历史 里依然存在. 在 /calc/branches 的 URL 上执行 一个简单的 svn log, 就可以看到分支的全部历史. 你的分支甚至可以某一时刻复活, 你期待吗 (见 “恢复已删除的文件”一节).

分支被合并到主干后, 如果选择不删除分支, 你可能会继续从主干同步 修改, 然后再次重新整合分支 [34]. 如果你这样做了, 那么只有第一次重新整合之后发生的 修改 才会被合并到主干上.

合并信息和预览

Subversion 跟踪变更集的基本机制—也就是判断哪些修改已经合并到哪些 分支上—是在版本化的属性中记录数据. 更确切地说, 与合并相关的数据 记录在文件和目录的 svn:mergeinfo 属性中. (如果读者 还不了解 Subversion 的属性, 见 “属性”一节.)

你可以像查看其他属性那样, 查看属性 svn:mergeinfo :

$ cd my-calc-branch

$ svn pg svn:mergeinfo -v
Properties on '.':
  svn:mergeinfo
    /calc/trunk:341-378
[警告] 警告

虽然你可以像修改其他属性那样修改 svn:mergeinfo, 但我们强烈建议你不要这么做, 除非你真地知道自己在做什么.

[提示] 提示

在单个路径上的 svn:mergeinfo 数量可以变得 非常庞大, svn propget --recursivesvn proplist --recursive 在处理大量的子目录合并 信息时, 命令的输出也会很多 (见 “子目录合并与子目录合并信息”一节). 为了减少输出的内容, 比较常用的方法是为命令添加选项 --verbose.

当用户执行 svn merge 时, Subversion 就会自动 更新属性 svn:mergeinfo, 属性的值指出了给定路径上 的哪些修改已经复制到目录上. 在我们之前的例子里, 修改的来源是 /calc/trunk, 被合并的目录是 /calc/branches/my-calc-branch. 旧版的 Subversion 会悄无 声息地维护属性 svn:mergeinfo, 合并后, 用户仍然可 以用命令 svn diffsvn status 查看合并产生的修改, 但是当合并操作修改属性 svn:mergeinfo 时不会显示任何提示信息. 而 Subversion 1.7 及以后的版本就 不再这样了, 当合并操作更新属性 svn:mergeinfo 时, Subversion 会给出一些提示信息. 这些提示信息都是以 --- Recording mergeinfo for 开始, 在合并的末尾输出. 不像其他的合并提示信息, 这些信息不是在描述差异被应用到工作副本 (见 “合并语法详解”一节), 而是在描述 为了跟踪合并而产生的 家务 变化.

Subversion 提供了子命令 svn mergeinfo, 用于查看 两个分支间的合并关系, 特别是查看目录吸收了哪些变更集, 或者查看哪些变 更集它是有资格吸收的, 后者提供了一种预览, 预览随后的 svn merge 命令将会复制哪些修改到分支上. 在默认情况下, svn mergeinfo 将会输出两条分支之间的关系的图形化 概览. 回到我们先前的例子, 用命令 svn mergeinfo 分析 /calc/trunk/calc/branches/my-calc-branch 之间的关系:

$ cd my-calc-branch

$ svn mergeinfo ^/calc/trunk
    youngest common ancestor
    |         last full merge
    |         |        tip of branch
    |         |        |         repository path

    340                382
    |                  |
  -------| |------------         calc/trunk
     \          /
      \        /
       --| |------------         calc/branches/my-calc-branch
              |        |
              379      382

图中显示了 /cal/branches/my-calc-branch 拷贝 自 /calc/trunk@340, 最近的一次自动合并是从分支到 主干的自动再整合合并, 在版本号 380. 注意到图中 没有 显示我们在版本号 352, 362, 372 和 379 执行的自动同步合并, 在每个 方向上只显示了最近的自动合并 [35] 这种默认输出对于获取两个分支之间的合并概览非常有用, 如果想要清楚地看到分支上合并了哪些版本号, 就增加选项 --show-revs=merged:

$ svn mergeinfo ^/calc/trunk --show-revs merged
r344
r345
r346
…
r366
r367
r368

同样地, 为了查看分支可以从主干上合并哪些修改, 就用选项 --show-revs=eligible:

$ svn mergeinfo ^/calc/trunk --show-revs eligible
r380
r381
r382

命令 svn mergeinfo 需要一个 URL (修改的来源), 接受一个可选的 目标 URL (合并修改 的目标). 如果没有指定目标 URL, 命令就把当前工作目录当成目标. 在上面 的例子里, 因为我们要查询的是分支工作副本, 命令假定我们想知道的是主干 URL 上的哪些修改可以合并到 /calc/branches/my-calc-branch .

从 Subversion 1.7 开始, svn mergeinfo 也可以 描述子目录合并信息和不可继承的合并信息. 为了描述子目录合并信息, 要加 上选项 --recursive--depth, 而不可继承的合并信息本来就会被考虑到.

假设有一个分支同时包含了子目录合并信息和不可继承的合并信息:

$ svn pg svn:mergeinfo -vR
# Non-inheritable mergeinfo
Properties on '.':
  svn:mergeinfo
    /calc/trunk:354,385-388*
# Subtree mergeinfo
Properties on 'Makefile':
  svn:mergeinfo
    /calc/trunk/Makefile:354,380

从合并信息中可以看到 r385-388 只被合并到了分支的根目录上, 但不 包括任何一个子文件. 还可以看到 r380 只被合并到了 Makefile 上. 如果用带上选项 --recursivesvn mergeinfo 查看从 /calc/trunk 那里合并了哪些版本号到这个分支上, 我们可以看到其中三个版本号带有星号 标记:

$ svn mergeinfo -R --show-revs=merged ^/calc/trunk .
r354
r380*
r385
r386
r387*
r388*

星号 * 表示该版本号只是被 部分地 合并到目标上 (对于 --show-revs=eligible, 其星号的意义是 相同的). 对于这个例子而言, 它的意思是说如果我们尝试从 ^/trunk 合并 r380, r387 或 r388, 将会产生更多的修改. 同样地, 因为 r354, r385 和 r386 没有 被星号标记, 所以再次合并这些版本号将不会产生任何修改. [36]

获取合并预览的另一种办法是使用选项 --dry-run:

$ svn merge ^/paint/trunk paint-feature-branch --dry-run
--- Merging r290 through r383 into 'paint-feature-branch':
U    paint-feature-branch/src/palettes.c
U    paint-feature-branch/src/brushes.c
U    paint-feature-branch/Makefile

$ svn status
#  nothing printed, working copy is still unchanged.

选项 --dry-run 不会真正地去修改工作副本, 它只会 输出一个真正的合并操作 将会 输出的信息. 如果嫌 svn diff 的输出过于详细, 就可以用这个选项获得一个 比较 高层的 合并预览.

[提示] 提示

执行完合并, 但是在提交之前, 可以用 svn diff --depth=empty /path/to/merge/target 查看被合并的直接目标的修改, 如果被合并的目标是目录, 那么命令就只会输出属性的修改. 这是查看合并后属性 svn:mergeinfo 的变化的简便方法, 从属性的 变化中可以看到这次合并合并了哪些版本号.

当然, 预览合并的最佳方法是执行合并. 记住, 执行 svn merge 并不是一个危险的操作 (除非在合并前, 工作副本含有本地修改, 但我们已经强调过不要在这种情况下执行合并). 如果你不喜欢合并的结果, 执行 svn revert . -R 就可以撤消合并产生的修改. 只有在执行了 svn commit 后, 合并的结果才会被提交到 仓库中.

撤消修改

人们经常使用 svn merge 撤消已经提交的修改. 假设你正开心地在 /calc/trunk 的工作副本上工作, 突然发现版本号 392 提交的修改是完全错误的, 它就不应该被提交. 此时你可 以用 svn merge 在工作副本中 撤消 版本号 392 的修改, 然后把用于撤消 r392 的修改提交到仓库中. 你所要做 的只是指定一个 差异 (对于这个例子而言, 指定 逆差异的命令行参数是 --revision 392:391--change -392).

$ svn merge ^/calc/trunk . -c-392
--- Reverse-merging r392 into '.':
U    src/real.c
U    src/main.c
U    src/button.c
U    src/integer.c
--- Recording mergeinfo for reverse merge of r392 into '.':
 U   .

$ svn st
M       src/button.c
M       src/integer.c
M       src/main.c
M       src/real.c

$ svn diff
…
# verify that the change is removed
…

$ svn commit -m "Undoing erroneous change committed in r392."
Sending        src/button.c
Sending        src/integer.c
Sending        src/main.c
Sending        src/real.c
Transmitting file data ....
Committed revision 399.

我们以前说过, 可以把版本号当成一个特定的变更集, 通过选项 -r, 可以要求 svn merge 向工作副本应用一 个特定的变更集, 或一段变更集范围. 在上面这个例子里, 我们是要求 svn merge 把变更集 r392 的逆修改应用到工作副本上.

记住, 像这样撤消修改和其他 svn merge 操作一样, 用户应该用 svn statussvn diff 确认修改的内容正是心里所期望的那样, 检查没问题后再用 svn commit 提交. 提交后, 在 HEAD 上就再也看 不到 r392 的修改.

读者可能在想: 好吧, 其实并没有真正地撤消提交, 版本号 392 的修改仍然 存在于历史中, 如果有人检出了版本在 r392 到 r398 之间的 calc, 他就会看到错误的修改, 对吧?

说得没错, 当我们谈论 删除 一个修改时, 我们实际上说得是 把修改从版本号 HEAD 中删除, 原始的修改仍然存在于仓库中 的历史中. 在大多数时候, 这种做法已经足够好了, 毕竟大多数人只对项目的 HEAD 感兴趣. 然而, 在少数情况下, 用户可能真地需要把 提交从仓库的历史中完全擦除 (可能是不小心提交了一份机密文档). 这做起来并不 容易, 因为 Subversion 的设计目标之一是不能丢失任何一个修改, 版本号是以其他 版本号为基础的不可修改的目录树, 从历史中删除一个版本号将会产生多米诺骨牌 效应, 使后面的版本号产生混乱, 甚至可能会使所有的工作副本失效.[37]

恢复已删除的文件

版本控制系统的一大好处是信息永远不会丢失. 即使你删除了一个文件或 目录, 虽然在版本号 HEAD 中已经看不到被删除的文件, 但它们在早先的版本中仍然存在. 新用户经常问的一个问题是 怎样才 能找回以前的文件或目录?

第一步是准确地指定你想要恢复的是哪一项条目. 一种比较形象的比喻是 把仓库中的每个对象都想像成一个二维坐标, 第一个坐标是特定的版本号目录 树, 第二个坐标是目录内的路径, 于是文件或目录的每一个版本都可以由一对 坐标唯一地确定.

首先, 用户可能要用 svn log 找到他想恢复的二维 坐标, 比较好的策略是在曾经含有被删除的项目的目录中运行 svn log --verbose, 选项 --verbose (-v) 显示了在每个版本号中 被修改的所有项目, 你所要做的就是找到那个删除了文件或目录的版本号. 用户 可以依靠自己的肉眼寻找, 也可以借助其他工具 (例如 grep ) 扫描 svn log 的输出. 如果用户已经知道 待恢复的项目是在最近的提交中才被删除, 那还可以用选项 --limit 限制 svn log 的输出.

$ cd calc/trunk

$ svn log -v --limit 3
------------------------------------------------------------------------
r401 | sally | 2013-02-19 23:15:44 -0500 (Tue, 19 Feb 2013) | 1 line
Changed paths:
   M /calc/trunk/src/main.c

Follow-up to r400: Fix typos in help text.
------------------------------------------------------------------------
r400 | bill | 2013-02-19 20:55:08 -0500 (Tue, 19 Feb 2013) | 4 lines
Changed paths:
   M /calc/trunk/src/main.c
   D /calc/trunk/src/real.c

* calc/trunk/src/main.c: Update help text.

* calc/trunk/src/real.c: Remove this file, none of the APIs
  implemented here are used anymore.
------------------------------------------------------------------------
r399 | sally | 2013-02-19 20:05:14 -0500 (Tue, 19 Feb 2013) | 1 line
Changed paths:
   M /calc/trunk/src/button.c
   M /calc/trunk/src/integer.c
   M /calc/trunk/src/main.c
   M /calc/trunk/src/real.c

Undoing erroneous change committed in r392.
------------------------------------------------------------------------

在上面的例子里, 我们假设要找的文件是 real.c, 通过查看父目录的日志, 可以看到 real.c 是在版本号 400 被删除. 因此, real.c 的最后一个版本就是紧挨着 400 的前一个版本号, 也 就是说你要从版本号 399 中恢复 /calc/trunk/real.c.

这本来是最难的地方—调研. 既然已经知道了要复原的是哪个项目, 接下来你有两个选择.

其中一个选择是使用 svn merge 反向 应用版本号 400 (我们已经在 “撤消修改”一节 介绍了如何撤消修改). 命令的效果是把 real.c 重新添加到工作副本里, 提交 后, 文件将重新出现在版本号 HEAD 中.

然而对于我们这个例子而言, 可能并不是最好的办法. 反向应用版本号 400 不仅会添加 real.c, 从版本号 400 的提交日志 可以看到, 反向应用还会撤消 main.c 的某些修改, 这应该不是用户想要的效果. 当然, 你也可以在逆合并完 r400 后, 再手动地 对 main.c 执行 svn revert. 但这种解决办法可扩展性不好, 如果有 90 个文件在 r400 中被修改了, 难道 也要一个个地执行 svn revert 吗?

第二种选择的目的性更强, 不使用 svn merge, 而是 用 svn copy 从仓库中复制特定的版本号与路径 坐标 到工作副本里:

$ svn copy ^/calc/trunk/src/real.c@399 ./real.c
A         real.c

$ svn st
A  +    real.c

# Commit the resurrection.
…

状态输出中的加号表示这个项目不仅仅是新增的, 而且还带有历史信息, Subversion 知道它是从哪里复制来的. 以后对 real.c 执行 svn log 将会遍历到 r399 之前的历史, 也就是说 real.c 并不是真正的新文件, 它是已删除的原始文件 的后继, 通常这就是用户想要的效果. 然而, 如果你不想维持文件以前的历史, 还可以下面的方法恢复文件:

$ svn cat ^/calc/trunk/src/real.c@399 > ./real.c

$ svn add real.c
A         real.c

# Commit the resurrection.
…

虽然我们的例子都是在演示如何恢复被删除的文件, 但同样的技术也可以 用在恢复目录上. 另外, 恢复被删除的文件不仅可以发生在工作副本中, 还可 直接发生在仓库中:

$ svn copy ^/calc/trunk/src/real.c@399 ^/calc/trunk/src/real.c \
           -m "Resurrect real.c from revision 399."

Committed revision 402.

$ svn up
Updating '.':
A    real.c
Updated to revision 402.


[30] 脱字符语法在 1.6 加入

[31] 命令 svn merge 的选项 --allow-mixed-revisions 允许用户关闭这个限制, 但是这样做的前提是用户必须理解可能的后果, 以及动机要足够充分.

[32] 从 Subversion 1.7 开始, 用户并非一定要像例子中演示的那样, 每次都将分支 的根目录与主 干进行同步. 如果 分支已经通过一系列的子目录合并, 在效果上已经实现了与主干的同步, 后面的再整合合并也可以正常工作. 但是请 读者扪心自问, 如果分支在效果上已经同步了, 那为什么还要再做子目录合并呢? 此时再做子目录合并只会带来无谓的复杂性.

[33] 自动再整合合 并也支持目标是浅检出的目录 (见 “稀疏目录”一节), 但是如果受影响的路径 由于目录是稀疏的, 而不出现在工作副本中, 那么该路径就会被忽略— 这可能 不是 用户想要的结果!

[34] 只有 Subversion 1.8 允许 这样重用一个特性分支. 较早的版本要求一个特性分支在被多次重新整合 之前, 需要一些特殊的处理, 更多的信息参见本书较早的版本: http://svnbook.red-bean.com/en/1.7/svn.branchmerge.basicmerging.html#svn.branchemerge.basicmerging.reintegrate

[35] 这里的 方向 指的是从主干到分支 (自动同步) 或从分支到主干 (自动再整合) 的合并.

[36] 这是不可实施的 合并版本号的好例子.

[37] Subversion 已经计划在未来的某一天, 能够实现永久地删除提交历史, 但在 Subversion 实现之前, 可以从 “svndumpfilter”一节 找到变通 办法.