本文档旨在描述 Subversion 1.1。如果您正在运行更新版本的 Subversion,我们强烈建议您访问https://svnbooks.subversion.org.cn/ 并查阅适合您 Subversion 版本的版本。
分支和 svn merge 有很多不同的用途,本节将描述您可能会遇到的最常见的用途。
为了完成我们正在进行的示例,我们将继续前进。假设已经过了几天,主干和您的私有分支都发生了很多变化。假设您已经完成了对私有分支的工作;功能或错误修复终于完成了,现在您想将所有分支更改合并回主干,以便其他人可以享用。
那么,在这种情况下我们如何使用 svn merge 呢?请记住,此命令比较两个树,并将差异应用于工作副本。因此,要接收更改,您需要拥有主干的工作副本。我们假设您仍然拥有您最初的工作副本(已完全更新),或者您最近检出了主干的新工作副本/calc/trunk.
但是应该比较哪两个树呢?乍一看,答案似乎很明显:只需将最新的主干树与最新的分支树进行比较。但要注意——这种假设是 错误的,并且已经困扰了许多新用户!由于 svn merge 的工作方式类似于 svn diff,因此比较最新的主干和分支树 不会仅仅描述您对分支所做的更改集。这种比较显示了太多更改:它不仅会显示分支更改的添加,还会显示从未在分支上发生的 删除 主干更改。
要仅表达分支上发生的更改,您需要将分支的初始状态与其最终状态进行比较。使用 svn log 在您的分支上,您可以看到您的分支是在修订版 341 中创建的。并且分支的最终状态只需使用HEAD修订版。这意味着您要比较修订版 341 和HEAD您的分支目录,并将这些差异应用于主干的工作副本。
找到创建分支的修订版(分支的“基础”)的一种好方法是使用--stop-on-copy选项到 svn log。log 子命令通常会显示对分支所做的所有更改,包括跟踪回创建分支的副本。因此通常,您也会看到来自主干的历史记录。这--stop-on-copy将停止 log 输出,只要 svn log 检测到其目标已被复制或重命名。
因此,在我们继续的示例中,
$ svn log --verbose --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 在您的工作副本中“撤消”更改,然后将本地修改提交到存储库。您需要做的就是指定一个 反向 差异
$ svn merge -r 303:302 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 故意设计为永远不会丢失信息。修订版是彼此建立的不变树。从历史记录中删除修订版会导致多米诺骨牌效应,在所有后续修订版中造成混乱,并可能使所有工作副本失效。[9]
版本控制系统最棒的地方在于信息永远不会丢失。即使您删除了文件或目录,它可能已从HEAD修订版中消失,但该对象仍然存在于早期修订版中。新用户问的最常见问题之一是,“我如何找回我的旧文件或目录?”
第一步是准确定义您要恢复的 哪个 项目。以下是一个有用的比喻:您可以将存储库中的每个对象都想象成存在于一种二维坐标系中。第一个坐标是特定的修订版树,第二个坐标是该树中的路径。因此,文件的每个版本或目录都可以通过特定的坐标对来定义。
Subversion 没有Attic目录像 CVS 一样,[10] 所以您需要使用 svn log 来发现您要恢复的确切坐标对。一个好的策略是在曾经包含您已删除项目的目录中运行 svn log --verbose。这--verbose选项显示每个修订版中所有更改项目的列表;您需要做的就是找到您删除文件或目录的修订版。您可以通过视觉方式进行操作,也可以使用其他工具(通过 grep,或者可能通过编辑器中的增量搜索)来检查日志输出。
$ cd parent-dir $ svn log --verbose … ------------------------------------------------------------------------ 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 中删除的。因此,文件存在的最后一个版本是在之前的修订版中。结论:您想要恢复路径/calc/trunk/real.c从修订版 807。
那是最难的部分——研究。现在您知道了要恢复的内容,您有两个不同的选择。
一个选项是使用 svn merge 来“反向”应用修订版 808。(我们已经讨论了如何撤消更改,请参见名为“撤消更改”的部分。)这将有重新添加的效果real.c作为本地修改。该文件将被安排添加,并且在提交后,该文件将再次存在于HEAD.
但是,在这个特定的例子中,这可能不是最好的策略。反向应用修订版 808 不仅会安排real.c进行添加,但日志消息表明它还会撤消对integer.c的某些更改,而您不希望这样做。当然,您可以反向合并修订版 808 然后 svn revert 对integer.c的本地修改,但这种技术扩展性不好。如果修订版 808 中更改了 90 个文件怎么办?
第二种更有针对性的策略是根本不使用 svn merge,而是使用 svn copy 命令。只需将存储库中的确切修订版和路径“坐标对”复制到您的工作副本中
$ svn copy --revision 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.
团队继续并行工作。 一个团队开始对发布分支进行严格测试,而另一个团队继续在/trunk上进行新工作(例如,2.0 版)。如果在任一位置发现了错误,则根据需要将修复程序移植到各个位置。但是,在某个时间点,即使该过程也会停止。该分支在发布前夕被“冻结”以进行最终测试。
该分支被标记并发布。 测试完成后,/branches/1.0被复制到/tags/1.0.0作为参考快照。该标签被打包并发布给客户。
该分支随着时间的推移而得到维护。 虽然工作继续进行/trunk用于 2.0 版,错误修复继续从/trunk移植到/branches/1.0。当积累了足够多的错误修复时,管理层可能会决定进行 1.0.1 版发布/branches/1.0被复制到/tags/1.0.1,该标签被打包并发布。
随着软件的成熟,整个过程会重复:当 2.0 版工作完成时,会创建一个新的 2.0 版发布分支,进行测试、标记,最终发布。几年后,存储库中最终会有几个处于“维护”模式的发布分支,以及几个表示最终发布版本的标签。
一个 功能分支 是一种分支,它是本章中占主导地位的示例,是您在 Sally 继续在/trunk上工作时一直在使用的一种分支。它是一个临时分支,用于在不影响/trunk的稳定性的情况下处理复杂变更。与发布分支(可能需要永久支持)不同,功能分支是诞生的,使用一段时间后,合并回主干,最终被删除。它们的使用范围是有限的。
同样,项目策略在何时创建功能分支方面差异很大。一些项目根本不使用功能分支:提交到/trunk是自由的。这种系统的优点是简单——没有人需要学习分支或合并。缺点是主干代码通常不稳定或不可用。其他项目则将分支使用到极致:任何变更都不会直接提交到主干。即使是最微不足道的变更也会在短暂的分支上创建,仔细审查并合并到主干。然后删除该分支。这种系统保证主干始终保持异常稳定和可用,但代价是巨大的流程开销。
大多数项目采用折中方案。他们通常坚持要求/trunk始终编译并通过回归测试。只有当变更需要大量不稳定提交时,才需要功能分支。一个好的经验法则是问自己这个问题:如果开发人员在孤立环境中工作了几天,然后一次性提交了所有变更(这样/trunk永远不会变得不稳定),那么这是否是一个太大的变更以至于无法审查?如果问题的答案是“是”,那么变更应该在功能分支上进行开发。随着开发人员将增量变更提交到分支,同行可以轻松地审查这些变更。
最后,还有如何最好地将功能分支与主干保持“同步”的问题,因为工作在不断进行。正如我们之前提到的,在分支上工作数周或数月存在很大风险;主干变更可能会不断涌入,以至于两条开发线之间的差异如此之大,以至于将分支合并回主干可能会变成一场噩梦。
这种情况最好通过定期将主干变更合并到分支来避免。制定一个策略:每周一次,将过去一周的主干变更合并到分支中。在执行此操作时要小心;需要手动跟踪合并,以避免重复合并问题(如
At some point, you'll be ready to merge the “synchronized” feature branch back to the trunk. To do this, begin by doing a final merge of the latest trunk changes to the branch. When that's done, the latest versions of branch and trunk will be absolutely identical except for your branch changes. So in this special case, you would merge by comparing the branch with the 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 …
By comparing theHEADrevision of the trunk with theHEADrevision of the branch, you're defining a delta that describes only the changes you made to the branch; both lines of development already have all of the trunk changes.
Another way of thinking about this pattern is that your weekly sync of trunk to branch is analogous to running svn update in a working copy, while the final merge step is analogous to running svn commit from a working copy. After all, what else is a working copy but a very shallow private branch? It's a branch that's only capable of storing one change at a time.
[9] The Subversion project has plans, however, to someday implement an svnadmin obliterate command that would accomplish the task of permanently deleting information. In the meantime, see the section called “svndumpfilter” for a possible workaround.
[10] Because CVS doesn't version trees, it creates anAtticarea within each repository directory as a way of remembering deleted files.