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.

外部定义

有时候, 构造一个由多个不同的检出所组成的工作副本是很有用的, 比如说, 用户可能想把多个来自不同位置的子目录放到一个目录里, 这些子目录甚至来自不 同的仓库. 用户当然可以手工实现—用 svn checkout 创建出嵌套的工作副本结构. 但是如果每个用户都有这种需求, 那么所有的用户 都得自己手工构造.

幸运的是, Subversion 支持 外部定义 ( externals definitions), 外部定义是一个本地目录到仓库目录 URL 的映射. 用户使用属性 svn:externals 批量地声明外 部定义, 创建或修改属性的命令是 svn propsetsvn propedit (见 “操作属性”一节). 属性 svn:externals 可以设置在任意一个被版本控制的目录上, 属性的值描述了外部 仓库的位置, 以及检出到本地时得到的本地目录.

svn:externals 的方便之处是一旦目录设置了该属性, 所有检出该目录的用户都会受益. 换句话说, 如果有一个用户已经用外部定义 构造好了一个嵌套的工作副本结构, 其他用户就不用再重新做一遍— 当原始的工作副本检出完毕后, Subversion 还会自动检出外部的工作副本.

[警告] 警告

外部定义里的目标子目录事先 不能 存在— Subversion 在检出外部的工作副本时会自动创建它们.

svn:externals 是版本化的属性, 如果用户需要 修改一个外部定义, 使用普通的属性修改子命令即可. 如果提交了属性 svn:externals 的修改, 下一次执行 svn update 时, Subversion 将会根据修改后的外部定义更新 检出的项目, 同样的事情也会发生在其他用户的工作副本里.

[提示] 提示

因为属性 svn:externals 的值由多行文本组成, 所以我 们强烈建议用户使用 svn propedit (而不是 svn propset) 修改属性.

Subversion 1.5 之前的外部定义的格式是一个多行表格, 每一行包括子 目录 (相对于设置了属性的目录), 可选的版本号标志, 以及一个完全限定的 Subversion 仓库 URL 的绝对路径. 外部定义的一个例子是:

$ svn propget svn:externals calc
third-party/sounds             http://svn.example.com/repos/sounds
third-party/skins -r148        http://svn.example.com/skinproj
third-party/skins/toolkit -r21 http://svn.example.com/skin-maker

如果用户检出了目录 calc, Subversion 会继续检出 外部定义里的项目.

$ svn checkout http://svn.example.com/repos/calc
A    calc
A    calc/Makefile
A    calc/integer.c
A    calc/button.c
Checked out revision 148.

Fetching external item into calc/third-party/sounds
A    calc/third-party/sounds/ding.ogg
A    calc/third-party/sounds/dong.ogg
A    calc/third-party/sounds/clang.ogg
…
A    calc/third-party/sounds/bang.ogg
A    calc/third-party/sounds/twang.ogg
Checked out revision 14.

Fetching external item into calc/third-party/skins
…

从 Subversion 1.5 开始, svn:externals 支持一 种新的格式, 外部定义仍然是多行文本, 但某些信息的顺序与格式发生了变化. 新的语法更加贴近 svn checkout 的参数: 首先是可选的 版本号标志, 然后是外部仓库的 URL, 本地目录的相对路径. 注意, 这次我们没有 说 完全限定的 Subversion 仓库的 URL 的绝对路径, 这是因为 新的格式支持相对 URL 和带有限定版本号的 URL. 上面的例子在 Subversion 1.5 里的写法是:

$ svn propget svn:externals calc
      http://svn.example.com/repos/sounds third-party/sounds
-r148 http://svn.example.com/skinproj third-party/skins
-r21  http://svn.example.com/skin-maker third-party/skins/toolkit

带有限定版本号 (见 “限定版本号与实施版本号”一节) 的写法是:

$ svn propget svn:externals calc
http://svn.example.com/repos/sounds third-party/sounds
http://svn.example.com/skinproj@148 third-party/skins
http://svn.example.com/skin-maker@21 third-party/skins/toolkit
[提示] 提示

用户应该在经过慎重地考虑后再决定要不要在外部定义中显式地指定版本号, 这意味着用户必须决定应该在什么时候抓取哪一个版本的快照到外部目录里. 显式地指定版本号除了可以避免向用户没有权限的仓库提交修改外, 当用户把 工作副本回退到之前的版本时, 外部定义属性也会回退到当时的版本, 外部工作 副本也会根据外部定义进行相应地更新. 对于软件项目而言, 这可能会造成旧版 代码构建失败.

