本文档用于描述 Subversion 1.4。如果您正在运行更新版本的 Subversion,我们强烈建议您访问 https://svnbook.subversion.org.cn/ 并查阅适合您 Subversion 版本的书籍。
尤其是在软件开发中,您在版本控制下维护的数据通常与他人的数据密切相关,或者可能依赖于他人的数据。通常,您的项目需求将决定您需要尽可能及时地更新外部实体提供的数据,而不会影响您自己项目的稳定性。这种情况一直都在发生——在任何一个群体生成的信息直接影响另一个群体生成的信息的地方。
例如,软件开发人员可能正在开发一个使用第三方库的应用程序。Subversion 与 Apache Portable Runtime 库(参见 名为“Apache Portable Runtime 库”的部分)就有这样的关系。Subversion 源代码依赖于 APR 库来满足其所有可移植性需求。在 Subversion 开发的早期阶段,该项目紧密跟踪 APR 变化的 API,始终坚持使用库代码变更的“前沿”。现在 APR 和 Subversion 都已经成熟,Subversion 尝试仅在经过充分测试的稳定发布点与 APR 库 API 同步。
现在,如果您的项目依赖于他人的信息,您可以尝试使用多种方法将该信息与您自己的信息同步。最痛苦的是,您可以向所有项目的贡献者发出口头或书面指示,告诉他们确保他们拥有项目所需的第三方信息的特定版本。如果第三方信息是在 Subversion 存储库中维护的,您还可以使用 Subversion 的外部定义,有效地将该信息的特定版本“固定”到您自己工作副本目录中的某个位置(参见 名为“外部定义”的部分)。
但是,有时您希望在自己的版本控制系统中维护对第三方数据的自定义修改。回到软件开发示例,程序员可能需要出于自身目的修改第三方库。这些修改可能包括新的功能或错误修复,仅在内部维护,直到它们成为第三方库的官方版本的一部分。或者,这些更改可能永远不会传达给库维护者,仅作为自定义调整而存在,以使库更适合软件开发人员的需求。
现在您面临着一个有趣的情况。您的项目可以以某种不连贯的方式在其自定义的第三方数据修改中容纳,例如使用补丁文件或文件的完整替代版本和目录。但这些很快就会成为维护方面的麻烦,需要一些机制来将您的自定义更改应用于第三方数据,并且需要在您跟踪的第三方数据的每个后续版本中重新生成这些更改。
解决此问题的方案是使用 供应商分支。供应商分支是您自己的版本控制系统中的一个目录树,其中包含第三方实体(或供应商)提供的信息。您决定吸收到项目的每个供应商数据版本都称为 供应商发布。
供应商分支提供两大优势。首先,通过将当前支持的供应商发布存储在您自己的版本控制系统中,您的项目成员无需质疑他们是否拥有供应商数据的正确版本。他们只需通过定期更新工作副本获取该正确版本。其次,由于数据驻留在您自己的 Subversion 存储库中,您可以在原地存储对数据的自定义更改——您不再需要自动(或者更糟的是手动)方法来切换您的自定义更改。
管理供应商分支通常是这样的。您创建一个顶级目录(例如 /vendor
)来保存供应商分支。然后,您将第三方代码导入该顶级目录的子目录中。然后,您将该子目录复制到您的主开发分支(例如,/trunk
)的适当位置。您始终在主开发分支中进行本地更改。对于您跟踪的代码的每个新版本,您都会将其引入供应商分支,并将更改合并到 /trunk
中,解决您本地更改和上游更改之间发生的任何冲突。
也许一个示例有助于阐明此算法。我们将使用一个场景,其中您的开发团队正在创建一个计算器程序,该程序链接到一个名为 libcomplex 的第三方复数算术库。我们将从供应商分支的初始创建以及第一个供应商发布的导入开始。我们将我们的供应商分支目录称为 libcomplex
,我们的代码发布将进入我们的供应商分支的子目录,称为 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' …
我们签出项目的开发分支——该分支现在包含第一个供应商发布的副本——然后我们开始自定义 libcomplex 代码。在我们不知情的情况下,我们修改的 libcomplex 版本现在已完全集成到我们的计算器程序中。[25]
几周后,libcomplex 的开发人员发布了他们的库的新版本——版本 1.1——其中包含一些我们真正想要的功能。我们希望升级到这个新版本,但不会丢失我们对现有版本所做的自定义更改。我们本质上想要做的是用 libcomplex 1.1 的副本替换我们当前的 libcomplex 1.0 基线版本,然后将我们之前对该库所做的自定义修改重新应用到新版本。但实际上,我们是从另一个方向解决这个问题的,将 libcomplex 在版本 1.0 和 1.1 之间的更改应用于我们修改过的副本。
要执行此升级,我们签出供应商分支的副本,并将 current
目录中的代码替换为新的 libcomplex 1.1 源代码。我们实际上是在现有文件之上复制新文件,也许是在现有文件和目录之上解压缩 libcomplex 1.1 发布的 tarball。这里的目标是使我们的 current
目录仅包含 libcomplex 1.1 代码,并确保所有代码都在版本控制下。哦,我们希望用尽可能少的版本控制历史记录干扰来完成此操作。
用 1.1 代码替换 1.0 代码后,svn status 将显示具有本地修改的文件以及可能的一些未版本控制或缺少的文件。如果我们做了应该做的事情,未版本控制的文件只是在 libcomplex 1.1 版本中引入的新文件——我们对这些文件运行 svn add 以将其纳入版本控制。缺少的文件是在 1.0 中存在但在 1.1 中不存在的文件,在这些路径上,我们运行 svn delete。最后,一旦我们的 current
工作副本仅包含 libcomplex 1.1 代码,我们提交我们进行的更改,使其看起来像这样。
我们的 current
分支现在包含新的供应商发布。我们标记新版本(与我们之前标记版本 1.0 供应商发布的方式相同),然后将先前版本标签和新当前版本之间的差异合并到我们的主开发分支中。
$ 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 源文件布局从一个供应商发布到另一个供应商发布是如何变化的。
包含多个删除、添加和移动的供应商发布会使升级到第三方数据的每个后续版本的流程变得复杂。因此,Subversion 提供了 svn_load_dirs.pl 脚本以帮助完成此过程。此脚本自动执行我们在通用供应商分支管理程序中提到的导入步骤,以确保将错误降至最低。您仍然需要使用合并命令将第三方数据的最新版本合并到您的主开发分支中,但 svn_load_dirs.pl 可以帮助您更快、更容易地达到这个阶段。
简而言之,svn_load_dirs.pl 是 svn import 的增强,具有几个重要的特点
它可以在任何时候运行,以使存储库中的现有目录完全匹配外部目录,执行所有必要的添加和删除,并可以选择执行移动。
它处理复杂的系列操作,这些操作需要 Subversion 进行中间提交——例如,在重命名文件或目录两次之前。
它可以选择标记新导入的目录。
它可以选择向与正则表达式匹配的文件和目录添加任意属性。
svn_load_dirs.pl 接受三个强制参数。第一个参数是要处理的 Subversion 基本目录的 URL。此参数后跟 URL(相对于第一个参数),当前供应商发布将被导入到该 URL 中。最后,第三个参数是要导入的本地目录。使用我们之前的示例,svn_load_dirs.pl 的典型运行可能如下所示
$ svn_load_dirs.pl http://svn.example.com/repos/vendor/libcomplex \ current \ /path/to/libcomplex-1.1 …
您可以通过传递 -t
命令行选项并指定标签名称来指示您希望 svn_load_dirs.pl 对新的供应商交付进行标记。此标签是相对于第一个程序参数的另一个 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”供应商交付的内容,并将它们与提议的新供应商交付进行比较。在最简单的情况下,将没有一个版本中存在而在另一个版本中不存在的文件,并且脚本将执行新的导入而不会出现问题。但是,如果两个版本之间的文件布局存在差异,svn_load_dirs.pl 会询问您如何解决这些差异。例如,您将有机会告诉脚本您知道 libcomplex 版本 1.0 中的 math.c
文件已在 libcomplex 1.1 中重命名为 arithmetic.c
。任何无法通过移动解释的差异都被视为常规添加和删除。
该脚本还接受一个单独的配置文件,用于在匹配正则表达式且 已添加 到存储库的文件和目录上设置属性。此配置文件使用 -p
命令行选项指定给 svn_load_dirs.pl。配置文件的每一行都是一组以空格分隔的两个或四个值:一个 Perl 风格的正则表达式,用于匹配要添加的路径;一个控制关键字(break
或 cont
);以及可选的属性名称和值。
\.png$ break svn:mime-type image/png \.jpe?g$ 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
的缩写),则匹配将继续使用配置文件的下一行。
正则表达式、属性名称或属性值中的任何空格都必须用单引号或双引号括起来。您可以通过在引号之前加上反斜杠 (\
) 字符来转义不用于包装空格的引号。反斜杠只在解析配置文件时转义引号,因此除了正则表达式所需的以外,不要保护任何其他字符。