本文档旨在描述 Apache™ Subversion® 的 1.7.x 系列。如果您运行的是其他版本的 Subversion,我们强烈建议您访问 https://svnbook.subversion.org.cn/ 并查阅适合您 Subversion 版本的文档。

基本合并

现在,你和 Sally 在项目的并行分支上工作:你在一个私有分支上工作,而 Sally 在主干或开发主线上工作。

对于拥有大量贡献者的项目,大多数人通常会拥有主干的工作副本。每当有人需要进行可能中断主干的长期更改时,标准做法是创建一个私有分支,并在所有工作完成之前将更改提交到该分支。

因此,好消息是你们和 Sally 不会互相干扰。坏消息是,很容易偏离 远。请记住,使用 躲进洞里 策略的问题之一是,当您完成分支工作时,可能几乎不可能将您的更改合并回主干,而不会产生大量冲突。

相反,你和 Sally 可以在工作时继续共享更改。您可以决定哪些更改值得共享;Subversion 使您能够选择性地 复制 分支之间的更改。当您完全完成分支工作时,您的整个分支更改集可以复制回主干。在 Subversion 术语中,从一个分支复制更改到另一个分支的通用行为称为 合并,它使用 svn merge 子命令的各种调用来执行。

在以下示例中,我们假设您的 Subversion 客户端和服务器都在运行 Subversion 1.7(或更高版本)。如果客户端或服务器的版本早于 1.5,则情况会更加复杂:系统不会自动跟踪更改,迫使您使用痛苦的手动方法来实现类似的结果。也就是说,您始终需要使用详细的合并语法来指定要复制的特定修订版本范围(请参阅本章后面的 “合并语法:完全披露”部分),并特别注意跟踪已经合并的内容和尚未合并的内容。为此,我们 强烈 建议您确保您的客户端和服务器至少处于 1.5 版本。

变更集

在我们继续之前,我们应该提醒您,在接下来的页面中,将会有很多关于 变更 的讨论。许多熟悉版本控制系统的人会将 变更变更集 互换使用,我们应该澄清 Subversion 对 变更集 的理解。

每个人似乎对变更集都有略微不同的定义,或者至少对版本控制系统拥有变更集的含义有不同的期望。为了我们的目的,让我们说变更集只是一组具有唯一名称的变更。这些变更可能包括对文件内容的文本编辑、对树结构的修改或对元数据的调整。更常见的说法是,变更集只是一个带有名称的补丁,您可以参考它。

在 Subversion 中,全局修订号 N 为存储库中的树命名:它是存储库在第 N 次提交后的样子。它也是一个隐式变更集的名称:如果您将树 N 与树 N-1 进行比较,您可以推导出提交的精确补丁。因此,很容易将修订号 N 视为不仅是树,也是变更集。如果您使用问题跟踪器来管理错误,您可以使用修订号来引用修复错误的特定补丁——例如,此问题已通过 r9238 修复。 然后,某人可以运行 svn log -r 9238 来阅读修复错误的精确变更集,并运行 svn diff -c 9238 来查看补丁本身。并且(正如您很快就会看到)Subversion 的 svn merge 命令能够使用修订号。您可以通过在合并参数中命名它们来将特定变更集从一个分支合并到另一个分支:将 -c 9238 传递给 svn merge 会将变更集 r9238 合并到您的工作副本中。

保持分支同步

继续我们的示例,假设您开始在私有分支上工作已经过了一周。您的新功能尚未完成,但同时您也知道团队中的其他人继续在项目的 /trunk 中进行重要更改。您最好将这些更改复制到自己的分支,以确保它们与您的更改很好地融合。这可以通过执行 同步合并 来完成——一种旨在将您的分支更新到自创建分支以来对其祖先父分支所做的任何更改的合并操作。

[Tip] 提示

经常将您的分支与主开发线保持同步有助于防止在您将更改合并回主干时出现 意外 冲突。

Subversion 了解您的分支历史记录,并知道它何时从主干分离。要执行同步合并,首先确保您的分支工作副本是 干净的——即它没有 svn status 报告的本地修改。然后只需运行

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

$ svn merge ^/calc/trunk
--- Merging r345 through r356 into '.':
U    button.c
U    integer.c
--- Recording mergeinfo for merge of r345 through r356 into '.':
 U   .
$

