此文本尚在编写中,内容可能发生重大变化,可能无法准确描述 Apache™ Subversion® 软件的任何已发布版本。将此页面添加为书签或以其他方式推荐给其他人可能不是一个明智的决定。请访问 https://svnbook.subversion.org.cn/ 以获取本书的稳定版本。

供应商分支

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

例如,软件开发人员可能正在开发一个使用第三方库的应用程序。Subversion 与 Apache Portable Runtime (APR) 库具有这种关系(请参见 名为“Apache Portable Runtime 库”的部分)。Subversion 源代码依赖 APR 库来满足其所有可移植性需求。在 Subversion 开发的早期阶段,该项目紧密跟踪 APR 变化的 API,始终坚持库代码 churn 的 领先边缘。现在,由于 APR 和 Subversion 都已成熟,Subversion 尝试仅在经过充分测试的稳定发布点与 APR 的库 API 同步。

现在,如果您的项目依赖他人的信息,您可以尝试通过多种方式将该信息与您自己的信息同步。最痛苦的是,您可以向您项目的所有贡献者发布口头或书面指示,告诉他们确保他们拥有项目所需的第三方信息的特定版本。如果第三方信息在 Subversion 存储库中维护,您还可以使用 Subversion 的 externals 定义来有效地 锁定 该信息的特定版本到您自己的工作副本中的某个位置(请参见 名为“Externals 定义”的部分)。

但有时您想在自己的版本控制系统中维护对第三方代码的自定义修改。回到软件开发示例,程序员可能需要出于自己的目的对第三方库进行修改。这些修改可能包括新功能或错误修复,仅在它们成为第三方库官方版本的一部分之前在内部维护。或者,这些更改可能永远不会传达给库维护者,仅作为自定义调整存在,以使库更适合软件开发人员的需求。

现在您面临一个有趣的情况。您的项目可以以某种不连贯的方式容纳对第三方数据的自定义修改,例如使用补丁文件或文件的完整替代版本和目录。但这很快就会成为维护难题,需要某种机制来将您的自定义更改应用到第三方代码,并需要在您跟踪的每个后续版本第三方代码中重新生成这些更改。

解决此问题的方法是使用 供应商分支。供应商分支是您自己的版本控制系统中的目录树,其中包含第三方实体(或供应商)提供的信息。您决定吸收到项目中的每个版本的供应商数据称为 供应商发布

供应商分支提供了两个好处。首先,通过将当前支持的供应商发布存储在您自己的版本控制系统中,您确保项目的成员永远不需要质疑他们是否拥有供应商数据的正确版本。他们只需在他们定期更新的工作副本中接收该正确版本。其次,因为数据存在于您自己的 Subversion 存储库中,您可以将您对它的自定义更改存储在原地——您不再需要自动(或更糟糕的是手动)方法来交换您的自定义内容。

不幸的是,没有一种最好的方法可以在 Subversion 中管理供应商分支。系统的灵活性提供了几种不同的方法,每种方法都有其优点和缺点,并且没有一种方法可以清楚地被认为是解决问题的 灵丹妙药。我们将在以下部分中概括地介绍其中一些方法,使用软件项目依赖第三方库的常见示例。

一般供应商分支管理程序

维护对第三方库的自定义更改涉及三个数据源:您最后基于的第三方库版本,库的自定义版本(即实际的供应商分支),以及您可能希望升级到的供应商库的任何新版本。因此,管理供应商分支(根据我们对事物的定义,它应该存在于您的源代码存储库中),基本上可以归结为执行合并操作(从广义上讲)。但不同的团队对其他数据源采用不同的方法——第三方库代码的原始版本。因此,执行必要的合并也有不同的具体方法。

严格来说,从广义上讲,有两种不同的方法可以执行这些合并。为了简单起见,并且以至少在本节中提供 具体 内容为目标,我们假设只有一个供应商分支,通过接收描述当前原始库和新的原始库版本之间的差异的更新,将其升级到第三方库的每个后续新版本。

[Note] 注意

