本文档描述了 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 status 和 svn 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 revert 对 integer.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。毕竟,工作副本到底是什么?它只是一个非常浅的私有分支。它是一个只能存储一次更改的分支。