ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
### 在分支间拷贝修改 现在你与Sally在同一个项目的并行分支上工作:你在私有分支上,而Sally在主干(*trunk*)或者叫做开发主线上。 由于有众多的人参与项目,大多数人拥有主干拷贝是很正常的,任何人如果进行一个长周期的修改会使得主干陷入混乱,所以通常的做法是建立一个私有分支,提交修改到自己的分支,直到这阶段工作结束。 所以,好消息就是你和Sally不会互相打扰,坏消息是有时候分离会*太*远。记住“闭门造车”策略的问题,当你完成你的分支后,可能因为太多冲突,已经无法轻易合并你的分支和主干的修改。 相反,在你工作的时候你和Sally仍然可以继续分享修改,这依赖于你决定什么值得分享,Subversion给你在分支间选择性“拷贝”修改的能力,当你完成了分支上的所有工作,所有的分支修改可以被拷贝回到主干。 ### 拷贝特定的修改 在上一章节,我们提到你和Sally对`integer.c`在不同的分支上做过修改,如果你看了Sally的344版本的日志信息,你会知道她修正了一些拼写错误,毋庸置疑,你的拷贝的文件也一定存在这些拼写错误,所以你以后的对这个文件修改也会保留这些拼写错误,所以你会在将来合并时得到许多冲突。最好是现在接收Sally的修改,而不是作了许多工作之后才来做。 是时间使用**svn merge**命令,这个命令的结果非常类似**svn diff**命令(在第3章的内容),两个命令都可以比较版本库中的任何两个对象并且描述其区别,举个例子,你可以使用**svn diff**来查看Sally在版本344作的修改: ~~~ $ svn diff -r 343:344 http://svn.example.com/repos/calc/trunk Index: integer.c =================================================================== --- integer.c (revision 343) +++ integer.c (revision 344) @@ -147,7 +147,7 @@ case 6: sprintf(info->operating_system, "HPFS (OS/2 or NT)"); break; case 7: sprintf(info->operating_system, "Macintosh"); break; case 8: sprintf(info->operating_system, "Z-System"); break; - case 9: sprintf(info->operating_system, "CPM"); break; + case 9: sprintf(info->operating_system, "CP/M"); break; case 10: sprintf(info->operating_system, "TOPS-20"); break; case 11: sprintf(info->operating_system, "NTFS (Windows NT)"); break; case 12: sprintf(info->operating_system, "QDOS"); break; @@ -164,7 +164,7 @@ low = (unsigned short) read_byte(gzfile); /* read LSB */ high = (unsigned short) read_byte(gzfile); /* read MSB */ high = high << 8; /* interpret MSB correctly */ - total = low + high; /* add them togethe for correct total */ + total = low + high; /* add them together for correct total */ info->extra_header = (unsigned char *) my_malloc(total); fread(info->extra_header, total, 1, gzfile); @@ -241,7 +241,7 @@ Store the offset with ftell() ! */ if ((info->data_offset = ftell(gzfile))== -1) { - printf("error: ftell() retturned -1.\n"); + printf("error: ftell() returned -1.\n"); exit(1); } @@ -249,7 +249,7 @@ printf("I believe start of compressed data is %u\n", info->data_offset); #endif - /* Set postion eight bytes from the end of the file. */ + /* Set position eight bytes from the end of the file. */ if (fseek(gzfile, -8, SEEK_END)) { printf("error: fseek() returned non-zero\n"); ~~~ **svn merge**命令几乎完全相同,但不是打印区别到你的终端,它会直接作为*本地修改*作用到你的本地拷贝: ~~~ $ svn merge -r 343:344 http://svn.example.com/repos/calc/trunk U integer.c $ svn status M integer.c ~~~ **svn merge**的输出告诉你的`integer.c`文件已经作了补丁(patched),现在已经保留了Sally修改―修改从主干“拷贝”到你的私有分支的工作拷贝,现在作为一个本地修改,在这种情况下,要靠你审查本地的修改来确定它们工作正常。 在另一种情境下,事情并不会运行得这样正常,也许`integer.c`也许会进入冲突状态,你必须使用标准过程(见第三章)来解决这种状态,或者你认为合并是一个错误的决定,你只需要运行**svn revert**放弃。 但是当你审查过你的合并结果后,你可以使用**svn commit**提交修改,在那一刻,修改已经合并到你的分支上了,在版本控制术语中,这种在分支之间拷贝修改的行为叫做*搬运*修改。 当你提交你的修改时,确定你的日志信息中说明你是从某一版本搬运了修改,举个例子: ~~~ $ svn commit -m "integer.c: ported r344 (spelling fixes) from trunk." Sending integer.c Transmitting file data . Committed revision 360. ~~~ 你将会在下一节看到,这是一条非常重要的“最佳实践”。 **为什么不使用补丁?** 也许你的脑中会出现一个问题,特别如果你是Unix用户,为什么非要使用**svn merge**?为什么不简单的使用操作系统的**patch**命令来进行相同的工作?举个例子: ~~~ $ svn diff -r 343:344 http://svn.example.com/repos/calc/trunk > patchfile $ patch -p0 < patchfile Patching file integer.c using Plan A... Hunk #1 succeeded at 147. Hunk #2 succeeded at 164. Hunk #3 succeeded at 241. Hunk #4 succeeded at 249. done ~~~ 在这种情况下,确实没有区别,但是**svn merge**有超越**patch**的特别能力,使用**patch**对文件格式有一定的限制,它只能针对文件内容,没有方法表现*目录树*的修改,例如添加、删除或是改名。如果Sally的修改包括增加一个新的目录,**svn diff**不会注意到这些,**svn diff**只会输出有限的补丁格式,所以有些问题无法表达。 但是**svn merge**命令会通过直接作用你的工作拷贝来表示目录树的修改。 一个警告:为什么**svn diff**和**svn merge**在概念上是很接近,但语法上有许多不同,一定阅读第9章来查看其细节或者使用**svn help**查看帮助。举个例子,**svn merge**需要一个工作拷贝作为目标,就是一个地方来施展目录树修改,如果一个目标都没有指定,它会假定你要做以下某个普通的操作: 1. 你希望合并目录修改到工作拷贝的当前目录。 1. 你希望合并修改到你的当前工作目录的相同文件名的文件。 如果你合并一个目录而没有指定特定的目标,**svn merge**假定第一种情况,在你的当前目录应用修改。如果你合并一个文件,而这个文件(或是一个有相同的名字文件)在你的当前工作目录存在,**svn merge**假定第二种情况,你想对这个同名文件使用合并。 如果你希望修改应用到别的目录,你需要说出来。举个例子,你在工作拷贝的父目录,你需要指定目标目录: ~~~ $ svn merge -r 343:344 http://svn.example.com/repos/calc/trunk my-calc-branch U my-calc-branch/integer.c ~~~ ### 合并背后的关键概念 你已经看到了**svn merge**命令的例子,你将会看到更多,如果你对合并是如何工作的感到迷惑,这并不奇怪,很多人和你一样。许多新用户(特别是对版本控制很陌生的用户)会对这个命令的正确语法感到不知所措,不知道怎样和什么时候使用这个特性,不要害怕,这个命令实际上比你想象的简单!有一个简单的技巧来帮助你理解**svn merge**的行为。 迷惑的主要原因是这个命令的*名称*,术语“合并”不知什么原因被用来表明分支的组合,或者是其他什么神奇的数据混合,这不是事实,一个更好的名称应该是**svn diff-and-apply**,这是发生的所有事件:首先两个版本库树比较,然后将区别应用到本地拷贝。 这个命令包括三个参数: 1. 初始的版本树(通常叫做比较的*左边*), 1. 最终的版本树(通常叫做比较的*右边*), 1. 一个接收区别的工作拷贝(通常叫做合并的*目标*)。 一旦这三个参数指定以后,两个目录树将要做比较,比较结果将会作为本地修改应用到目标工作拷贝,当命令结束后,结果同你手工修改或者是使用**svn add**或**svn delete**没有什么区别,如果你喜欢这结果,你可以提交,如果不喜欢,你可以使用**svn revert**恢复修改。 **svn merge**的语法允许非常灵活的指定参数,如下是一些例子: ~~~ $ svn merge http://svn.example.com/repos/branch1@150 \ http://svn.example.com/repos/branch2@212 \ my-working-copy $ svn merge -r 100:200 http://svn.example.com/repos/trunk my-working-copy $ svn merge -r 100:200 http://svn.example.com/repos/trunk ~~~ 第一种语法使用*URL@REV*的形式直接列出了所有参数,第二种语法可以用来作为比较同一个URL的不同版本的简略写法,最后一种语法表示工作拷贝是可选的,如果省略,默认是当前目录。 ### 合并的最佳实践 #### 手工追踪合并 合并修改听起来很简单,但是实践起来会是很头痛的事,如果你重复合并两个分支,你也许会合并*两次*同样的修改。当这种事情发生时,有时候事情会依然正常,当对文件打补丁时,Subversion如果注意到这个文件已经有了相应的修改,而不会作任何操作,但是如果已经应用的修改又被修改了,你会得到冲突。 理想情况下,你的版本控制系统应该会阻止对一个分支做两次改变操作,必须自动的记住那一个分支的修改已经接收了,并且可以显示出来,用来尽可能帮助自动化的合并。 不幸的是,Subversion不是这样一个系统,类似于CVS,Subversion并不记录任何合并操作,当你提交本地修改,版本库并不能判断出你是通过**svn merge**还是手工修改得到这些文件。 这对你这样的用户意味着什么?这意味着除非Subversion以后发展这个特性,你必须手工的记录这些信息。最佳的方式是使用提交日志信息,像前面的例子提到的,推荐你在日志信息中说明合并的特定版本号(或是版本号的范围),之后,你可以运行**svn log**来查看你的分支包含哪些修改。这可以帮助你小心的依序运行**svn merge**命令而不会进行多余的合并。 在下一小节,我们要展示一些这种技巧的例子。 #### 预览合并 因为合并只是导致本地修改,它不是一个高风险的操作,如果你在第一次操作错误,你可以运行**svn revert**来再试一次。 有时候你的工作拷贝很可能已经改变了,合并会针对存在的那一个文件,这时运行**svn revert**不会恢复你在本地作的修改,两部分的修改无法识别出来。 在这个情况下,人们很乐意能够在合并之前预测一下,一个简单的方法是使用运行**svn merge**同样的参数运行**svn diff**,另一种方式是传递`--dry-run`选项给merge命令: ~~~ $ svn merge --dry-run -r 343:344 http://svn.example.com/repos/calc/trunk U integer.c $ svn status # nothing printed, working copy is still unchanged. ~~~ `--dry-run`选项实际上并不修改本地拷贝,它只是显示实际合并时的状态信息,对于得到“整体”的印象,这个命令很有用,因为**svn diff**包括太多细节。 **Subversion与修改集** 每一个人对于“修改集”的概念都有些不一样,至少对于版本控制系统的“修改集特性”这一概念有着不同的期望,根据我们的用途,可以说修改集只是一个有唯一名字的一系列修改集合,修改也许包括文件内容的修改,目录树结构的修改,或是元数据的调整,更通常的说法,一个修改集就是我们可以引用的有名字的补丁。 在Subversion里,一个全局的修订版本号N标示一个版本库中的树:它代表版本库在N次提交后的样子,它也是一个修改集的隐含名称:如果你比较树N与树N-1,你可以得到你提交的补丁。出于这个原因,想象“版本N”并不只是一棵树,也是一个修改集。如果你使用一个问题追踪工具来管理bug,你可以使用版本号来表示特定的补丁修正了bug―举个例子,“这个问题是在版本9238修正的”,然后其他人可以运行**svn log -r9238**来查看修正这个bug的修改集,或者使用**svn diff -r9237:9238**来看补丁本身。Subversion合并命令也使用版本号作为参数,可以将特定修改集从一个分支合到另一个分支:**svn merge -r9237:9238**将会合并修改集#9238到本地拷贝。 #### 合并冲突 就像**svn update**命令,**svn merge**会把修改应用到工作拷贝,因此它也会造成冲突,因为**svn merge**造成的冲突有时候会有些不同,本小节会解释这些区别。 作为开始,我们假定本地没有修改,当你**svn update**到一个特定修订版本时,修改会“干净的”应用到工作拷贝,服务器产生比较两树的增量数据:一个工作拷贝和你关注的版本树的虚拟快照,因为比较的左边同你拥有的完全相同,增量数据确保你把工作拷贝转化到右边的树。 但是**svn merge**没有这样的保证,会导致很多的混乱:用户可以询问服务器比较*任何*两个树,即使一个与工作拷贝毫不相关的!这意味着有潜在的人为错误,用户有时候会比较两个错误的树,创建的增量数据不会干净的应用,**svn merge**会尽力应用更多的增量数据,但是有一些部分也许会难以完成,就像Unix下**patch**命令有时候会报告“failed hunks”错误,**svn merge**会报告“skipped targets”: ~~~ $ svn merge -r 1288:1351 http://svn.example.com/repos/branch U foo.c U bar.c Skipped missing target: 'baz.c' U glub.c C glorb.h $ ~~~ 在前一个例子中,`baz.c`也许会存在于比较的两个分支快照里,但工作拷贝里不存在,比较的增量数据要应用到这个文件,这种情况下会发生什么?“skipped”信息意味着用户可能是在比较错误的两棵树,这是经典的驱动器错误,当发生这种情况,可以使用迭代恢复(**svn revert --recursive**)合并所作的修改,删除恢复后留下的所有未版本化的文件和目录,并且使用另外的参数运行**svn merge**。 也应当注意前一个例子显示`glorb.h`发生了冲突,我们已经规定本地拷贝没有修改:冲突怎么会发生呢?因为用户可以使用**svn merge**将过去的任何变化应用到当前工作拷贝,变化包含的文本修改也许并不能干净的应用到工作拷贝文件,即使这些文件没有本地修改。 另一个**svn update**和**svn merge**的小区别是冲突产生的文件的名字不同,在[“解决冲突(合并别人的修改)”一节]( "解决冲突(合并别人的修改)"),我们看到过更新产生的文件名字为`filename.mine`、`filename.rOLDREV`和`filename.rNEWREV`,当**svn merge**产生冲突时,它产生的三个文件分别为 `filename.working`、`filename.left`和`filename.right`。在这种情况下,术语“left”和“right”表示了两棵树比较时的两边,在两种情况下,不同的名字会帮助你区分冲突是因为更新造成的还是合并造成的。 #### 关注还是忽视祖先 当与Subversion开发者交谈时你一定会听到提及术语*祖先*,这个词是用来描述两个对象的关系:如果他们互相关联,一个对象就是另一个的祖先,或者相反。 举个例子,假设你提交版本100,包括对`foo.c`的修改,则foo.c@99是foo.c@100的一个“祖先”,另一方面,假设你在版本101删除这个文件,而在102版本提交一个同名的文件,在这个情况下,`foo.c@99`与`foo.c@102`看起来是关联的(有同样的路径),但是事实上他们是完全不同的对象,它们并不共享同一个历史或者说“祖先”。 指出**svn diff**和**svn merge**区别的重要性在于,前一个命令忽略祖先,如果你询问**svn diff**来比较文件`foo.c`的版本99和102,你会看到行为基础的区别,区别命令只是盲目的比较两条路径,但是如果你使用**svn merge**是比较同样的两个对象,它会注意到他们是不关联的,而且首先尝试删除旧文件,然后添加新文件,你会看到`A foo.c`后面紧跟`D foo.c`。 大多数合并包括比较包括祖先关联的两条树,因此**svn merge**这样运作,然而,你也许会希望合并命令能够比较两个不相关的目录树,举个例子,你有两个目录树分别代表了卖主软件项目的不同版本(见[“卖主分支”一节]( "卖主分支")),如果你使用**svn merge**进行比较,你会看到第一个目录树被删除,而第二个树添加上! 在这个情况下,你只是希望**svn merge**能够做一个以路径为基础的比较,忽略所有文件和目录的关系,增加`--ignore-ancestry`选项会导致命令象**svn diff**一样。(相应的,`--notice-ancestry`选项会使**svn diff**象合并命令一样行事。) 在将来,Subversion项目将会计划(或者发明)一种扩展补丁格式来描述目录树改变。