另一种方法是为每个后续原始库版本创建新的供应商分支,将当前原始库和其自定义版本(来自当前供应商分支)之间的差异应用到新分支。这种方法没有问题——我们只是没有必要在此空间中记录所有合法的可能性。

以下部分介绍如何在几种不同情况下创建和管理供应商分支。在以下示例中,我们假设第三方库称为 libcomplex,并且我们将基于 libcomplex 1.0.0 实现一个供应商分支,该分支位于我们存储库中的 ^/vendor/libcomplex-custom。然后,我们将看看如何升级到 libcomplex 1.0.1,同时仍然保留我们对库的自定义更改。

来自外部存储库的供应商分支

首先,让我们看看当原始第三方库本身可以通过 Subversion 访问时,可以使用的一种供应商分支管理方法。为了便于说明,我们假设我们之前讨论过的 libcomplex 库是在一个公开可访问的 Subversion 存储库中开发的,并且其开发人员使用了合理的发布程序,包括为每个稳定发布版本创建标签。

从 Subversion 1.5 开始,svn merge 已能够执行所谓的 外部存储库合并,其中合并的来源位于与签出合并目标工作副本的存储库不同的存储库中。在 Subversion 1.8 中,svn copy 的行为发生了变化,因此,当您从外部存储库复制到现有工作副本时,生成的树将被合并到该工作副本中并计划添加。我们将使用这种 外部存储库复制功能来引导我们的供应商分支。

因此,让我们创建我们的供应商分支。我们将从为所有此类供应商分支在我们的存储库中创建一个占位符目录开始,然后签出该位置的工作副本。

$ svn mkdir http://svn.example.com/projects/vendor \
            -m "Create a container for vendor branches."
Committed revision 1160.
$ svn checkout http://svn.example.com/projects/vendor \
               /path/to/vendor
Checked out revision 1160.
$

现在,我们将利用 Subversion 的外部存储库复制支持来获取 libcomplex 1.0.0 的精确副本——包括存储在其文件和目录上的任何 Subversion 属性——来自供应商存储库。

$ cd /path/to/vendor
$ svn copy http://svn.othervendor.com/repos/libcomplex/tags/1.0.0 \
           libcomplex-custom
--- Copying from foreign repository URL 'http://svn.othervendor.com/repos/lib\
complex/tags/1.0.0':
A    libcomplex-custom
A    libcomplex-custom/README
A    libcomplex-custom/LICENSE
…
A    libcomplex-custom/src/code.c
A    libcomplex-custom/tests
A    libcomplex-custom/tests/TODO
$ svn commit -m "Initialize libcomplex vendor branch from libcomplex 1.0.0."
Adding         libcomplex-custom
Adding         libcomplex-custom/README
Adding         libcomplex-custom/LICENSE
…
Adding         libcomplex-custom/src
Adding         libcomplex-custom/src/code.h
Adding         libcomplex-custom/src/code.c
Transmitting file data .......................................
Committed revision 1161.
$
[Note] 注意

如果您碰巧使用的是较早版本的 Subversion,则svn copy 中新的外部存储库复制支持的最佳近似值是改为导入(通过svn import)供应商标签的工作副本,包括--no-auto-props--no-ignore选项,以便在您自己的存储库中准确地复制完整的树及其任何版本属性。

现在我们有了基于 libcomplex 1.0.0 的供应商分支,我们可以开始对 libcomplex 进行自定义更改,这些更改是我们项目所需的,并将它们直接提交到我们创建的供应商分支。当然,我们可以开始在自己的应用程序中使用 libcomplex。

一段时间后,libcomplex 1.0.1 发布了。在审查了它的更改后,我们决定将我们的供应商分支升级到新版本。这里就是 Subversion 的外部存储库合并操作有用的地方。我们的供应商分支中包含原始的 libcomplex 1.0.0 以及我们对它的自定义更改。我们现在需要的是将供应商在 1.0.0 和 1.0.1 之间进行的更改集合并到我们的供应商分支中,理想情况下不会覆盖我们自己的自定义更改。这正是svn merge 命令的 2-URL 形式的作用。

