本文档描述了 Subversion 1.4。如果您运行的是更新版本的 Subversion,我们强烈建议您访问 https://svnbook.subversion.org.cn/ 并查阅适合您的 Subversion 版本的书籍版本。

常见用例

分支和 svn merge 有很多不同的用途,本节将介绍您可能遇到的最常见的用途。

将整个分支合并到另一个分支

为了完成我们的运行示例,我们将向前推进时间。假设已经过去了几天,主干和您的私有分支都发生了很多变化。假设您已经完成了在私有分支上的工作;功能或错误修复终于完成了,现在您想将所有分支更改合并回主干,以便其他人可以使用。

那么,我们如何在这种情况中使用 svn merge?请记住,此命令比较两棵树,并将差异应用于工作副本。因此,要接收更改,您需要拥有主干的工作副本。我们假设您仍然拥有您的原始工作副本(已完全更新),或者您最近检出了 /calc/trunk 的全新工作副本。

但是,应该比较哪两棵树?乍一看,答案似乎显而易见:只需将最新的主干树与您的最新分支树进行比较。但请注意,这个假设是 错误的,并且已经困扰了许多新手用户!由于 svn merge 的工作方式类似于 svn diff,因此比较最新的主干和分支树并 不会 仅仅描述您对分支所做的更改集。这种比较显示了太多的更改:它不仅会显示分支更改的添加,还会显示 删除 在您的分支上从未发生的 trunk 更改。

要仅表达发生在您的分支上的更改,您需要比较分支的初始状态和最终状态。使用 svn log 查看您的分支,您会发现您的分支是在修订版 341 中创建的。并且,您的分支的最终状态只是使用 HEAD 修订版的问题。这意味着您想要比较分支目录的修订版 341 和 HEAD,并将这些差异应用于主干的工作副本。

提示

查找创建分支的修订版(分支的“基础”)的一个好方法是将 --stop-on-copy 选项用于 svn log。log 子命令通常会显示对分支所做的所有更改,包括追溯到创建分支的复制操作。因此,通常情况下,您也会看到来自主干的历史记录。当 svn log 检测到其目标被复制或重命名时,--stop-on-copy 会停止日志输出。

因此,在我们继续的示例中,

$ svn log -v --stop-on-copy \
          http://svn.example.com/repos/calc/branches/my-calc-branch
…
------------------------------------------------------------------------
r341 | user | 2002-11-03 15:27:56 -0600 (Thu, 07 Nov 2002) | 2 lines
Changed paths:
   A /calc/branches/my-calc-branch (from /calc/trunk:340)

$

正如预期的那样,此命令打印的最终修订版是 my-calc-branch 通过复制创建的修订版。

以下是最终的合并过程,

$ cd calc/trunk
$ svn update
At revision 405.

$ svn merge -r 341:405 http://svn.example.com/repos/calc/branches/my-calc-branch
U   integer.c
U   button.c
U   Makefile

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

# ...examine the diffs, compile, test, etc...

$ svn commit -m "Merged my-calc-branch changes r341:405 into the trunk."
Sending        integer.c
Sending        button.c
Sending        Makefile
Transmitting file data ...
Committed revision 406.

再次注意,提交日志消息非常具体地提到了已合并到主干的更改范围。始终牢记这一点,因为这是您以后需要的重要信息。

例如,假设您决定继续在您的分支上工作一个星期,以便完成对原始功能或错误修复的增强。存储库的 HEAD 修订版现在是 480,您准备从您的私有分支到主干进行另一个合并。但是,正如在 名为“合并最佳实践”的部分 中所述,您不想合并之前已经合并的更改;您只想合并自上次合并以来您的分支上的所有“”内容。诀窍是找出什么是新的。

第一步是在主干上运行 svn log,并查找有关您上次从分支合并的日志消息

$ cd calc/trunk
$ svn log
…
------------------------------------------------------------------------
r406 | user | 2004-02-08 11:17:26 -0600 (Sun, 08 Feb 2004) | 1 line

Merged my-calc-branch changes r341:405 into the trunk.
------------------------------------------------------------------------
…

