This text is a work in progress—highly subject to change—and may not accurately describe any released version of the Apache™ Subversion® software. Bookmarking or otherwise referring others to this page is probably not such a smart idea. Please visit http://www.svnbook.com/ for stable versions of this book.

供方分支

软件开发过程中可能会遇到这样一种情况, 用户所维护的代码依赖其他人的数据, 通常来说, 项目会要求所依赖的数据尽量处于最新状态, 但不能影响稳定性. 只要某个团队所维护的数据会对另一个团队产生直接的影响, 这种场景就会一直 存在.

比如说软件开发人员所开发的应用程序会用到一个第三方函数库, Subversion 和 Apache Portable Runtime (APR) 的关系即是如此, 见 “Apache 可移植运行库”一节. 为了实现可移植性, Subversion 的源代码依赖于 APR 函数库, 在 Subversion 的早期阶段, 项目 总是紧紧追随 APR 的 API 更新. 现在 Subversion 和 APR 都已经进入成熟期, 所以 Subversion 只使用 APR 经过充分测试的稳定版 API.

如果你的项目依赖其他人的数据, 有若干种方式可以用来同步这些数据. 其中 最麻烦的一种是以口头或书面的方式通知项目的所有成员, 将项目所需的第三方数 据更新到某个特定版本. 如果第三方数据使用 Subversion 进行管理, 就可以利用 Subversion 的外部定义, 快速地将第三方数据更新到特定版本, 并存放在工作 副本中 (见 “外部定义”一节).

有时候用户可能需要使用自己的版本控制系统去维护第三方代码的定制化 修改. 回到软件开发的例子中, 程序员可能需要修改第三方函数库, 以满足自己 的特殊需求. 这些定制化修改可能包括新功能或问题修正, 直到成为第三方函数 库的官方修改之前, 它们只在内部维护. 或者这些定制化修改永远不会发送给函数 库的官方维护人员, 它们只是为了满足项目的需求而单独存在.

现在你面临一种非常有趣的情况. 你的项目可以使用几种互不相交的方式存放 第三方数据的定制化修改, 比如说使用补丁文件, 或文件和目录的成熟的替代 版本. 但是维护人员很快就会感到头疼, 因此迫切需要一种机制, 能够方便地把你的定制化修改应用到第三方代码上, 并 且当第三方代码更新时能够迫使开发人员重新生成这些修改.

解决办法是使用 供方分支 (vendor branches). 供方分支是一个存在于你自己的版本控制系统中的 目录, 包含了由第三方提供的数据. 被项目吸收的每一个供方数据版本都称为 一个 供方物资 (vendor drop).

供方分支有两个好处. 首先, 通过在自己的版本控制系统中存放当前支持 的供方物资, 你就可以确认项目成员不必再担心他们是否使用了供方数据的正确 版本, 只需要更新工作副本, 他们就可以得到供方数据的正确版本. 第二, 因为 供方数据使用 Subversion 进行管理, 所以用户可以方便地在仓库中存放自己的 定制化修改, 而无需再使用某种自动的 (或手动的) 方法对定制化修改进行换 入换出.

不幸的是, 在 Subversion 中并不存在一种管理供方分支的最佳方法. 系统 的灵活性提供了多种不同的管理方法, 每一种都有各自的优缺点, 没有一种方法 可以当成 万能钥匙. 在下面几节里, 我们将从较高的层面介绍其中几种方法, 所使用的例子也是依赖第三方函数库的 典型示例.

通常的供方分支管理过程

维护第三方函数库的定制化修改会牵涉到 3 个数据源: 最新版的定制化 修改所基于的第三方函数库的版本, 项目所使用的定制化版本 (即实际上的 供方分支), 以及第三方函数库的新版本. 于是, 管理供方分支 (供方分支应 存放在用户自己的代码仓库中, 包含了前面提到的 3 个数据源) 本质上可以归结为 执行合并操作 (指的是一般意义上的合并), 但是其他开发团队可能会对其他 数据源—第三方函数库代码的全新版本—采取不同的策略, 所以说 同样存在几种不同的方法去执行合并操作.

严格来说, 有几种不同的方式用来执行这些合并操作, 为简单起见, 也为了 向读者展示一些具体的东西, 我们假设只有一个供方分支, 每当第三方函数库 发布新版本时, 通过应用当前版本与最新版之间的差异, 将分支更新到新的发 布版本.

[注意] 注意

另一种办法是为第三方函数库的每一个新版本都创建一个新的供方分支, 并将当前原版函数库与定制版本 (来自当前的供方分支) 之间的差异应用到新 的分支上. 这种方法并没有什么问题—我们只是觉得没必要在这里介绍 所有的可能性.