此基本语法——svn merge URL——告诉 Subversion 合并所有尚未从 URL 合并到当前工作目录(通常是工作副本的根目录)的更改。请注意,我们使用插入符号 (^) 语法[26] 以避免必须键入整个 /trunk URL。还要注意 正在记录合并信息以进行合并… 通知。这告诉您合并正在更新 svn:mergeinfo 属性。我们将在本章后面的 “合并信息和预览”部分 中讨论此属性和这些通知。

[Tip] 提示

在本书和其他地方(Subversion 邮件列表、有关合并跟踪的文章等)中,您会经常遇到 合并信息 这个词。这只是 svn:mergeinfo 属性的简写。

在运行前面的示例后,你的分支工作副本现在包含新的本地修改,这些修改是自你首次创建分支以来主干上发生的所有更改的副本。

$ svn status
 M      .
M       button.c
M       integer.c
$

此时,明智的做法是使用 svn diff 仔细查看更改,然后构建和测试你的分支。请注意,当前工作目录(.)也已被修改;svn diff 将显示其 svn:mergeinfo 属性已被创建或修改。这是重要的合并相关元数据,你应该 不要 触碰,因为它在未来的 svn merge 命令中需要。(我们将在本章后面详细了解此元数据。)

在执行合并后,你可能还需要解决一些冲突——就像使用 svn update 一样——或者可能需要进行一些小的编辑才能使一切正常工作。(请记住,仅仅因为没有 语法 冲突并不意味着没有 语义 冲突!)如果你遇到严重问题,你可以随时通过运行 svn revert . -R(这将撤消所有本地修改)并与你的合作者开始长时间的 发生了什么事? 讨论来中止本地更改。但是,如果一切看起来都很好,你可以将这些更改提交到存储库中。

$ svn commit -m "Merged latest trunk changes to my-calc-branch."
Sending        .
Sending        button.c
Sending        integer.c
Transmitting file data ..
Committed revision 357.
$

此时,你的私有分支现在与主干 同步,因此你可以放心地知道,当你继续孤立地工作时,你不会偏离其他人的工作太远。

假设又过了一周。你已经将更多更改提交到你的分支,你的同事也一直在改进主干。你再次想要将最新的主干更改复制到你的分支,并使自己同步。只需再次运行相同的合并命令!

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

这真是出乎意料!在你过去一周对分支进行更改后,你发现自己的工作副本包含了混合的修订版(请参阅 名为“Mixed-revision working copies”的部分)。在 Subversion 1.7 版本发布后, svn merge 子命令默认情况下会禁用合并到混合修订版的工作副本。不赘述细节,这是因为 svn:mergeinfo 属性跟踪合并的方式存在限制(请参阅 名为“Mergeinfo and Previews”的部分 获取详细信息)。这些限制意味着合并到混合修订版的工作副本会导致意外的文本和树冲突。 [27] 我们不希望出现任何不必要的冲突,因此我们将更新工作副本,然后重新尝试合并。

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

$ svn merge ^/calc/trunk
--- Merging r357 through r380 into '.':
U    integer.c
U    Makefile
A    README
--- Recording mergeinfo for merge of r357 through r380 into '.':
 U   .
$

Subversion 知道您之前将哪些主干更改复制到您的分支,因此它只小心地复制您还没有的更改。 再次,您构建、测试并 svn commit 将本地修改提交到您的分支。

子树合并和子树合并信息

在本章的大多数示例中,合并目标是分支的根目录(参见 名为“什么是分支?”的部分)。虽然这是最佳实践,但您可能偶尔需要直接合并到分支根目录的某个子目录。这种类型的合并称为 子树合并,记录以描述它的合并信息称为 子树合并信息。子树合并或子树合并信息没有什么特别之处。事实上,关于这些概念,只有一个重要的点需要牢记:分支的完整合并记录可能不完全包含在分支根目录的合并信息中。您可能需要查看任何子树合并信息才能获得完整的记录。幸运的是,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

您可能想知道为什么上面的示例中的 INSTALL 有 r651-652 的合并信息,而我们只合并了 r958。这是由于合并信息继承,我们将在侧边栏 合并信息继承 中介绍。还要注意,doc/INSTALL 上的子树合并信息已被删除,或 省略。这称为 合并信息省略,它发生在 Subversion 检测到冗余子树合并信息时。

[Tip] 提示

