这份文档是为了描述 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 statussvn 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 revertinteger.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 区域来记住已删除的文件。

TortoiseSVN 官方中文版 1.14.7 发布