对大多数仓库而言, 三种格式的外部定义的最终效果都是一样的, 它们都有 同样的好处, 也都有同样的麻烦. 因为用到了 URL 的绝对路径, 如果移动或 复制一个带有外部定义的目录, 这并不会对外部定义的检出造成影响 (虽然本地 目标子目录的绝对路径会随着目录的重命名而发生变化). 在某些情况下, 这会给 用户造成困扰, 比如说, 用户有一个名为 my-project 的顶层目录, 用户在它的其中一个子目录 (my-project/some-dir ) 上设置了外部定义, 外部定义指向的是另一个子目录 ( my-project/external-dir).

$ svn checkout http://svn.example.com/projects .
A    my-project
A    my-project/some-dir
A    my-project/external-dir
…
Fetching external item into 'my-project/some-dir/subdir'
Checked out external at revision 11.

Checked out revision 11.
$ svn propget svn:externals my-project/some-dir
subdir http://svn.example.com/projects/my-project/external-dir

$

现在用户用 svn move 重命名 my-project , 但外部定义仍然指向 my-project 的子目 录, 即使这个目录已经不存在了.

$ svn move -q my-project renamed-project
$ svn commit -m "Rename my-project to renamed-project."
Deleting       my-project
Adding         renamed-project

Committed revision 12.
$ svn update
Updating '.':

svn: warning: W200000: Error handling externals definition for 'renamed-projec
t/some-dir/subdir':
svn: warning: W170000: URL 'http://svn.example.com/projects/my-project/externa
l-dir' at revision 12 doesn't exist
At revision 12.
svn: E205011: Failure occurred processing one or more externals definitions
$

另外, 当使用绝对的 URL 路径时, 如果仓库支持多种 URL 模式, 这也会产 生问题. 比如说仓库服务器允许任意用户通过 http://https:// 访问仓库, 但只允许通过 https:// 提交修改. 如果用户的外部定义使用了 http:// 形式的 URL, 用户将无法从外部定义创建的工作副本里提交修改. 另一方面, 如果仓库服务器只支持 https:// 形式的 URL, 但客户端 只支持 http://, 那么它将无法检出外部项目. 还要注意, 如果用户重定位了工作副本 (通过命令 svn relocate), 外部定义检出的工作副本并 不会 被重定位.

在改善这些问题方面, Subversion 1.5 前进了一大步. 前面已经说过新的 外部定义支持 URL 的相对路径, Subversion 1.5 提供了多种指定相对 URL 路径 的语法.

../

相对于设置了 svn:externals 的目录的 URL

^/

相对于 svn:externals 所在的仓库的根 目录

//

相对于设置了属性 svn:externals 的目录 的 URL 模式

/

相对于 svn:externals 所在的仓库的根 URL

^/../REPO-NAME

相对于和定义了 svn:externals 的仓库 处于同一个 SVNParentPath 位置下的兄弟仓库

如果把绝对的 URL 路径改成相对路径, 之前的例子就可以写成:

$ svn propget svn:externals calc
^/sounds third-party/sounds
/skinproj@148 third-party/skins
//svn.example.com/skin-maker@21 third-party/skins/toolkit
$

Subversion 1.6 为外部定义添加了两个增强功能, 首先, 利用引号和转义字 符, 外部工作副本的路径可以包含空格, 在此之前如何处理路径中的空格是一 件很麻烦的事情, 因为空格被用作外部定义的字段分隔符, 现在只需要用双引号 包裹路径, 或者用反斜杆 (\) 转义路径中会引起问题的字符. 如果外部定义的 URL 部分包含空格, 此时应该使用标准的 URL 编码表示空格.

$ svn propget svn:externals paint
http://svn.thirdparty.com/repos/My%20Project "My Project"
http://svn.thirdparty.com/repos/%22Quotes%20Too%22 \"Quotes\ Too\"
$

Subversion 1.6 还支持为文件设置外部定义. 外部文件 (file externals) 的配置方式与目录相同, 外部文件 将以版本化文件的形式出现在工作副本里.

比如说仓库中有一个文件 /trunk/bikeshed/blue.html, 现在你想把文件在版本号 40 时的版本放在 /trunk/www/ 的工作副本里, 作为 green.html.

实现这个要求的外部定义是:

$ svn propget svn:externals www/
^/trunk/bikeshed/blue.html@40 green.html
$ svn update
Updating '.':

Fetching external item into 'www'
E    www/green.html
Updated external to revision 40.

Update to revision 103.
$ svn status
    X   www/green.html
$

可以看到, 把文件抓取到工作副本里时, Subversion 在外部文件的左边显示字 符 E, 执行 svn status 时, 在外部文 件的左边显示字符 X.