在 Subversion 1.7 之前,合并会无条件地更新目标下的所有子树合并信息,以描述合并。对于拥有大量子树合并信息的使用者来说,这意味着相对简单的合并(例如,只对单个文件应用了差异)会导致所有具有合并信息的子树发生变化,即使这些子树不是受影响路径的父级。这会导致一定程度的困惑和沮丧。Subversion 1.7 通过仅更新合并修改路径的父级子树的合并信息来解决此问题(即,通过应用差异更改、添加或删除的路径,请参阅 名为“合并语法:完整披露”的部分)。此行为的一个例外是实际的合并目标;合并目标的合并信息始终会更新以描述合并,即使应用的差异没有进行任何更改。

重新整合分支

但是,当您最终完成工作时会发生什么?您的新功能已完成,您已准备好将分支更改合并回主干(以便您的团队可以享受您的劳动成果)。这个过程很简单。首先,将您的分支与主干同步,就像您一直以来所做的那样[28]

$ svn merge ^/calc/trunk
--- Merging r381 through r385 into '.':
U    button.c
U    README
--- Recording mergeinfo for merge of r381 through r385 into '.':
 U   .

$ # build, test, ...

$ svn commit -m "Final merge of trunk changes to my-calc-branch."
Sending        .
Sending        button.c
Sending        README
Transmitting file data ..
Committed revision 390.

现在,使用 svn merge--reintegrate 选项将您的分支更改复制回主干。您需要一个 /trunk 的工作副本。您可以通过执行 svn checkout、从磁盘上的某个位置挖掘出旧的主干工作副本,或使用 svn switch 来获取一个(请参阅 名为“遍历分支”的部分)。您的主干工作副本不能有任何本地编辑或包含混合修订版(请参阅 名为“混合修订版工作副本”的部分)。虽然这些通常是合并的最佳实践,但它们在使用 --reintegrate 选项时是 必需的

一旦你拥有了干净的工作副本,你就可以将你的分支合并回主干了。

$ pwd
/home/user/calc-trunk

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

$ svn merge --reintegrate ^/calc/branches/my-calc-branch
--- Merging differences between repository URLs into '.':
U    button.c
U    integer.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        button.c
Sending        integer.c
Sending        Makefile
Transmitting file data ..
Committed revision 391.

恭喜你,你分支特定的更改现在已经合并回主开发线了。请注意我们这次使用了 --reintegrate 选项。这个选项对于将分支的更改重新整合回其原始开发线至关重要——不要忘记它!这是必要的,因为这种 合并回 与你之前所做的工作不同。之前,我们要求 svn merge 从一个开发线(主干)获取 下一组 更改,并将它们复制到另一个开发线(你的分支)。这相当简单,每次 Subversion 都知道如何从它停止的地方继续。在我们之前的例子中,你可以看到它首先将主干到分支的 345:356 范围合并;之后,它继续合并下一个连续可用的范围 356:380。在进行最终同步时,它将合并 380:385 范围。

然而,当将你的分支合并回主干时,底层数学原理完全不同。你的功能分支现在是复制的主干更改和私有分支更改的混合体,因此没有简单的连续修订范围可以复制。通过指定 --reintegrate 选项,你要求 Subversion 仔细复制 你的分支特有的更改。(实际上,它是通过比较最新的主干树和最新的分支树来实现的:结果的差异正是你的分支更改!)

请记住,--reintegrate 选项与大多数 Subversion 子命令选项的通用性相比,非常专业。它支持上面描述的用例,但在该用例之外几乎没有适用性。由于这种狭隘的关注点,除了需要一个没有混合修订的最新工作副本[29] 之外,它不能与大多数其他 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 r391."
Committed revision 392.

但是等等!该分支的历史记录难道不宝贵吗?如果有人想在将来审核您的功能的演变过程并查看您分支的所有更改,怎么办?不用担心。请记住,即使您的分支不再在 /branches 目录中可见,它的存在仍然是存储库历史记录中不可改变的一部分。在 /branches URL 上执行简单的 svn log 命令将显示您分支的整个历史记录。如果您需要,甚至可以恢复您的分支(请参阅 名为“恢复已删除的项目”的部分)。

从分支到主干完成 --reintegrate 合并后,该分支将不再可用于进一步的工作。它无法正确吸收新的主干更改,也不能正确地再次合并到主干。因此,如果您想继续使用您的功能分支,建议您将其销毁,然后从主干重新创建它。

$ svn delete http://svn.example.com/repos/calc/branches/my-calc-branch \
             -m "Remove my-calc-branch, reintegrated with trunk in r391."
Committed revision 392.

$ svn copy http://svn.example.com/repos/calc/trunk \
           http://svn.example.com/repos/calc/branches/my-calc-branch \
           -m "Recreate my-calc-branch from trunk@HEAD."