$ cd /path/to/vendor
$ svn merge http://svn.othervendor.com/repos/libcomplex/tags/1.0.0 \
            http://svn.othervendor.com/repos/libcomplex/tags/1.0.1 \
            libcomplex-custom
--- Merging differences between foreign repository URLs into '.':
U    libcomplex-custom/src/code.h
C    libcomplex-custom/src/code.c
U    libcomplex-custom/README
Summary of conflicts:
  Text conflicts: 1
Conflict discovered in file 'libcomplex-custom/src/code.c'.
Select: (p) postpone, (df) diff-full, (e) edit, (m) merge,
        (mc) mine-conflict, (tc) theirs-conflict, (s) show all options: 

如您所见,svn merge 将使 libcomplex 1.0.0 看起来像 libcomplex 1.0.1 所需的更改合并到了我们的工作副本中。在我们的示例中,它甚至注意到并标记了一个文件上的冲突。看来供应商修改了我们也自定义过的一个文件的某个区域。Subversion 安全地检测到此冲突,并给了我们机会解决它,以便我们对现在称为 libcomplex 1.0.1 的自定义更改仍然有意义。(请参见 名为“解决任何冲突”的部分,了解有关解决此类冲突的更多信息。)

一旦我们解决了冲突并执行了任何必要的测试或审查,我们就可以将更改提交到我们的供应商分支。

$ svn status libcomplex-custom
M       libcomplex-custom/src/code.h
M       libcomplex-custom/src/code.c
M       libcomplex-custom/README
$ svn commit -m "Upgrade vendor branch to libcomplex 1.0.1." \
             libcomplex-custom
Sending        libcomplex-custom/README
Sending        libcomplex-custom/src/code.h
Sending        libcomplex-custom/src/code.c
Transmitting file data ...
Committed revision 1282.
$

简而言之,这就是在原始来源可通过 Subversion 访问时管理供应商分支的方法。不过,也有一些明显的缺点。首先,Subversion 本身不会像同一存储库合并那样自动跟踪外部存储库合并。这意味着用户需要知道在他们的供应商分支上执行了哪些合并,以及在升级该分支时如何构建下一个合并。此外,与 Subversion 的所有合并支持一样,合并来源中的重命名会导致不少复杂性和挫折。不幸的是,目前,我们还没有特别可靠的建议来缓解这种痛苦。

来自镜像源的供应商分支

在上一节(名为“从外部仓库创建供应商分支”的那一节)中,我们探讨了当供应商发布版本可通过 Subversion 访问时如何实现和维护供应商分支,这对于供应商分支来说是最理想的情况。Subversion 在处理经过 Subversion 管理的内容的合并方面非常出色。不幸的是,并非所有第三方库都通过 Subversion 公开访问。很多时候,一个项目依赖于一个只能通过非 Subversion 机制交付的库,例如源代码发行版压缩包。在这种情况下,我们强烈建议您尽一切努力将这些非 Subversion 信息以尽可能干净的方式导入到 Subversion 中。因此,让我们来考察一种将第三方库的各种发行版本镜像到我们自己的仓库中的供应商分支方法。

第一次设置供应商分支其实非常简单。在我们的示例中,我们假设 libcomplex 1.0.0 是通过常见的压缩包机制交付的。要创建我们的供应商分支,我们将首先获取 libcomplex 1.0.0 压缩包的内容,并将其作为一种只读(仅按惯例)的供应商标签导入到我们的仓库中。

$ tar xvfz libcomplex-1.0.0.tar.gz
libcomplex-1.0.0/
libcomplex-1.0.0/README
libcomplex-1.0.0/LICENSE
…
libcomplex-1.0.0/src/code.c
libcomplex-1.0.0/tests
libcomplex-1.0.0/tests/TODO
$ svn import libcomplex-1.0.0 \
             http://svn.example.com/projects/vendor/libcomplex-1.0.0 \
             --no-ignore --no-auto-props \
             -m "Import libcomplex 1.0.0 sources."
