本文档旨在描述 Subversion 的 1.6.x 版本系列。如果您正在运行其他版本的 Subversion,我们强烈建议您访问 https://svnbook.subversion.org.cn/ 并参考适合您的 Subversion 版本的文档。
特别是对于软件开发而言,您在版本控制下维护的数据通常与其他人的数据密切相关,或者可能依赖于其他人的数据。通常,您项目的需要将决定您需要尽可能地与该外部实体提供的数据保持同步,而不会牺牲您自己项目的稳定性。这种情况一直都在发生——无论何时,一个组生成的信息都会直接影响另一个组生成的信息。
例如,软件开发人员可能正在开发一个使用第三方库的应用程序。Subversion 与 Apache 可移植运行时 (APR) 库(参见 “Apache 可移植运行时库”部分)就存在这样的关系。Subversion 源代码依赖于 APR 库来满足其所有可移植性需求。在 Subversion 开发的早期阶段,该项目密切跟踪 APR 的 API 变化,始终坚持使用该库代码更改的 “最前沿”。现在,由于 APR 和 Subversion 都已成熟,Subversion 尝试仅在经过充分测试的稳定发布点与 APR 的库 API 同步。
现在,如果您的项目依赖于其他人的信息,您可以尝试通过多种方式将该信息与您自己的信息同步。最痛苦的方式是向您项目的所有贡献者发布口头或书面说明,告诉他们确保他们拥有项目所需的特定版本的第三方信息。如果第三方信息在 Subversion 存储库中维护,您还可以使用 Subversion 的 externals 定义来有效地 “固定” 该信息的特定版本到您自己的工作副本目录中的某个位置(参见 “Externals 定义”部分)。
但有时您想在自己的版本控制系统中维护对第三方代码的自定义修改。回到软件开发示例,程序员可能需要出于自己的目的修改第三方库。这些修改可能包括新功能或错误修复,仅在内部维护,直到它们成为第三方库的正式版本的一部分。或者,这些更改可能永远不会反馈给库维护者,仅仅作为自定义调整存在,以使库更适合软件开发人员的需求。
现在,您面临着一种有趣的情况。您的项目可以以某种不连贯的方式容纳对第三方数据的自定义修改,例如使用补丁文件或文件的完整替代版本和目录。但这些很快就会成为维护方面的难题,需要某种机制将您的自定义更改应用于第三方代码,并且需要在您跟踪的第三方代码的每个后续版本中重新生成这些更改。
解决这个问题的方案是使用 供应商分支。供应商分支是您自己的版本控制系统中的一个目录树,其中包含第三方实体或供应商提供的信息。您决定吸收进您项目的每个版本的供应商数据都称为 供应商交付。
供应商分支提供两方面优势。首先,通过将当前支持的供应商交付存储在您自己的版本控制系统中,您可以确保您项目成员永远无需质疑他们是否拥有正确的版本供应商数据。他们只需作为定期工作副本更新的一部分接收该正确版本。其次,由于数据存在于您自己的 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 版本现在已完全集成到我们的计算器程序中。 [29]
几周后,libcomplex 的开发人员发布了他们的库的新版本——版本 1.1——其中包含一些我们真正想要的功能。我们想升级到这个新版本,但不想丢失我们对现有版本所做的自定义更改。我们实际上想做的是用 libcomplex 1.1 的副本替换我们当前的 libcomplex 1.0 基线版本,然后将我们之前对该库所做的自定义修改重新应用到新版本。但我们实际上是从另一个方向解决问题的,将 libcomplex 版本 1.0 和 1.1 之间所做的更改应用到我们修改后的副本。
要执行此升级,我们签出供应商分支的副本,并将 current
目录中的代码替换为新的 libcomplex 1.1 源代码。我们实际上是在现有文件之上复制新文件,也许是在我们的现有文件和目录之上解压 libcomplex 1.1 发布的压缩包。这里的目标是使我们的 current
目录仅包含 libcomplex 1.1 代码,并确保所有这些代码都在版本控制下。哦,我们还想以尽可能少的版本控制历史干扰来做到这一点。
用 1.1 代码替换 1.0 代码后,svn status 将显示具有本地修改的文件以及可能的一些未版本控制的文件。如果我们做了我们应该做的事情,未版本控制的文件只是 libcomplex 1.1 版本中引入的新文件——我们在这些文件上运行 svn add 以将它们置于版本控制下。如果 1.1 代码不再包含 1.0 树中存在的某些文件,可能很难注意到它们;您必须使用某些外部工具比较这两个树,然后 svn delete 1.0 中存在但 1.1 中不存在的任何文件。(尽管让这些相同的文件继续以未使用的模糊方式存在也可能很好!)最后,一旦我们的 current
工作副本仅包含 libcomplex 1.1 代码,我们就提交我们为使其看起来像这样所做的更改。
我们的 current
分支现在包含新的供应商交付。我们将其标记为 1.1(与我们之前标记版本 1.0 供应商交付的方式相同),然后将前一个版本标签与新当前版本之间的差异合并到我们的主要开发分支中
$ cd working-copies/calc $ svn merge ^/vendor/libcomplex/1.0 \ ^/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 脚本来协助此过程。此脚本自动执行我们在一般供应商分支管理程序中提到的导入步骤,以确保将错误降至最低。您仍然需要使用 merge 命令将第三方数据的最新版本合并到您的主要开发分支中,但 svn_load_dirs.pl 可以帮助您更快更轻松地到达该阶段。
简而言之,svn_load_dirs.pl 是对 svn import 的增强,它具有以下几个重要特点
它可以在任何时间点运行,以使存储库中的现有目录与外部目录完全匹配,执行所有必要的添加和删除操作,并且还可以选择执行移动操作。
它负责处理复杂的系列操作,而 Subversion 在这些操作之间需要中间提交——例如在重命名文件或目录两次之前。
它可以选择标记新导入的目录。
它可以选择将任意属性添加到与正则表达式匹配的文件和目录。
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 …
您可以通过传递 -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 时,它会检查您现有 “当前” 供应商发布版本的内容,并将它们与建议的新供应商发布版本进行比较。在最简单的情况下,两个版本中都不会有文件,并且脚本将执行新的导入而不会出现问题。但是,如果两个版本的文件布局之间存在差异,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
的缩写),则匹配将继续下一行配置文件。
正则表达式、属性名称或属性值中的任何空格都必须用单引号或双引号括起来。您可以通过在引号之前加上反斜杠 (\
) 字符来转义未用于括起空格的引号。反斜杠仅在解析配置文件时转义引号,因此不要保护除正则表达式所需的任何其他字符。