Committed revision 393.

还有一种方法可以在重新集成后使分支再次可用,而无需删除分支。请参阅 名为“保持已重新集成的分支处于活动状态”的部分

Mergeinfo 和预览

Subversion 用于跟踪变更集(即哪些更改已合并到哪些分支)的基本机制是通过在版本化的属性中记录数据。具体来说,合并数据是在附加到文件和目录的 svn:mergeinfo 属性中跟踪的。(如果您不熟悉 Subversion 属性,请参阅 名为“属性”的部分。)

您可以像检查任何其他属性一样检查该属性。

$ cd my-calc-branch
$ svn propget svn:mergeinfo .
/trunk:341-390
$
[Warning] 警告

虽然可以像修改任何其他版本化的属性一样修改 svn:mergeinfo,但我们强烈建议您不要这样做,除非您 确实 知道自己在做什么。

[Tip] 提示

单个路径上的 svn:mergeinfo 数量可能会非常大,当处理大量的子树 mergeinfo 时,svn propget --recursivesvn proplist --recursive 的输出也会很大,请参阅 名为“子树合并和子树 mergeinfo”的部分。在这两种情况下,--verbose 选项产生的格式化输出通常非常有用。

每次运行 svn merge 时,Subversion 会自动维护 svn:mergeinfo 属性。它的值指示已将哪些对给定路径的更改复制到目标目录中。在我们之前的示例中,合并更改的源路径是 /trunk,而接收更改的目录是 /branches/my-calc-branch。早期版本的 Subversion 会在后台维护 svn:mergeinfo 属性。您仍然可以使用 svn diffsvn status 子命令在合并完成后检测更改,但合并本身不会在更改 svn:mergeinfo 属性时提供任何指示。在 Subversion 1.7 中,这种情况不再存在,它提供了一些新的通知来提醒您合并何时更新 svn:mergeinfo 属性。这些通知都以 --- Recording mergeinfo for 开头,并出现在合并的末尾。与其他合并通知不同,这些通知不描述将差异应用于工作副本(请参阅 名为“合并语法:完整披露”的部分),而是描述为了跟踪已合并内容而进行的“维护”更改。

Subversion 还提供了一个子命令 svn mergeinfo,它不仅可以查看目录已吸收了哪些变更集,还可以查看它还有资格接收哪些变更集。这可以预先了解后续 svn merge 操作将复制到您的分支的更改。

$ cd my-calc-branch

# Which changes have already been merged from trunk to branch?
$ svn mergeinfo ^/calc/trunk
r341
r342
r343
…
r388
r389
r390

# Which changes are still eligible to merge from trunk to branch?
$ svn mergeinfo ^/calc/trunk --show-revs eligible
r391
r392
r393
r394
r395
$

命令 svn mergeinfo 需要一个 URL(更改来源),并接受一个可选的 目标 URL(更改合并到的位置)。如果没有给出目标 URL,则假定当前工作目录为目标。在前面的示例中,因为我们正在查询分支工作副本,所以该命令假定我们有兴趣从指定的 trunk URL 接收对 /branches/mybranch 的更改。

随着 Subversion 1.7 的发布,svn mergeinfo 子命令也可以考虑子树合并信息和不可继承的合并信息。它通过使用 --recursive--depth 选项来考虑子树合并信息,而不可继承的合并信息默认情况下会被考虑。

假设我们有一个分支,它既包含子树合并信息,也包含非继承合并信息。

$ svn propget svn:mergeinfo --recursive -v
# Non-inheritable mergeinfo
Properties on '.':
  svn:mergeinfo
    /trunk:651-652,758*
# Subtree mergeinfo
Properties on 'doc/INSTALL':
  svn:mergeinfo
    /trunk/doc/INSTALL:651-652,958,1060

从上面的合并信息中,我们可以看到 r758 仅被合并到分支的根目录,但没有被合并到任何根目录的子目录。我们还看到 r958 和 r1060 仅被合并到 doc/INSTALL 文件中。当我们使用 svn mergeinfo 命令并加上 --recursive 选项来查看从 ^/trunk 合并到该分支的内容时,我们会看到两个修订版本被标记为 *

$ svn mergeinfo --show-revs=merged ^/trunk . --recursive
651
652
758*
958*
1060