Adding         libcomplex-custom
Adding         libcomplex-custom/README
Adding         libcomplex-custom/LICENSE
…
Adding         libcomplex-custom/src
Adding         libcomplex-custom/src/code.h
Adding         libcomplex-custom/src/code.c
Transmitting file data .......................................
Committed revision 1160.
$

请注意,在我们的示例中,我们在导入期间使用了 --no-ignore 选项,以便 Subversion 确保获取供应商发布版本中的每个文件,并且不会遗漏任何文件。我们还提供了 --no-auto-props 选项,以防止我们的客户端生成供应商发布版本中不存在的属性信息。[44].

现在,第一个供应商发行版本已经存在于我们的仓库中,我们可以使用 svn copy 创建供应商分支,就像创建任何其他分支一样。

$ svn copy http://svn.example.com/projects/vendor/libcomplex-1.0.0 \
           http://svn.example.com/projects/vendor/libcomplex-custom \
           -m "Initialize libcomplex vendor branch from libcomplex 1.0.0."
Committed revision 1161.
$

好的。现在我们已经创建了基于 libcomplex 1.0.0 的供应商分支。现在我们可以开始对 libcomplex 进行定制,这些定制是我们目的所需的——将它们直接提交到我们创建的供应商分支——然后开始在我们自己的应用程序中使用我们定制的 libcomplex。

一段时间后,libcomplex 1.0.1 发布了。在审查了它的更改后,我们决定将我们的供应商分支升级到新版本。为了在我们的分支上执行此升级,我们需要将供应商在 1.0.0 和 1.0.1 之间所做的相同更改应用到我们的供应商分支,而不会覆盖我们自己的定制。执行此应用最安全的方法是首先将 libcomplex 1.0.1 导入到我们的仓库中,以我们仓库中的 libcomplex 1.0.0 代码的差异形式。之后,我们将使用 svn merge 命令的双 URL 形式将相同的更改复制到我们的供应商分支。

事实证明,我们可以采用几种不同的方法将 libcomplex 1.0.1 正确地导入到我们的仓库中。[45] 我们将在此处描述的方法相对简单,但它将满足我们的说明需要。

请记住,我们希望 libcomplex 1.0.1 供应商发布版本的镜像与我们的 1.0.0 供应商发布版本的镜像共享祖先,这将在以后我们需要将这两个发布版本之间的更改合并到我们的供应商分支时产生最佳结果。因此,我们将首先创建一个 libcomplex-1.0.1 分支,作为我们之前创建的 libcomplex-1.0.0 供应商标签 的副本——这个副本最终将成为 libcomplex 1.0.1 的副本。

$ svn copy http://svn.example.com/projects/vendor/libcomplex-1.0.0 \
           http://svn.example.com/projects/vendor/libcomplex-1.0.1 \
           -m "Setup a construction zone for libcomplex 1.0.1."
Committed revision 1282.
$

现在我们需要创建一个 libcomplex-1.0.1 分支的工作副本,然后让它真正看起来像 libcomplex 1.0.1。为此,我们将利用 svn checkout 可以覆盖现有目录的事实,并且如果提供了 --force 选项,则可以以允许在签出树和签出覆盖的目标树之间的差异作为新工作副本中的本地修改的方式进行覆盖。

$ tar xvfz libcomplex-1.0.1.tar.gz
libcomplex-1.0.1/
libcomplex-1.0.1/README
libcomplex-1.0.1/LICENSE
…
libcomplex-1.0.1/src/code.c
libcomplex-1.0.1/tests
libcomplex-1.0.1/tests/TODO
$ svn checkout http://svn.example.com/projects/vendor/libcomplex-1.0.1 \
               libcomplex-1.0.1 \
               --force
E    libcomplex-1.0.1/README
E    libcomplex-1.0.1/LICENSE
E    libcomplex-1.0.1/INSTALL
…
E    libcomplex-1.0.1/src/code.c
E    libcomplex-1.0.1/tests
E    libcomplex-1.0.1/tests/TODO
Checked out revision 1282.
$ svn status libcomplex-1.0.1
M       libcomplex-1.0.1/src/code.h
M       libcomplex-1.0.1/src/code.c
M       libcomplex-1.0.1/README
$

