常见的用例

分支和svn merge命令有很多不同的用途,本节描述那些你可能碰到的最常用的用法。

把整个分支合并到另一个

为了完成我们的例子,我们要向前一点时间。假设几天已经过去了,在主线和你的私有分支上都有了很多修改。 假设有已经完成了在你私有分支上的工作,新特性或者bug修正终于完成了,现在你想把你的分支中所有的修改都合并回 主线以便别人欣赏。

那么在这个场景下我们应怎样使用svn merge?要记得这个命令比较两个树并把差异应用到工作副本。 因此要接收修改,你应该有主线的工作副本。我们假设或者你已经有一个在那儿(完全更新了),或者你刚刚检出了一个 /calc/trunk的新的工作副本。

但是应该比较那两个树呢?初看起来,答案似乎很明显:就比较最新的主线树和你最新的分支树。但是小心—— 这个假设是错误的,已经欺骗了很多新用户!既然 svn merge 操作起来像svn diff, 比较最新的主线和分支树将仅仅描述你对你的分支作的那些修改。这样的比较会显示太多的修改:它将不仅 显示添加了你的修改,也会显示除去了主线中那些没有发生在你的分支的修改。

为了只表达发生在你的分支中的修改,你需要比较你的分支的初始状态和最终状态。在你的分支上使用svn log, 你可以看到你的分支是在修订版341创建的。你的分支最终的状态可以简单的用修订版HEAD表示。这意味着你 要比较修订版341和你的分支目录的HEAD,并把这些差别应用到主线的工作副本。

提示

发现分支是在哪个修订版创建(分支的“base”)的好办法是使用带--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:HEAD 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.

此外要注意提交日志消息中特别提到被合并到主线中的修改的范围。始终记着要这么做,因为这是你将来会用的到的关键信息。

例如,设想你决定继续在你的分支上工作一个星期,来完成你的新特性的改进或修正bug。现在资料库的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。如果某人检出calc 项目在修订版303和349之间的某个版本,他们仍然将看到哪个错误的修改,对吧?

是的,这是对的。当我们谈到“除去”一个修改,我们实际上在说从HEAD中除去它。 原来的修改仍然存在于资料库的历史中。在大部分情况下,这足够好了。毕竟大部分人只对跟踪HEAD有兴趣。 但也有些特殊情况,你确实想消除所有提交的痕迹(可能某人不小心提交了一个保密文档)。事实表明这不那么容易,因为 Subversion特意设计成从不丢失信息。修订版是互相依赖的不可修改的树。除去一个修订版会导致多米诺效应,对所有后续的 修订版造成混乱,还可能使所有的工作副本无效。 [8]

恢复删掉的项

版本控制系统的一个重大的特性是信息永不会丢失。甚至在你删除了一个文件或目录时,它可能从HEAD 中去掉了,但是这个对象仍然存在于以前的修订版里。一个新用户最常问的问题之一是:“我怎么才能把我的旧 文件或目录弄回来?

第一步是定义究竟哪个是你要恢复的项。这有个有用的隐喻:你可以把资料库中的每一个对象想象为 存在于一种两维坐标系统中。第一个坐标是一个特定的修订版树,第二个坐标是这个树中的一个路径。 因此你文件或目录的每个修订版都可以用一个特定的坐标点来定义。

Subversion 没有像CVS那样的Attic目录。 [9] 因此你需要使用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的术语来描述有助于理解它们。

发行版分支

大部分软件有一个典型的生命周期:编码,测试,发现,然后重复这个过程。这个过程有两个问题。 首先,在质量保证团队在测试假定为稳定的软件版本时,开发人员需要继续写新的特性。在软件被测试时,新的开发不能停止。 第二,团队几乎总是需要支持软件的旧的,发行了的版本;如果在最新的代码里发现了一个bug,它很可能也存在于发行了的版本中, 客户会想得到修正而不用等新的主版本发布。

这是版本控制能帮忙的地方。典型的过程像这样:

  • 开发人员把所有的新工作提交到主线。 日常的修改被提交到/trunk:新特性,bug修正等等。

  • 主线被复制到“发行版”分支。 当团队认为软件可以发布了(比如,1.0版),那么/trunk可以被复制到/branches/1.0

  • 团队继续并行的工作着。 一个团队开始严格测试发行版分支,同时另一个团队在/trunk上继续新的工作(比如,开发2.0版)。 如果某个位置发现了bug,必要时修正可以在分支之间来回移植。但有时,甚至这样的过程也要停下来。分支被“冻结”以在一次 发布前作最后的测试。

  • 分支被加标签并发布 当测试完成时,/branches/1.0被复制到/tags/1.0.0作为一个供参考的快照。 这个tag被打包并发布给客户。

  • 分支被维护着/trunk上2.0版的工作进行时,bug修正继续从/trunk移植到 /branches/1.0。当积累了足够多的bug修正后,管理层可能决定做一个1.0.1发行版: /branches/1.0被复制到/tags/1.0.1,然后tag被打包并发布。

随着软件的成熟,整个过程不断重复:当2.0的工作完成,一个新的2.0发行版分支被创建,测试,加标签,并最终发布。 几年后,资料库结束于这样的状态:有很多发行版分支处于“维护”模式,和很多代表最终发布的版本。

特性分支

特性分支(feature branch)就是在本章中作为主要例子的那种分支, 那个当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类比。毕竟,一个工作副本和一个非常短命的私有分支什么区别呢? 它是一个仅仅能在某时保存一个修改的分支。



[8] 不过,Subversion项目已经计划在将来实现一个svnadmin obliterate,来 完成永久删除信息的任务。同时,可参考“svndumpfilter”一节寻找可能的办法。

[9] 因为CVS不版本化树,它在每个资料库目录中创建Attic作为记住删除文件的办法。