* 表示仅 部分 合并到目标(如果我们正在检查符合条件的修订版本,则含义相同)。在本例中,这意味着如果我们尝试从 ^/trunk 合并 r758 或 r958,则会产生更多更改。同样,因为 r1060 没有 被标记为 *,我们知道它只影响 doc/INSTALL,并且尝试合并它不会产生任何结果。[30]

另一种获得更精确的合并操作预览的方法是使用 --dry-run 选项。

$ svn merge ^/calc/trunk --dry-run
--- Merging r391 through r395 into 'branch':
U    integer.c

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

--dry-run 选项不会实际将任何本地更改应用到工作副本。它只显示在实际合并中 打印的状态代码。它对于获取潜在合并的 高级 预览非常有用,尤其是在运行 svn diff 命令时信息过于详细的情况下。

[Tip] 提示

在执行合并操作后,但在提交合并结果之前,可以使用 svn diff --depth=empty /path/to/merge/target 命令来查看仅合并目标的直接更改。如果您的合并目标是目录,则只会显示属性差异。这是一种查看合并操作记录的 svn:mergeinfo 属性更改的便捷方法,它会提醒您刚刚合并的内容。

当然,预览合并操作的最佳方法就是直接执行它!请记住,运行 svn merge 命令本身并不存在风险(除非您对工作副本进行了本地修改,但我们已经强调过,您不应该在这样的环境中进行合并)。如果您不喜欢合并的结果,只需运行 svn revert . -R 命令来撤销工作副本中的更改,然后使用不同的选项重新运行该命令。合并操作只有在您实际 svn commit 结果后才算完成。

撤销更改

使用 svn merge 的一个非常常见的用途是回滚已经提交的更改。假设您正在愉快地使用 /calc/trunk 的工作副本,并且您发现早在修订版 303 中做出的更改,该更改修改了 integer.c,是完全错误的。它不应该被提交。您可以使用 svn merge撤销 工作副本中的更改,然后将本地修改提交到存储库。您只需要指定一个 反向 差异。(您可以通过指定 --revision 303:302 或等效的 --change -303 来实现。)

$ svn merge -c -303 ^/calc/trunk
--- Reverse-merging r303 into 'integer.c':
U    integer.c
--- Recording mergeinfo for reverse merge of r303 into 'integer.c':
 U   A-branch

$ svn status
 M      .
M       integer.c

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

$ svn commit -m "Undoing change committed in r303."
Sending        integer.c
Transmitting file data .
Committed revision 350.

正如我们之前提到的,将存储库修订版视为一个特定的变更集是一种思考方式。通过使用 -r 选项,您可以要求 svn merge 将一个变更集或一系列变更集应用到您的工作副本。在我们撤销更改的情况下,我们要求 svn merge 将变更集 r303 向后 应用到我们的工作副本。

请记住,回滚这样的更改就像任何其他 svn merge 操作一样,因此您应该使用 svn statussvn diff 来确认您的工作处于您想要的状态,然后使用 svn commit 将最终版本发送到存储库。提交后,此特定变更集将不再反映在 HEAD 修订版中。

再说一次,您可能在想:好吧,这并没有真正撤销提交,是吗?更改仍然存在于修订版 303 中。如果有人检出修订版 303 和 349 之间的 calc 项目版本,她仍然会看到错误的更改,对吧?

是的,这是真的。当我们谈论删除一个变更时,实际上是指从HEAD修订版中删除它。原始变更仍然存在于仓库的历史记录中。对于大多数情况来说,这已经足够了。大多数人只关心跟踪项目的HEAD。然而,在某些特殊情况下,您可能真的想销毁提交的所有证据。(也许有人不小心提交了机密文件。)事实证明,这并不容易,因为 Subversion 被刻意设计成永远不会丢失信息。修订版是不可变的树,它们相互构建。从历史记录中删除一个修订版会导致多米诺骨牌效应,在所有后续修订版中造成混乱,并可能使所有工作副本失效。[31]

恢复已删除的项目

版本控制系统最棒的一点是信息永远不会丢失。即使您删除了文件或目录,它可能已从HEAD修订版中消失,但该对象仍然存在于早期修订版中。新用户最常问的问题之一是,“如何找回我的旧文件或目录?

第一步是准确定义您要恢复的哪个项目。这里有一个有用的比喻:您可以将仓库中的每个对象都看作存在于一种二维坐标系中。第一个坐标是特定的修订版树,第二个坐标是该树中的路径。因此,您的文件或目录的每个版本都由一个特定的坐标对定义。(请记住“Peg 修订版”语法——foo.c@224——在“Peg 和操作修订版”部分中提到过。)

