本文档仍在编写中,内容可能随时变更,可能无法准确描述 Apache™ Subversion® 软件的任何已发布版本。将此页面添加为书签或以其他方式将其推荐给他人可能不是一个明智的选择。请访问 https://svnbooks.subversion.org.cn/ 以获取本书的稳定版本。

基本合并

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

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

因此,好消息是你们并没有互相干扰。坏消息是,很容易走得太远。请记住,躲进洞里 策略的问题之一是,当你完成分支时,将你的更改合并回主干可能会变得非常困难,因为要解决大量的冲突。

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

在以下示例中,我们假设你的 Subversion 客户端和服务器都在运行 Subversion 1.8(或更高版本)。如果客户端或服务器的版本低于 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 中进行重要更改。将这些更改复制到自己的分支中对你来说是最有利的,以确保它们与你的更改很好地配合。这可以通过执行 自动同步合并 来完成——这是一种旨在将你的分支更新到其祖先父分支自创建分支以来所做的所有更改的合并操作。 自动 合并只是一种合并,在这种合并中,你只需要提供完成合并所需的最少信息(即一个合并源和一个工作副本目标),并让 Subversion 确定哪些更改需要合并——在自动合并中,不会通过 -r-c 选项将任何变更集传递给 svn merge

[Tip] 提示

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

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

$ 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 合并到当前工作目录(通常是工作副本的根目录)的更改合并。请注意,我们使用插入符号 (^) 语法[33] 来避免键入完整的 /trunk URL。还要注意 为合并记录合并信息… 通知。这告诉你合并正在更新 svn:mergeinfo 属性。我们将在本章后面的 名为“合并信息和预览”的部分 中讨论此属性和这些通知。

[Tip] 提示

在这本书和其他地方(Subversion 邮件列表、关于合并跟踪的文章等)中,你将经常遇到 合并信息 这个术语。这只是 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
$

这真是出乎意料!在过去的一周中对您的分支进行更改后,您现在发现您的工作副本包含各种修订版(请参阅 名为“混合修订版工作副本”的部分)。在 Subversion 1.7 及更高版本中, svn merge 子命令默认情况下会禁用合并到混合修订版工作副本。不进行过多详细介绍,这是因为 svn:mergeinfo 属性跟踪合并的方式存在限制(有关详细信息,请参阅 名为“合并信息和预览”的部分)。这些限制意味着合并到混合修订版工作副本会导致意外的文本和树冲突。 [34] 我们不希望出现任何不必要的冲突,因此我们将更新工作副本,然后重新尝试合并。

$ 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 到您的分支。

子树合并和子树合并信息

在本节的大多数示例中,合并目标是分支的根目录(请参阅 名为“什么是分支?”的部分)。虽然这是一个最佳实践,但您有时可能需要直接合并到分支根目录的某个子目录中。这种类型的合并称为 子树合并,用于描述它的合并信息称为 子树合并信息。子树合并或子树合并信息没有什么特别之处。事实上,关于这些概念只有一个重要的要点需要牢记:分支的完整合并记录可能不完全包含在分支根目录上的合并信息中。您可能需要考虑子树合并信息才能获得完整的记账。幸运的是,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 及更高版本通过仅更新合并修改路径的父路径的子树上的合并信息来解决此问题(即,由差异应用更改、添加或删除的路径,请参阅 名为“合并语法:完整披露”的部分)。此行为的唯一例外是实际的合并目标;即使应用的差异没有进行任何更改,也会始终更新合并目标的合并信息以描述合并。

重新整合分支

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

$ 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 子命令将您的分支更改自动复制回主干。这种类型的合并称为 自动重新整合 合并。您将需要 /calc/trunk 的工作副本。您可以通过执行 svn checkout、从磁盘上的某个位置找到旧的主干工作副本,或使用 svn switch 来获取它(请参阅 名为“遍历分支”的部分)。

[Tip] 提示

术语 重新整合 来自 merge 选项 --reintegrate。此选项在 Subversion 1.8 中已弃用(该版本会自动检测何时需要重新整合合并),但在 Subversion 1.5 到 1.7 客户端执行重新整合合并时是必需的。

您的主干工作副本不能有任何本地编辑、切换路径或包含混合修订版(请参阅 名为“混合修订版工作副本”的部分)。虽然这些通常是合并的最佳实践,但对于自动重新整合合并而言,它们是 必需的