下面几节介绍了在几种不同的场景中, 如何创建并管理供方分支. 在下面 的例子里, 我们假设第三方函数库的名字是 libcomplex, 当前供方分支所基于 的版本是 libcomplex 1.0.0, 分支的位置是 ^/vendor/libcomplex-custom. 稍后读者将会看到如何 把供方分支升级到 libcomplex 1.0.1, 同时保留定制化修改.

来自外部仓库的供方分支

先来看第一种管理供方分支的方法, 该方法的适用条件是第三方函数库 可以通过 Subversion 进行访问. 为了方便说明, 我们假设函数库 libcomplex 存放在可以公开访问的 Subversion 仓库中, 而且函数库的开发人员也使用了 通常的发布步骤, 即为每一个稳定的发布版创建一个标签.

从 Subversion 1.5 开始, svn merge 支持 外部仓库合并 (foreign repository merges), 也就是合并的源与目标属于不同的仓库. 与旧版相 比, Subversion 1.8 的 svn copy 的行为有所变化: 如果从外部仓库复制目录到工作副本中, 得到的目录将被工作副本收录, 等待 添加到仓库中. 这个特性叫做 外部仓库复制 (foreign repository 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.
$
[注意] 注意

如果用户使用的 Subversion 版本较旧, 不支持外部仓库复制, 那么与 此最类似的替代操作是导入 (通过命令 svn import) 供方标签的工作副本, 导入时需要加上选项 --no-auto-props --no-ignore, 这样才能保证目录及其 所有的属性都能被完整地导入到你的仓库中.

有了基于 libcomplex 1.0.0 的供方分支后, 我们就可以开始对 libcomplex 进行定制化修改, 然后提交到分支上, 并且可以开始在自己的应用程序中使用 修改后的 libcomplex.

一段时间后, 官方发布了 libcomplex 1.0.1, 查看新版的修改日志后, 我 们决定把自己的供方分支也升级到 1.0.1, 这时候需要用到 Subversion 的 外部仓库合并. 当前的供方分支是原始的 libcomplex 1.0.0 再加上我们的定 制化修改, 现在我们需要把原始的 1.0.0 与 1.0.1 之间的差异应用到供方分 支, 最理想的情况是被应用的差异不会影响到我们的定制化修改. 合并操作需要 使用 二路 URL 形式的 svn merge.

$ 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 升级 到 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 时, 尽量保持干净. 下面我们将介绍另一 种供方分支管理方法, 其中第三方函数库的发布版将以镜像的方式存放在我 们的仓库中.

首次创建供方分支非常简单, 对于我们的例子而言, 假设 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, 这样的话, Subversion 客户端就不会生成 供方物资中原本就没有的属性信息.[41]

供方发布物资进入我们的仓库后, 接下来就可以用 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 的增量版本 导入到我们的仓库中, 然后使用 二路 URL 形式的 svn merge, 把差异应用到供方分支中.

事实证明, 有多种方式都可以正确地把 libcomplex 1.0.1 添加到仓库中. [42] 我们在这里介绍的方法相对比较原始, 但作为说明 已经足够了.

记住, 我们希望 libcomplex 1.0.1 在我们这儿的镜像能和 1.0.0 的镜像 共享祖先, 这样的话在把它们之间的差异合并到供方分支时, 能产生最好的效果. 于是, 首先通过复制 供方标签 libcomplex-1.0.0 创建分支 libcomplex-1.0.1—它最终将变成 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 的工作副本, 然后把工作副本 中的代码升级到 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.1 的目录中检出 libcomplex 1.0.0 的 代码, 将得到一个包含了本地修改的工作副本—正是这些修改, 把 libcomplex 1.0.0 升级到 libcomplex 1.0.1.

的确, 这是一个非常简单的例子, 升级操作只涉及到已有文件的修改. 在实际 工作中, 第三方函数库的新版修改可能还包括添加或删除文件 (目录), 重命名文 件或目录等. 在这种情况下, 把供方标签升级到新版会困难得多, 作为训练, 具体 的升级过程将留给读者完成.[43]

不管怎么, 我们成功地把供方标签的工作副本升级到了 libcomplex 1.0.1, 然后提交修改.

$ 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.
$

我们终于准备好了升级供方分支. 记住, 我们的目标是把原始的 1.0.1 和 1.0.0 之间的差异应用到供方分支中. 下面展示了如何使用 二路 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.
$

到此为止, 供方分支的升级工作就算完成了. 如果将来还要再次升级, 仍然 可以按照本节介绍的步骤进行操作.



[41] 严格来说, 可以允许自动属性 工作, 但其中的关键问题是确保每一个供方物资都能得到相同的对待.

[42] 不正确的做法是再使用一次 svn import, 因为这将导致 libcomplex 1.0.0 和 libcomplex 1.0.1 不含有共同的祖 先.

[43] 提示: svn add --force /path/to/working-copy --no-ignore --no-auto-props 可以方便地把任意一个新的供方物资条目 添加到仓库中.