[警告] 警告

外部目录可以把工作副本检出到任意深度的子目录内, 中间缺失的目录会 被自动创建, 而外部文件只能检出到已存在的目录内.

使用 svn info 检查外部文件时, 可以看到外部文件 URL 与版本号:

$ svn info www/green.html 
Path: www/green.html
Name: green.html
Working Copy Root Path: /home/harry/projects/my-project
URL: http://svn.example.com/projects/my-project/trunk/bikeshed/blue.html
Relative URL: ^/trunk/bikeshed/blue.html
Repository Root: http://svn.example.com/projects/my-project
Repository UUID: b2a368dc-7564-11de-bb2b-113435390e17
Revision: 40
Node kind: file
Schedule: normal
Last Changed Author: harry
Last Changed Rev: 40
Last Changed Date: 2009-07-20 20:38:20 +0100 (Mon, 20 Jul 2009)
Text Last Updated: 2009-07-20 23:22:36 +0100 (Mon, 20 Jul 2009)
Checksum: 01a58b04617b92492d99662c3837b33b
$

因为外部文件是作为版本化的文件出现在工作副本里, 它们可以被修改, 如 果引用的是版本号 HEAD 的文件, 还可以提交修改, 提交后的修改不仅会出现在 外部文件时, 还包括被引用的文件. 然而在我们的例子里, 外部文件被指定了一 个较旧的版本号, 所以无法提交成功:

$ svn status
M   X   www/green.html
$ svn commit -m "change the color" www/green.html
Sending        www/green.html
svn: E155011: Commit failed (details follow):
svn: E155011: File '/trunk/bikeshed/blue.html' is out of date
$

定义外部文件时要始终牢记这点: 如果外部定义指向的是一个特定版本号的 文件, 将无法提交外部文件的修改. 如果用户希望可以提交外部文件的修改, 就不要指定除了 HEAD 之外的其他版本号, 这与没有指定 版本号是同样的效果.

不幸的是, Subversion 对外部定义的支持仍然不够理想. 外部文件与外部 目录都还有不足之外需要完善. 比如说外部定义的本地子目录不能包含父目录指示 符 .. (例如 ../../skins/myskin). 外部文件不能引用其他仓库的文件, 不能直接对外部文件进行移动或删除 (但可被 复制), 而是应该修改 svn:externals.

或许最令人失望的是由外部定义创建的工作副本与主工作副本 (属性 svn:externals 所在的工作副本) 之间是分离的, 而且 Subversion 也只能操作非正交的工作副本. 也就是说如果你想要提交一个或多个外部工作副本 里的修改, 你只能显式地在每个外部工作副本里执行 svn commit —在主工作副本内提交并不会影响外部工作副本.

我们已经介绍了 svn:externals 旧格式的缺点, 以及 Subversion 1.5 的新格式如何改善这些缺点, 但是在使用新的格式时注意不要 引入新的问题. 举个例子, 最新的客户端仍然支持旧的外部定义格式, 1.5 版以前 的客户端却不支持新格式. 如果用户把所有的外部定义格式都更新成新格式, 那 就相当于强迫所有的用户都要把客户端更新成最新版. 同时还要注意外部定义里的 -rNNN 部分—旧格式把 它作为限定版本号, 而新格式把它作为实施版本号 (除非显式指定, 否则使用 HEAD 作为限定版本号, 限定版本号与实施版本号的区别见 “限定版本号与实施版本号”一节).

[警告] 警告

外部工作副本是一个完全自给自足的工作副本, 和普通的工作副本没有 任何区别. 这是一个很方便的特性, 允许用户独立地对外部工作副本进行操作, 而不会受到主工作副本 (属性 svn:externals 所在的工 作副本) 的影响, 但要注意不要无意间修改工作副本而产生一些微妙的问题. 比如说外部定义可能指定了外部工作副本的版本号, 如果用户直接在外部工作 副本里执行 svn update, Subversion 将允许操作执行, 这会导致外部工作副本的版本号与外部定义指定的版本号不一致. 如果主工作 副本期望外部工作副本具有特定的内容, 那么使用 svn switch 把外部工作副本 (或其中的一部分) 切换到另一个 URL 也会造成 类似的问题.

除了 svn checkout, svn update, svn switchsvn export 外 (这些子命令实际上管理了 互不相交的 (disjoint) (或者说互相分离的), 检出了外部 定义的子目录), svn status 也可以识别外部工作副本. svn status 为外部 工作副本所在的子目录显示字符 X, 然后递归地显示外部 工作副本内的各个项目的状态. 为子命令添加选项 --ignore-externals 将会禁止子命令处理外部定义.