本文档旨在描述 Subversion 1.1。如果您使用的是较新版本的 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' …
我们现在拥有 libcomplex 源代码的当前版本,位于/vendor/libcomplex/current。现在,我们标记该版本(参见 名为“标签”的部分),然后将其复制到主开发分支中。我们的副本将创建一个名为libcomplex的新目录,位于我们现有的calc项目目录中。我们将对供应商数据的这个复制版本进行自定义。
$ 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 现在已完全集成到我们的计算器程序中。[34]
几周后,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 - 相对于第一个参数 - 将当前供应商发布导入其中。最后,第三个参数是导入的本地目录。使用我们之前的示例,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 会提示您如何解决这些差异。例如,您将有机会告诉脚本您知道文件math.c在 libcomplex 的 1.0 版本中被重命名为arithmetic.c在 libcomplex 1.1 中。任何不能通过移动来解释的差异都将被视为常规的添加和删除。
该脚本还接受一个单独的配置文件,用于设置对匹配正则表达式的文件和目录的属性,这些文件和目录被 添加到 存储库中。此配置文件通过以下方式指定给 svn_load_dirs.pl:-p命令行选项。配置文件的每一行都是一个由空格分隔的两个或四个值的集合:一个 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— 那么匹配将继续使用配置文件的下一行。
正则表达式、属性名称或属性值中的任何空格都必须用单引号或双引号括起来。您可以通过在引号字符之前添加反斜杠 (\) 字符来转义不用于包装空格的引号字符。反斜杠仅在解析配置文件时转义引号,因此不要保护除正则表达式所需以外的任何其他字符。