如您所见,在签出实际上是 libcomplex 1.0.0 的内容并将其放在 libcomplex 1.0.1 解压的压缩包之上之后,我们得到一个工作副本,其中包含本地修改——这些修改是将我们之前的供应商发布版本转换为新版本所需的修改。

诚然,这是一个非常简单的例子。执行此特定升级所需的更改仅涉及对现有文件的更改。实际上,第三方库的新版本也可能添加或删除文件或目录,可能重命名文件或目录,等等。在这些情况下,将新的供应商标签转换为能够准确反映它声称反映的供应商发布版本的状态可能会更具挑战性。我们将把这些转换的细节留作读者的练习。[46]

无论我们如何做到这一点,一旦我们的新的供应商标签工作副本与原始源代码发行版相协调,我们就可以将这些更改提交到我们的仓库。

$ svn commit -m "Upgrade vendor branch to libcomplex 1.0.1." \
             libcomplex-1.0.1
Sending        libcomplex-1.0.1/README
Sending        libcomplex-1.0.1/src/code.h
Sending        libcomplex-1.0.1/src/code.c
Transmitting file data ...
Committed revision 1283.
$

我们终于可以升级我们的供应商分支了。请记住,我们的目标是将供应商在 libcomplex 1.0.0 和 1.0.1 发布版本之间所做的更改导入到我们的供应商分支。这就是双 URL svn merge 操作发挥作用的地方,它将应用于我们供应商分支的工作副本。

$ svn checkout http://svn.example.com/projects/vendor/libcomplex-custom \
               libcomplex-custom
E    libcomplex-custom/README
E    libcomplex-custom/LICENSE
E    libcomplex-custom/INSTALL
…
E    libcomplex-custom/src/code.c
E    libcomplex-custom/tests
E    libcomplex-custom/tests/TODO
Checked out revision 1283.
$ cd libcomplex-custom
$ svn merge ^/vendor/libcomplex-1.0.0 \
            ^/vendor/libcomplex-1.0.1
--- Merging differences between repository URLs into '.':
U    src/code.h
C    src/code.c
U    README
Summary of conflicts:
  Text conflicts: 1
Conflict discovered in file 'src/code.c'.
Select: (p) postpone, (df) diff-full, (e) edit, (m) merge,
        (mc) mine-conflict, (tc) theirs-conflict, (s) show all options: 

如您所见,svn merge 已将必需的更改合并到我们的工作副本中,并标记了一个冲突,因为供应商和我们在定制过程中修改了同一个文件的相同区域。Subversion 检测到此冲突,并给了我们机会解决它(使用 名为“解决任何冲突”的那一节 中描述的方法),以便我们对现在 libcomplex 1.0.1 的定制仍然有效。在我们解决完冲突并执行了必要的测试或审查之后,我们可以将更改提交到我们的供应商分支。

$ svn status
M       src/code.h
M       src/code.c
M       README
$ svn commit -m "Upgrade vendor branch to libcomplex 1.0.1."
Sending        README
Sending        src/code.h
Sending        src/code.c
Transmitting file data ...
Committed revision 1284.
$

我们的供应商分支升级已完成。下次我们需要升级该分支时,我们将遵循与这次升级相同的过程。



[44] 从技术上讲,我们可以让自动属性功能发挥作用,但让它正常工作的关键是确保每个供应商发布版本都获得相同的自动属性处理。

[45] 使用另一个 svn import 操作将是一个 错误的 方法,因为 libcomplex 1.0.0 和 1.0.1 分支将没有任何共同的祖先。

[46] 不过,这里有一个提示:svn add --force /path/to/working-copy --no-ignore --no-auto-props 在这种情况下,非常适合将任何新的供应商发布版本项添加到版本控制中。

TortoiseSVN 官方中文版 1.14.7 发布