这份文档是为了描述 Subversion 1.2 而编写的。如果您运行的是更新版本的 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
会在 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 被刻意设计为永远不会丢失信息。修订版是不可变的树,它们相互构建。从历史记录中删除一个修订版会导致连锁反应,在所有后续修订版中造成混乱,并可能使所有工作副本失效。 [10]
版本控制系统的优点是信息永远不会丢失。即使您删除了文件或目录,它可能已从 HEAD
修订版中消失,但该对象仍然存在于早期修订版中。新手用户最常问的问题之一是,“如何找回我的旧文件或目录?”。
第一步是定义您要恢复的 哪个 项目。这里有一个有用的比喻:您可以将仓库中的每个对象视为存在于一种二维坐标系中。第一个坐标是特定修订版树,第二个坐标是该树中的路径。因此,您的文件或目录的每个版本都可以通过一个特定的坐标对来定义。
Subversion 没有像 CVS 那样拥有 Attic
目录, [11] 因此您需要使用 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 中删除的。因此,该文件存在于此之前的修订版中。结论:您想要从修订版 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 --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
就不会变得不稳定),那么该更改是否太大而无法审查?如果答案是“是”,那么更改应该在功能分支上开发。随着开发人员将增量更改提交到分支,同行可以轻松地对其进行审查。
最后,还有一个问题是,如何在工作进行时最好地将功能分支与主干保持“同步”。正如我们之前提到的,在分支上工作数周或数月存在很大风险;主干更改可能会不断涌入,以至于两条开发线之间的差异非常大,以至于尝试将分支合并回主干可能会变成一场噩梦。
这种情况可以通过定期将主干更改合并到分支来避免。制定一项策略:每周一次,将过去一周的主干更改合并到分支中。在执行此操作时要小心;合并需要手动跟踪以避免重复合并问题(如 名为“手动跟踪合并”的部分 中所述)。您需要编写详细说明已合并的哪些修订范围的日志消息(如 名为“将整个分支合并到另一个分支”的部分 中所示)。听起来可能很吓人,但实际上很容易做到。
在某个时候,您将准备好将“同步”功能分支合并回主干。为此,首先将最新的主干更改合并到分支中。完成后,分支和主干的最新版本将完全相同,除了您对分支所做的更改。因此,在这种特殊情况下,您将通过比较分支和主干来进行合并
$ 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。毕竟,工作副本是什么 是 一个非常浅层的私有分支?它是一个分支,只能存储一次更改。
[10] 但是,Subversion 项目计划在将来实现一个 svnadmin obliterate 命令来完成永久删除信息的任务。在此期间,请参阅 名为“svndumpfilter”的部分,了解可能的解决方法。
[11] 由于 CVS 不对树进行版本控制,因此它在每个存储库目录中创建了一个 Attic
区域来记住已删除的文件。