### 常见用例 分支和**svn merge**有很多不同的用法,这个小节描述了最常见的用法。 ### 合并一条分支到另一支 为了完成这个例子,我们将时间往前推进,假定已经过了几天,在主干和你的分支上都有许多更改,假定你完成了分支上的工作,已经完成了特性或bug修正,你想合并所有分支的修改到主干上,让别人也可以使用。 这种情况下如何使用**svn merge**?记住这个命令比较两个目录树,然后应用比较结果到工作拷贝,所以要接受这种变化,你需要主干的工作拷贝,我们假设你有一个最初的主干工作拷贝(完全更新),或者是你最近取出了`/calc/trunk`的一个干净的工作拷贝。 但是要哪两个树进行比较呢?乍一看,回答很明确,只要比较最新的主干与分支。但是你要意识到―这个想法是*错误的*,伤害了许多新用户!因为**svn merge**的操作很像**svn diff**,比较最新的主干和分支树不仅仅会描述你在分支上所作的修改,这样的比较会展示太多的不同,不仅包括分支上的增加,也包括了主干上的删除操作,而这些删除根本就没有在分支上发生过。 为了表示你的分支上的修改,你只需要比较分支的初始状态与最终状态,在你的分支上使用**svn log**命令,你可以看到你的分支在341版本建立,你的分支最终的状态用`HEAD`版本表示,这意味着你希望能够比较版本341和`HEAD`的分支目录,然后应用这些分支的修改到主干目录的工作拷贝。 ### 提示 查找分支产生的版本(分支的“基准”)的最好方法是在**svn log**中使用`--stop-on-copy`选项,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还依然存在着修改,如果任何人取出`calc`的303-349版本,他还会得到错误的修改,对吧? 是的,这是对的。当我们说“删除”一个修改时,我们只是说从`HEAD`删除,原始的修改还保存在版本库历史中,在多数情况下,这是足够好的。大多数人只是对追踪`HEAD`版本感兴趣,在一些特定情况下,你也许希望毁掉所有提交的证据(或许某个人提交了一个秘密文件),这不是很容易的,因为Subversion设计用来不丢失任何信息,每个修订版本都是不可变的目录树 ,从历史删除一个版本会导致多米诺效应,会在后面的版本导致混乱甚至会影响所有的工作拷贝。 ### 找回删除的项目 版本控制系统非常重要的一个特性就是它的信息从不丢失,即使当你删除了文件或目录,它也许从HEAD版本消失了 ,但这个对象依然存在于历史的早期版本 ,一个新手经常问到的问题是“怎样找回我的文件和目录?” 第一步首先要知道需要拯救的项目是**什么**,这里有个很有用的比喻:你可以认为任何存在于版本库的对象生活在一个二维的坐标系统里,第一维是一个特定的版本树,第二维是在树中的路径,所以你的文件或目录的任何版本可以有这样一对坐标定义。 Subversion没有向CVS一样的`古典`目录, 所以你需要**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`。 在这个例子里,这不是一个好的策略,这样做不仅把`real.c`加入添加到计划,也取消了对`integer.c`的修改,而这不是你期望的。确实,你可以恢复到版本808,然后对`integer.c`执行取消**svn revert**操作,但这样的操作无法扩大使用,因为如果从版本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`,这个标签被打包发布给客户。 - *分支多次维护。*当继续在`/trunk`上为版本2.0工作,bug修正继续从`/trunk`运送到`/branches/1.0`,如果积累了足够的bug修正,管理部门决定发布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**,毕竟,工作拷贝不就是一个非常浅的分支吗?只是它一次只可以保存一个修改。 Subversion项目有计划,不管用什么方式,总有一天要实现**svnadmin obliterate**命令来进行永久删除操作,而此时可以看[“svndumpfilter”一节]( "svndumpfilter")。 因为CVS没有版本树,它会在每个版本库目录创建一个`古典`区域用来保存增量数据。