获得主干的干净工作副本后,您就可以将您的分支合并回主干了

$ 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 都知道如何从它停止的地方继续。在我们之前的示例中,您可以看到,首先它将 /calc/trunk 中的范围 341:351 合并到 /calc/branches/my-calc-branch;后来,它继续合并下一个连续可用的范围 351:361。在执行最终同步时,它会合并范围 361:378。

但是,将 /calc/branches/my-calc-branch 合并回 /calc/trunk 时,底层数学运算完全不同。您的功能分支现在是复制的主干更改和私有分支更改的混合,因此没有简单的连续修订版范围可以复制过去。通过使用自动合并,您要求 Subversion 谨慎地仅复制您的分支中 唯一 的更改。(实际上,它是通过比较最新的主干树与最新的分支树来实现的:结果差异正是您的分支更改!)

请记住,自动重新整合合并仅支持上述用例。由于这种狭隘的关注点,除了之前提到的要求(最新的工作副本 [36] 没有混合修订版、切换路径或本地更改)之外,它还不能与大多数其他 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 命令将显示您分支的完整历史记录。如果您愿意,甚至可以恢复您的分支(请参阅 名为“恢复已删除的项目”的部分)。

如果您选择在将分支重新整合到主干后不删除它,则可以继续从主干执行同步合并,然后再次重新整合分支。 [37] 如果您这样做,则只将您在第一次重新整合后对分支进行的更改合并到主干。

合并信息和预览

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

您可以像检查任何其他版本化属性一样检查 mergeinfo 属性

$ cd my-calc-branch

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

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

[Tip] 提示

单个路径上的 svn:mergeinfo 数量可能会非常大,处理大量子树 mergeinfo 时,svn propget --recursivesvn proplist --recursive 的输出也会很大。参见 名为“子树合并和子树 mergeinfo”的部分。在这两种情况下,--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 属性时提醒您。这些通知都以 --- Recording mergeinfo for 开头,并出现在合并结束时。与其他合并通知不同,这些通知不会描述将差异应用到工作副本(参见 名为“合并语法:完整披露”的部分),而是描述为跟踪已合并内容而进行的“维护”更改。

Subversion 还提供了一个子命令 svn mergeinfo,它有助于查看两个分支之间的合并关系;具体来说,哪些变更集被目录吸收或哪些变更集仍然有资格接收。后者提供了一种预演,即后续的 svn merge 操作将复制到您的分支的更改。默认情况下,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

该图表显示 /calc/branches/my-calc-branch 是从 /calc/trunk@340 复制的,最近的自动合并是我们在 r380 中从分支到主干进行的重新集成合并。请注意,该图表 显示我们在修订版 352、362、372 和 379 中进行的四次自动同步合并。仅显示两个方向上最近的一次自动合并[38]。此默认输出对于获取两个分支之间合并的概述很有用,但要查看已合并的具体修订版,我们需要使用 --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,则假定当前工作目录是目标。在前面的示例中,因为我们正在查询分支工作副本,所以该命令假定我们有兴趣从指定的 trunk URL 接收对 /calc/branches/my-calc-branch 的更改。

从 Subversion 1.7 开始,svn mergeinfo 子命令还可以考虑子树 mergeinfo 和不可继承的 mergeinfo。它通过使用 --recursive--depth 选项来考虑子树 mergeinfo,而默认情况下会考虑不可继承的 mergeinfo。

假设我们有一个包含子树 mergeinfo 和不可继承 mergeinfo 的分支

$ 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

从上面的 mergeinfo 可以看出,r385-388 仅合并到分支的根目录,而不是其任何子目录。我们还看到 r380 仅合并到 Makefile。当我们使用 svn mergeinfo 以及 --recursive 选项来查看从 /calc/trunk 合并到此分支的内容时,我们会看到三个修订版用 * 标记

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

* 表示仅 部分 合并到相关目标的修订版(如果我们正在检查合格修订版,则含义相同)。在此示例中,这意味着如果我们尝试从 ^/trunk 合并 r380、r387 或 r388,则会产生更多更改。同样,因为 r354、r385 和 r386 没有* 标记,所以我们知道重新合并这些修订版不会产生任何结果。 [39]

另一种更精确地预览合并操作的方法是使用 --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 会提供太多详细信息的情况,它对于获取潜在合并的 高级 预览很有用。

