本文档旨在描述 Subversion 1.2。如果您运行的是更新版本的 Subversion,我们强烈建议您访问 https://svnbooks.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 版本现在已完全集成到我们的计算器程序中。[39]
几周后,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.0 代码替换为 1.1 代码后,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
的缩写),则匹配将继续进行配置文件的下一行。
正则表达式、属性名称或属性值中的任何空格都必须用单引号或双引号括起来。您可以通过在引号之前加上反斜杠 (\
) 字符来转义未用于包装空格的引号。反斜杠仅在解析配置文件时转义引号,因此不要保护正则表达式以外的任何其他字符。