首先,您可能需要使用svn log来发现您想要恢复的确切坐标对。一个好策略是在曾经包含已删除项目的目录中运行svn log --verbose--verbose (-v) 选项显示每个修订版中所有更改项目的列表;您只需找到您删除文件或目录的修订版即可。您可以通过视觉方式进行操作,也可以使用其他工具来检查日志输出(通过grep,或者可能通过编辑器中的增量搜索)。

$ cd parent-dir
$ svn log -v
…
------------------------------------------------------------------------
r808 | joe | 2003-12-26 14:29:40 -0600 (Fri, 26 Dec 2003) | 3 lines
Changed paths:
   D /calc/trunk/real.c
   M /calc/trunk/integer.c

Added fast fourier transform functions to integer.c.
Removed real.c because code now in double.c.
…

在这个例子中,我们假设你正在寻找一个已删除的文件 real.c。通过查看父目录的日志,你发现该文件在版本 808 中被删除。因此,该文件存在的最后一个版本是在该版本之前的版本。结论:你想从版本 807 中恢复路径 /calc/trunk/real.c

这是最难的部分——研究。现在你已经知道要恢复什么,你有两种选择。

一种选择是使用 svn merge 来反向应用版本 808 反向应用。(我们已经讨论过如何在 名为“撤销更改”的部分 中撤销更改。)这将使 real.c 作为本地修改重新添加。该文件将被安排添加,并且在提交后,该文件将再次存在于 HEAD 中。

然而,在这个特定的例子中,这可能不是最好的策略。反向应用版本 808 不仅会安排 real.c 添加,而且日志消息表明它还会撤销对 integer.c 的某些更改,而你并不希望这样做。当然,你可以反向合并版本 808,然后 svn revertinteger.c 的本地修改,但这种技术扩展性不好。如果版本 808 中更改了 90 个文件怎么办?

第二个更具针对性的策略是不使用 svn merge,而是使用 svn copy 命令。只需将确切的版本和路径 坐标对 从仓库复制到你的工作副本

$ svn copy ^/calc/trunk/real.c@807 ./real.c

$ svn status
A  +    real.c

$ svn commit -m "Resurrected real.c from revision 807, /calc/trunk/real.c."
Adding         real.c
Transmitting file data .
Committed revision 1390.

状态输出中的加号表示该项目不仅计划添加,而且计划添加带历史记录。Subversion 会记住它从哪里复制的。将来,在该文件上运行svn log 将遍历文件的复活以及它在修订版 807 之前的全部历史记录。换句话说,这个新的 real.c 并不是真正的新文件;它是原始删除文件的直接后代。这通常被认为是一件好事。但是,如果您想复活该文件维护与旧文件的历史链接,此技术同样有效。

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

$ svn add real.c
A         real.c

$ svn commit -m "Re-created real.c from revision 807."
Adding         real.c
Transmitting file data .
Committed revision 1390.

虽然我们的示例展示了我们如何复活文件,但请注意,这些相同技术同样适用于复活已删除的目录。还要注意,复活不必发生在您的工作副本中——它可以在存储库中完全发生。

$ svn copy ^/calc/trunk/real.c@807 ^/calc/trunk/ \
           -m "Resurrect real.c from revision 807."
Committed revision 1390.

$ svn update
Updating '.':
A    real.c
Updated to revision 1390.


[26] 这是在 svn 1.6 中引入的。

[27] svn merge 子命令选项 --allow-mixed-revisions 允许您覆盖此禁止,但您应该只在您了解其影响并有充分理由的情况下这样做。

[28] 使用 Subversion 1.7,您不必像本示例中那样将所有同步合并操作都执行到分支的根目录。如果您的分支通过一系列子树合并有效地同步,那么重新集成将起作用,但问问自己,如果分支有效地同步,那么为什么要执行子树合并呢?这样做几乎总是毫无必要地复杂。

[29] 如果目标是浅层检出(请参阅名为“稀疏目录”的部分),则允许重新集成合并,但由于稀疏工作副本而丢失的任何受差异影响的路径将被跳过,这可能不是您想要的!

[30] 这通常被称为无效合并。虽然在本示例中,r1060 的合并会做一些事情:它会更新分支根目录上的 mergeinfo,但它在某种意义上是无效的,因为不会应用任何差异。

[31] 但是,Subversion 项目计划在将来实现一个命令来完成永久删除信息的任務。在此期间,请参阅名为“svndumpfilter”的部分,了解可能的解决方法。