[Tip] 提示

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

当然,预览合并操作的最佳方法就是执行它!请记住,运行 svn merge 本身并非一件有风险的事情(除非您对工作副本进行了本地修改,但我们已经强调您不应该合并到这种环境中)。如果您不喜欢合并的结果,只需运行 svn revert . -R 来还原工作副本中的更改,然后使用不同的选项重试命令。合并只有在您实际 svn commit 结果时才会变为最终结果。

撤消更改

svn merge 的一个极其常见的用途是回滚已提交的更改。假设您正在愉快地处理 /calc/trunk 的工作副本,并且您发现修订版 392 中所做的更改(它更改了几个代码文件)完全错误。它不应该被提交。您可以使用 svn merge撤消 工作副本中的更改,然后将本地修改提交到存储库。您只需指定 反向 差异。(您可以通过指定 --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 修订版中。

您可能又在想:好吧,这并没有真正撤消提交,对吧?更改仍然存在于修订版 392 中。如果有人检出修订版 392 和 398 之间的 calc 项目版本,他们仍然会看到错误的更改,对吧?

是的,确实如此。当我们谈到 删除 更改时,我们实际上指的是将其从 HEAD 修订版中删除。原始更改仍然存在于仓库的历史记录中。对于大多数情况来说,这已经足够了。大多数人只对跟踪项目的 HEAD 感兴趣。但是,在某些特殊情况下,您可能真的希望销毁提交的所有证据。(也许有人意外提交了机密文件。)事实证明,这并不容易,因为 Subversion 的设计初衷是永远不会丢失信息。修订版是相互构建的不可变树。从历史记录中删除修订版会导致连锁反应,在所有后续修订版中造成混乱,并可能使所有工作副本失效。[40]

恢复已删除的项目

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

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

首先,您可能需要使用 svn log 来发现您要恢复的精确坐标对。一个好策略是在以前包含已删除项目的目录中运行 svn log --verbose--verbose (-v) 选项显示每个修订版中所有更改项目的列表;您只需找到删除文件或目录的修订版即可。您可以通过视觉方式执行此操作,或者使用其他工具检查日志输出(通过 grep,或者可能通过编辑器中的增量搜索)。如果您知道该项目最近被删除,您还可以使用 --limit 选项来使日志输出简短到足以手动检查。

$ 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。通过查看父目录的日志,您已经发现此文件在修订版 400 中被删除。因此,该文件存在的最后一个版本是在此修订版之前的修订版中。结论:您要从修订版 399 中恢复路径 /calc/trunk/real.c

这是最困难的部分——研究。现在您知道了要恢复的内容,您有两个不同的选择。

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

然而,在本例中,这可能不是最佳策略。反向应用修订版 400 不仅会安排 real.c 添加,而且日志消息表明它还会撤消对 main.c 的某些更改,而您不希望这样做。当然,您可以反向合并修订版 400,然后 svn revertmain.c 的本地修改,但这项技术无法很好地扩展。如果修订版 400 中更改了 90 个文件怎么办?

第二个更具针对性的策略是不使用 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 会记住从哪里复制的。将来,对该文件运行 svn log 将会追溯到文件的恢复以及它在修订版 399 之前的所有历史记录。换句话说,这个新的 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.


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

[34] svn merge 子命令选项 --allow-mixed-revisions 允许您覆盖此禁止,但您只有在理解后果并有充分理由时才能这样做。

[35] 从 Subversion 1.7 开始,您不再需要将所有同步合并都执行到分支的根目录,正如我们在这个示例中所做的那样。 如果 您的分支通过一系列子树合并有效地同步,那么重新整合将起作用,但问问自己,如果分支有效地同步,那么为什么要执行子树合并呢?这样做几乎总是无谓地复杂。

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

[37] 只有 Subversion 1.8 支持这种功能分支的重复使用。早期版本在功能分支可以重新整合多次之前需要一些特殊处理。有关更多信息,请参阅本章的早期版本: https://svnbook.subversion.org.cn/en/1.7/svn.branchmerge.basicmerging.html#svn.branchemerge.basicmerging.reintegrate

[38] 我们所说的 方向 是指主干到分支(自动同步)或分支到主干(自动重新整合)合并。

[39] 这是一个无效合并修订版的很好的例子。

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

TortoiseSVN 官方中文版 1.14.7 发布