本篇文档旨在描述 Subversion 1.6.x 系列。如果您运行的是其他版本的 Subversion,强烈建议您访问 https://svnbook.subversion.org.cn/ 并查看适用于您 Subversion 版本的文档。

基本合并

现在您和 Sally 正在项目并行分支上工作:您正在私有分支上工作,Sally 正在主干(或主开发线)上工作。

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

所以,好消息是您和 Sally 不会相互干扰。坏消息是,很容易 过度 偏离。请记住,使用 躲进洞穴 策略的问题之一是,当您完成分支工作时,将您的更改合并回主干可能变得非常困难,因为会出现大量冲突。

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

在以下示例中,我们假设您的 Subversion 客户端和服务器都在运行 Subversion 1.5(或更高版本)。如果客户端或服务器的版本低于 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
$

这个基本语法——svn merge URL—告诉 Subversion 将所有来自 URL 的最新更改合并到当前工作目录(通常是您的工作副本的根目录)。还要注意,我们使用插入符号 (^) 语法[24] 来避免键入完整的 /trunk URL。

运行上一个示例后,您的分支工作副本现在包含新的本地修改,这些编辑是自您第一次创建分支以来在主干上发生的所有更改的副本

$ 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
--- Merging r357 through r380 into '.':
U    integer.c
U    Makefile
A    README
$

Subversion 知道您已经将哪些主干更改复制到您的分支,因此它只仔细复制您还没有的那些更改。再次,您将不得不构建、测试并将 svn commit 本地修改到您的分支。

重新整合分支

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

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

$ # 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)
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
 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 都知道如何接续它之前的地方。在我们之前的示例中,您可以看到它首先将 trunk 到 branch 的 345:356 范围合并;之后,它继续合并下一个连续可用的范围 356:380。在进行最终同步时,它合并了 380:385 范围。

但是,将您的分支合并回主干时,底层数学完全不同。您的功能分支现在是复制的主干更改和私有分支更改的混合体,因此没有简单的连续修订版本范围可以复制过来。通过指定 --reintegrate 选项,您要求 Subversion 仔细复制 那些对您的分支唯一的更改。(实际上,它是通过将最新主干树与最新分支树进行比较来完成此操作:结果差异正是您的分支更改!)

请记住,与大多数 Subversion 子命令选项的更通用性质相比,--reintegrate 选项非常专门。它支持上面描述的用例,但在该用例之外几乎没有适用性。由于这种狭隘的重点,除了需要一个没有混合修订版本的最新工作副本之外,它还不能与大多数其他 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,但我们强烈建议您不要这样做,除非您 真正 知道自己在做什么。

每当您运行 svn merge 时,Subversion 都会自动维护 svn:mergeinfo 属性。它的值指示已将对给定路径所做的哪些更改复制到相关目录中。在我们之前的示例中,合并更改的来源路径是 /trunk,而接收更改的目录是 /branches/my-calc-branch

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 的更改。

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

$ svn merge ^/calc/trunk --dry-run
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 结果后才算最终完成。

[Tip] 提示

虽然反复运行 svn mergesvn revert 来试验合并是完全可以的,但您可能会遇到一些烦人(但很容易绕过)的障碍。例如,如果合并操作添加了一个新文件(即将其计划为添加),则 svn revert 实际上不会删除该文件;它只会取消计划添加。您将剩下一个非版本化文件。如果您然后尝试再次运行合并,您可能会因为非版本化文件 挡道 而发生冲突。解决方案?在执行撤消操作后,请务必清理工作副本并删除非版本化文件和目录。svn status 的输出应尽可能干净,理想情况下不显示任何输出。

撤消更改

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

$ 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 将更改集 #303 向后 应用到我们的工作副本。

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

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

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

恢复已删除的项目

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

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

首先,你可能需要使用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。(我们已经讨论过如何在名为“Undoing Changes”的部分中撤消更改)。这将起到重新添加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
A    real.c
Updated to revision 1390.


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

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