### 卖主分支 当开发软件时有这样一个情况,你版本控制的数据可能关联于或者是依赖于其他人的数据,通常来讲,你的项目的需要会要求你自己的项目对外部实体提供的数据保持尽可能最新的版本,同时不会牺牲稳定性,这种情况总是会出现―只要某个小组的信息对另一个小组的信息有直接的影响。 举个例子,软件开发者会工作在一个使用第三方库的应用,Subversion恰好是和Apache的Portable Runtime library(见[“Apache可移植运行库”一节])有这样一个关系。Subversion源代码依赖于APR库来实现可移植需求。在Subversion的早期开发阶段,项目紧密地追踪APR的API修改,经常在库代码的“流血的边缘”粘住,现在APR和Subversion都已经成熟了,Subversion只尝试同步APR的经过良好测试的,稳定的API库。 现在,如果你的项目依赖于其他人的信息,有许多方法可以用来尝试同步你的信息,最痛苦的,你可以为项目所有的贡献者发布口头或书写的指导,告诉他们确信他们拥有你们的项目需要的特定版本的第三方信息。如果第三方信息是用Subversion版本库维护,你可以使用Subversion的外部定义来有效的“强制”特定的版本的信息在你的工作拷贝的的位置(见[“外部定义”一节])。 但是有时候,你希望在你自己的版本控制系统维护一个针对第三方数据的自定义修改,回到软件开发的例子,程序员为了他们自己的目的会需要修改第三方库,这些修改会包括新的功能和bug修正,在成为第三方工具官方发布之前,只是内部维护。或者这些修改永远不会传给库的维护者,只是作为满足软件开发需要的单独的自定义修改存在。 现在你会面对一个有趣的情形,你的项目可以用某种脱节的样式保持它关于第三方数据自己的修改,如使用补丁文件或者是完全的可选版本的文件和目录。但是这很快会成为维护的头痛的事情,需要一种机制来应用你对第三方数据的自定义修改,并且迫使在第三方数据的后续版本重建这些修改。 这个问题的解决方案是使用*卖主分支*,一个卖主分支是一个目录树保存了第三方实体或卖主的信息,每一个卖主数据的版本吸收到你的项目叫做*卖主drop*。 卖主分支提供了两个关键的益处,第一,通过在我们的版本控制系统保存现在支持的卖主drop,你项目的成员不需要指导他们是否有了正确版本的卖主数据,他们只需要作为不同工作拷贝更新的一部份,简单的接受正确的版本就可以了。第二,因为数据存在于你自己的Subversion版本库,你可以在恰当的位置保存你的自定义修改―你不需要一个自动的(或者是更坏,手工的)方法来交换你的自定义行为。 ### 常规的卖主分支管理过程 管理卖主分支通常会像这个样子,你创建一个顶级的目录(如`/vendor`)来保存卖主分支,然后你导入第三方的代码到你的子目录。然后你将拷贝这个子目录到主要的开发分支(例如`/trunk`)的适当位置。你一直在你的主要开发分支上做本地修改,当你的追踪的代码有了新版本,你会把带到卖主分支并且把它合并到你的`/trunk`,解决任何你的本地修改和他们的修改的冲突。 也许一个例子有助于我们阐述这个算法,我们会使用这样一个场景,我们的开发团队正在开发一个计算器程序,与一个第三方的复杂数字运算库libcomplex关联。我们从卖主分支的初始创建开始,并且导入卖主drop,我们会把每株分支目录叫做`libcomplex`,我们的代码drop会进入到卖主分支的子目录`current`,并且因为**svn import**创建所有的需要的中间父目录,我们可以使用一个命令完成这一步。 ~~~ $ svn import /path/to/libcomplex-1.0 \ http://svn.example.com/repos/vendor/libcomplex/current \ -m 'importing initial 1.0 vendor drop' … ~~~ 我们现在在`/vendor/libcomplex/current`有了libcomplex当前版本的代码,现在我们为那个版本作标签(见[“标签”一节]),然后拷贝它到主要开发分支,我们的拷贝会在`calc`项目目录创建一个新的目录`libcomplex`,它是这个我们将要进行自定义的卖主数据的拷贝版本。 ~~~ $ svn copy http://svn.example.com/repos/vendor/libcomplex/current \ http://svn.example.com/repos/vendor/libcomplex/1.0 \ -m 'tagging libcomplex-1.0' … $ svn copy http://svn.example.com/repos/vendor/libcomplex/1.0 \ http://svn.example.com/repos/calc/libcomplex \ -m 'bringing libcomplex-1.0 into the main branch' … ~~~ 我们取出我们项目的主分支―现在包括了第一个卖主drop的拷贝―我们开始自定义libcomplex的代码,我们知道,我们的libcomplex修改版本是已经与我们的计算器程序完全集成。 几周之后,libcomplex得开发者发布了一个新的版本―版本1.1―包括了我们很需要的一些特性和功能。我们很希望升级到这个版本,但不希望失去在当前版本所作的修改。我们本质上会希望把我们当前基线版本是的libcomplex1.0的拷贝替换为libcomplex 1.1,然后把前面自定义的修改应用到新的版本。但是实际上我们通过一个相反的方向解决这个问题,应用libcomplex从版本1.0到1.1的修改到我们修改的拷贝。 为了执行这个升级,我们取出一个我们卖主分支的拷贝,替换`current`目录为新的libcomplex 1.1的代码,我们只是拷贝新文件到存在的文件上,或者是解压缩libcomplex 1.1的打包文件到我们存在的文件和目录。此时的目标是让我们的`current`目录只保留libcomplex 1.1的代码,并且保证所有的代码在版本控制之下,哦,我们希望在最小的版本控制历史扰动下完成这件事。 完成了这个从1.0到1.1的代码替换,**svn status**会显示文件的本地修改,或许也包括了一些未版本化或者丢失的文件,如果我们做了我们应该做的事情,未版本化的文件应该都是libcomplex在1.1新引入的文件―我们运行**svn add**来将它们加入到版本控制。丢失的文件是存在于1.1但是不是在1.1,在这些路径我们运行**svn delete**。最终一旦我们的`current`工作拷贝只是包括了libcomplex1.1的代码,我们可以提交这些改变目录和文件的修改。 我们的`current`分支现在保存了新的卖主drop,我们为这个新的版本创建一个新的标签(就像我们为1.0版本drop所作的),然后合并这从个标签前一个版本的区别到主要开发分支。 ~~~ $ cd working-copies/calc $ svn merge http://svn.example.com/repos/vendor/libcomplex/1.0 \ http://svn.example.com/repos/vendor/libcomplex/current \ libcomplex … # resolve all the conflicts between their changes and our changes $ svn commit -m 'merging libcomplex-1.1 into the main branch' … ~~~ 在这个琐碎的用例里,第三方工具的新版本会从一个文件和目录的角度来看,就像前一个版本。没有任何libcomplex源文件会被删除、被改名或是移动到别的位置―新的版本只会保存针对上一个版本的文本修改。在完美世界,我们对呢修改会干净得应用到库的新版本,不会产生任何并发和冲突。 但是事情总不是这样简单,实际上源文件在不同的版本间的移动是很常见的,这种过程复杂性可以确保我们的修改会一直对新的版本代码有效,可以很快使形势退化到我们需要在新版本手工的重新创建我们的自定义修改。一旦Subversion知道了给定文件的历史―包括了所有以前的位置―合并到新版本的进程就会很简单,但是我们需要负责告诉Subversion卖主drop之间源文件布局的改变。 ### **svn_load_dirs.pl** 不仅仅包含一些删除、添加和移动的卖主drops使得升级第三方数据后续版本的过程变得复杂,所以Subversion提供了一个**svn_load_dirs.pl**脚本来辅助这个过程,这个脚本自动进行我们前面提到的常规卖主分支管理过程的导入步骤,从而使得错误最小化。你仍要负责使用合并命令合并第三方的新版本数据合并到主要开发分支,但是**svn_load_dirs.pl**帮助你快速到达这一步骤。 一句话,**svn_load_dirs.pl**是一个增强的**svn import**,具备了许多重要的特性: - 它可以在任何有一个存在的版本库目录与一个外部的目录匹配时执行,会执行所有必要的添加和删除并且可以选则执行移动。 - 它可以用来操作一系列复杂的操作,如那些需要一个中间媒介的提交―如在操作之前重命名一个文件或者目录两次。 - 它可以随意的为新导入目录打上标签。 - 它可以随意为符合正则表达式的文件和目录添加任意的属性。 **svn_load_dirs.pl**利用三个强制的参数,第一个参数是Subversion工作的基本目录URL,第二个参数在URL之后―相对于第一个参数―指向当前的卖主分支将会导入的目录,最后,第三个参数是一个需要导入的本地目录,使用前面的例子,一个典型的**svn_load_dirs.pl**调用看起来如下: ~~~ $ svn_load_dirs.pl http://svn.example.com/repos/vendor/libcomplex \ current \ /path/to/libcomplex-1.1 … ~~~ 你可以说明你会希望**svn_load_dirs.pl**同时打上标签,这使用`-t`命令行选项,需要制定一个标签名。这个标签是第一个参数的一个相对URL。 ~~~ $ svn_load_dirs.pl -t libcomplex-1.1 \ http://svn.example.com/repos/vendor/libcomplex \ current \ /path/to/libcomplex-1.1 … ~~~ 当你运行**svn_load_dirs.pl**,它会检验你的存在的“current”卖主drop,并且与提议的新卖主drop比较,在这个琐碎的例子里,没有文件只出现在一个版本里,脚本执行新的导入而不会发生意外。然而如果版本之间有了文件布局的区别,**svn_load_dirs.pl**会询问你如何解决这个区别,例如你会有机会告诉脚本libcomplex版本1.0的`math.c`文件在1.1已经重命名为`arithmetic.c`,任何没有解释为移动的差异都会被看作是常规的添加和删除。 这个脚本也接受单独配置文件用来为*添加到*版本库的文件和目录设置匹配正则表达式的属性。配置文件通过**svn_load_dirs.pl**的`-p`命令行选项指定,这个配置文件的每一行都是一个空白分割的两列或者四列值:一个Perl样式的正则表达式来匹配添加的路径、一个控制关键字(`break`或者是`cont`)和可选的属性名和值。 ~~~ \.png$ break svn:mime-type image/png \.jpeg$ break svn:mime-type image/jpeg \.m3u$ cont svn:mime-type audio/x-mpegurl \.m3u$ break svn:eol-style LF .* break svn:eol-style native ~~~ 对每一个添加的路径,会按照顺序为匹配正则表达式的文件配置属性,除非控制标志是`break`(意味着不需要更多的路径匹配应用到这个路径)。如果控制说明是`cont`―`continue`的缩写―然后匹配工作会继续到配置文件的下一行。 任何正则表达式,属性名或者属性值的空格必须使用单引号或者双银行环绕,你可以使用反斜杠(`\`)换码符来回避引号,反斜杠只会在解析配置文件时回避引号,所以不要保护对正则表达式不需要的其它字符。 而且完全没有bug,当然!