本文档旨在描述 Apache™ Subversion® 的 1.7.x 系列。如果您运行的是其他版本的 Subversion,强烈建议您访问 https://svnbook.subversion.org.cn/ 并查阅适合您 Subversion 版本的文档。

供应商分支

尤其是在软件开发中,您在版本控制下维护的数据通常与其他人的数据密切相关,或者可能依赖于其他人的数据。通常,您项目的需要会要求您尽可能及时地更新外部实体提供的数据,而不会牺牲您自己项目的稳定性。这种情况在任何地方都会发生——无论是在一个群体生成的信息直接影响另一个群体生成的信息的地方。

例如,软件开发人员可能正在开发一个使用第三方库的应用程序。Subversion 与 Apache Portable Runtime (APR) 库(参见 名为“Apache Portable Runtime 库”的部分)之间存在这种关系。Subversion 源代码依赖于 APR 库来满足其所有可移植性需求。在 Subversion 开发的早期阶段,该项目密切跟踪 APR 变化的 API,始终坚持使用库代码 churn 的 最前沿。现在,随着 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"
…

我们检出项目的 main 分支,该分支现在包含第一个供应商发布的副本,然后开始定制 libcomplex 代码。不知不觉中,我们修改后的 libcomplex 版本已完全集成到我们的计算器程序中。[35]

几周后,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.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 源文件布局从供应商发布到供应商发布是如何变化的。

svn_load_dirs.pl

包含多个删除、添加和移动的供应商发布会使升级到第三方数据的每个后续版本的过程变得复杂。因此,Subversion 提供了 svn_load_dirs.pl 脚本以帮助完成此过程。此脚本自动执行我们在一般供应商分支管理过程中提到的导入步骤,以确保将错误降至最低。您仍然需要使用合并命令将第三方数据的最新版本合并到您的主开发分支中,但 svn_load_dirs.pl 可以帮助您更快、更轻松地到达该阶段。

简而言之,svn_load_dirs.plsvn 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 时,它会检查您现有 当前 供应商发布版本的内容,并将它们与建议的新供应商发布版本进行比较。在最简单的情况下,两个版本中都没有文件,并且脚本将执行新的导入而不会出现问题。但是,如果两个版本的文件布局之间存在差异,svn_load_dirs.pl 会询问您如何解决这些差异。例如,您将有机会告诉脚本您知道 libcomplex 版本 1.0 中的 math.c 文件已在 libcomplex 1.1 中重命名为 arithmetic.c。任何未通过移动解释的差异都将被视为常规添加和删除。

该脚本还接受一个单独的配置文件,用于设置与正则表达式匹配的 添加 文件和目录的属性。此配置文件使用 -p 命令行选项指定给 svn_load_dirs.pl。配置文件的每一行都是一个用空格分隔的两个或四个值集:一个 Perl 风格的正则表达式,用于匹配要添加的路径,一个控制关键字(breakcont),然后可选地一个属性名称和值。

\.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(这意味着不再对该路径应用任何属性更改)。如果控制规范为 contcontinue 的缩写),则匹配将继续进行配置文件的下一行。

正则表达式、属性名称或属性值中的任何空格都必须用单引号或双引号括起来。您可以通过在引号前面加上反斜杠 (\) 字符来转义不用于包装空格的引号。反斜杠仅在解析配置文件时转义引号,因此不要保护正则表达式以外的任何其他字符。



[35] 当然,它完全没有错误!