啊哈!由于修订版 341 和 405 之间发生的的所有分支更改以前都已作为修订版 406 合并到主干,因此您现在知道您只想合并此后发生的的更改,方法是比较修订版 406 和 HEAD

$ cd calc/trunk
$ svn update
At revision 480.

# We notice that HEAD is currently 480, so we use it to do the merge:

$ svn merge -r 406:480 http://svn.example.com/repos/calc/branches/my-calc-branch
U   integer.c
U   button.c
U   Makefile

$ svn commit -m "Merged my-calc-branch changes r406:480 into the trunk."
Sending        integer.c
Sending        button.c
Sending        Makefile
Transmitting file data ...
Committed revision 481.

现在,主干包含对分支进行的第二波完整更改。此时,您可以删除您的分支(我们将在后面讨论),或者继续在您的分支上工作并重复此过程以进行后续合并。

撤销更改

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

$ svn merge -c -303 http://svn.example.com/repos/calc/trunk
U  integer.c

$ svn status
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 的设计初衷是永远不会丢失信息。修订版是相互构建的不可变树。从历史记录中删除修订版会导致多米诺骨牌效应,在所有后续修订版中造成混乱,并可能使所有工作副本失效。 [23]

恢复已删除的项目

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

第一步是明确定义您要恢复的 哪个 项目。这是一个有用的比喻:您可以将存储库中的每个对象都看作存在于一种二维坐标系中。第一个坐标是特定修订版树,第二个坐标是该树中的路径。因此,您的文件或目录的每个版本都可以通过特定的坐标对来定义。(请记住我们在 名为“钉住修订版和操作修订版”的部分 中提到的“钉住修订版”语法 — 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“反向”应用。 (我们已经讨论了如何撤消更改,请参阅 名为“撤销更改”的部分。) 这将使 real.c 作为本地修改重新添加。该文件将被计划添加,并在提交后,该文件将再次存在于 HEAD 中。

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

第二种更针对性的策略是不使用 svn merge 命令,而是使用 svn copy 命令。只需将存储库中的精确修订版本和路径“坐标对”复制到您的工作副本中即可。

$ svn copy -r 807 \
           http://svn.example.com/repos/calc/trunk/real.c ./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 并不是真正新的;它只是原始已删除文件的直接后代。

虽然我们的示例展示了我们如何恢复文件,但请注意,这些相同的技术同样适用于恢复已删除的目录。

常见的分支模式

版本控制最常用于软件开发,因此以下快速介绍了程序员团队使用的两种最常见的分支/合并模式。如果您没有将 Subversion 用于软件开发,您可以跳过本节。如果您是第一次使用版本控制进行软件开发,请密切关注,因为这些模式通常被经验丰富的人员视为最佳实践。这些流程不是特定于 Subversion 的;它们适用于任何版本控制系统。尽管如此,用 Subversion 的术语来描述它们可能会有所帮助。

发布分支

大多数软件都有一个典型的生命周期:编码、测试、发布、重复。此流程存在两个问题。首先,开发人员需要继续编写新功能,而质量保证团队则需要时间来测试看似稳定的软件版本。在测试软件时,新工作无法停止。其次,团队几乎总是需要支持较旧的已发布软件版本;如果在最新代码中发现错误,则该错误很可能也存在于已发布的版本中,客户希望获得该错误修复,而无需等待重大新版本发布。

版本控制可以提供帮助。典型的过程如下所示

  • 开发人员将所有新工作提交到主干。日常更改会提交到 /trunk:新功能、错误修复等。

  • 主干被复制到“发布”分支。当团队认为软件已准备好发布(例如,1.0 版本)时,/trunk 可能会被复制到 /branches/1.0

  • 团队继续并行工作。一个团队开始对发布分支进行严格的测试,而另一个团队则继续进行新的工作(例如,2.0 版本)在 /trunk 上。如果在任一位置发现错误,则需要根据需要将修复程序来回移植。然而,在某些时候,即使该过程也会停止。分支在发布前夕被“冻结”以进行最终测试。

  • 分支被标记并发布。测试完成后,/branches/1.0 将被复制到 /tags/1.0.0 作为参考快照。该标签被打包并发布给客户。

  • 分支随着时间的推移而维护。当 2.0 版本的工作继续进行时,/trunk 上的错误修复会继续从 /trunk 移植到 /branches/1.0。当积累了足够多的错误修复时,管理层可能会决定进行 1.0.1 版本发布:/branches/1.0 被复制到 /tags/1.0.1,该标签被打包并发布。

随着软件的成熟,整个过程会重复:当 2.0 版本的工作完成后,会创建一个新的 2.0 版本发布分支,进行测试、标记,并最终发布。几年后,存储库中最终会出现一些处于“维护”模式的发布分支,以及一些代表最终发布版本的标签。

功能分支

一个 功能分支 是一种分支,它在本章中一直都是主要示例,即您在莎莉继续在 /trunk 上工作时一直使用的一种分支。它是为了进行复杂更改而创建的临时分支,不会影响 /trunk 的稳定性。与发布分支(可能需要永久维护)不同,功能分支有生之年,使用一段时间后会合并回主干,最终被删除。它们的使用寿命是有限的。

同样,项目策略在何时创建功能分支方面存在很大差异。一些项目根本不使用功能分支:对 /trunk 的提交是自由的。这种系统的优势在于简单易行——没有人需要学习分支或合并。缺点是主干代码经常不稳定或不可用。其他项目则将分支使用到极致:任何更改从不直接提交到主干。即使是最微不足道的更改也会在短暂的分支上创建,经过仔细审查后合并到主干中。然后,该分支会被删除。这种系统保证了主干始终保持异常稳定和可用,但代价是巨大的流程开销。

大多数项目采取折中的方法。它们通常坚持要求 /trunk 始终编译并通过回归测试。只有在更改需要大量不稳定提交时才需要功能分支。一个好的经验法则是问自己这个问题:如果开发人员孤立地工作数天,然后一次性提交大型更改(这样 /trunk 永远不会不稳定),那么这是否是一个过于庞大的更改需要审查?如果答案是“”,那么应该在功能分支上进行开发。当开发人员将增量更改提交到分支时,同行可以轻松地审查这些更改。

最后,还有一个问题是如何最好地将功能分支与主干“同步”,因为工作会不断进行。正如我们之前提到的,在分支上工作数周或数月存在很大风险;主干更改可能会源源不断地涌入,以至于这两条开发路线的差异如此之大,以至于尝试将分支合并回主干可能会成为一场噩梦。

最好避免这种情况,方法是定期将主干更改合并到分支中。制定一个策略:每周合并上周的主干更改到分支中。在执行此操作时要小心;合并需要手动跟踪,以避免重复合并问题(如 名为“手动跟踪合并”的部分 中所述)。您需要编写详细的日志消息,其中详细说明已经合并了哪些修订版本范围(如 名为“将整个分支合并到另一个分支”的部分 中所示)。这听起来可能很吓人,但实际上非常容易做到。

在某些时候,您将准备好将“同步”后的功能分支合并回主干。为此,首先将最新的主干更改合并到分支中。完成此操作后,分支和主干的最新版本将完全相同,除了您的分支更改。因此,在这种特殊情况下,您可以通过比较分支与主干来进行合并

$ cd trunk-working-copy

$ svn update
At revision 1910.

$ svn merge http://svn.example.com/repos/calc/trunk@1910 \
            http://svn.example.com/repos/calc/branches/mybranch@1910
U  real.c
U  integer.c
A  newdirectory
A  newdirectory/newfile
…

通过比较主干的 HEAD 修订版本与分支的 HEAD 修订版本,您定义了一个增量,该增量仅描述了您对分支所做的更改;两条开发路线已经包含所有主干更改。

另一种理解这种模式的方式是,您每周将主干同步到分支类似于在工作副本中运行 svn update,而最终的合并步骤类似于从工作副本运行 svn commit。毕竟,工作副本到底是什么?它只是一个非常浅的私有分支。它是一个只能存储一次更改的分支。



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