本手册编写是为了描述 Subversion 1.2 版本。如果您运行的是更新版本的 Subversion,我们强烈建议您访问 https://svnbook.subversion.org.cn/ 并参阅适合您 Subversion 版本的版本。
目录
Subversion 是一个开源软件项目,在 Apache 风格的软件许可下开发。该项目得到 CollabNet, Inc. 的资金支持,CollabNet, Inc. 是一家位于加州的软件开发公司。围绕 Subversion 开发形成的社区始终欢迎能够为项目贡献时间和精力的新成员。鼓励志愿者以任何可能的方式提供帮助,无论是发现和诊断错误、改进现有源代码还是完善全新的功能。
本章面向那些希望通过直接参与源代码来帮助 Subversion 继续发展的人。我们将介绍一些软件更详细的内容,这些内容是那些开发 Subversion 本身或编写基于 Subversion 库的全新工具的人应该了解的技术细节。如果您没有预见到自己会以这种方式参与软件,您可以放心地跳过本章,因为您作为 Subversion 用户的体验不会受到影响。
Subversion 采用模块化设计,实现为 C 库的集合。每个库都有明确定义的用途和接口,大多数模块被认为存在于三个主要层之一——仓库层、仓库访问 (RA) 层或客户端层。我们将在稍后检查这些层,但首先,请参阅我们对 Subversion 库的简要清单 表 8.1“Subversion 库简要清单”。为了保持一致性,我们将使用它们的无扩展名 Unix 库名称引用这些库(例如:libsvn_fs、libsvn_wc、mod_dav_svn)。
表 8.1 Subversion 库简要清单
库 | 描述 |
---|---|
libsvn_client | 客户端程序的主要接口 |
libsvn_delta | 树和字节流差异化例程 |
libsvn_diff | 上下文差异化和合并例程 |
libsvn_fs | 文件系统公共部分和模块加载器 |
libsvn_fs_base | Berkeley DB 文件系统后端 |
libsvn_fs_fs | 本地文件系统 (FSFS) 后端 |
libsvn_ra | 仓库访问公共部分和模块加载器 |
libsvn_ra_dav | WebDAV 仓库访问模块 |
libsvn_ra_local | 本地仓库访问模块 |
libsvn_ra_svn | 自定义协议仓库访问模块 |
libsvn_repos | 仓库接口 |
libsvn_subr | 各种有用的子例程 |
libsvn_wc | 工作副本管理库 |
mod_authz_svn | Apache 授权模块,用于通过 WebDAV 访问 Subversion 仓库 |
mod_dav_svn | Apache 模块,用于将 WebDAV 操作映射到 Subversion 操作 |
在 表 8.1“Subversion 库简要清单” 中,“各种”这个词只出现了一次,这是一个好兆头。Subversion 开发团队非常重视确保功能位于正确的层和库中。模块化设计也许最大的优势在于从开发人员的角度来看,它没有复杂性。作为开发人员,您可以快速制定那种“大局观”,这使您可以轻松地查找到特定功能的所在位置。
模块化的另一个好处是能够用实现相同 API 的全新库替换给定模块,而不会影响代码库的其余部分。从某种意义上说,这已经在 Subversion 中发生了。libsvn_ra_dav、libsvn_ra_local 和 libsvn_ra_svn 都实现了相同的接口。所有三个都与仓库层通信——libsvn_ra_dav 和 libsvn_ra_svn 通过网络进行通信,而 libsvn_ra_local 直接连接到它。libsvn_fs_base 和 libsvn_fs_fs 库是另一个例子。
客户端本身也突出了 Subversion 设计中的模块化。虽然 Subversion 本身只带有一个命令行客户端程序,但有几个第三方程序提供了各种形式的客户端 GUI。这些 GUI 使用与库存命令行客户端相同的 API。Subversion 的 libsvn_client 库是为设计有效的 Subversion 客户端所需的大多数功能的一站式商店(参见 名为“客户端层”的部分)。
当提到 Subversion 的仓库层时,我们通常指的是两个库——仓库库和文件系统库。这些库为版本控制数据的各种修订版提供存储和报告机制。此层通过仓库访问层与客户端层连接,从 Subversion 用户的角度来看,这是“另一端”的内容。
Subversion 文件系统可以通过 libsvn_fs API 访问,它不是可以在操作系统中安装的内核级文件系统(如 Linux ext2 或 NTFS),而是一个虚拟文件系统。它不存储“文件”和“目录”作为真实文件和目录(即,您可以使用您最喜欢的 shell 程序导航到的类型),而是使用两个可用的抽象存储后端之一——Berkeley DB 数据库环境或平面文件表示形式。(要了解有关两个仓库后端的更多信息,请参见 名为“仓库数据存储”的部分。)但是,开发社区对让未来版本的 Subversion 能够使用其他后端数据库系统(也许通过 Open Database Connectivity (ODBC) 之类的机制)表现出浓厚的兴趣。
libsvn_fs 导出的文件系统 API 包含您对任何其他文件系统 API 预期的那类功能:您可以创建和删除文件和目录、复制和移动它们、修改文件内容等等。它还具有一些不太常见的特性,例如能够在每个文件或目录上添加、修改和删除元数据(“属性”)。此外,Subversion 文件系统是一个版本控制文件系统,这意味着当您对树进行更改时,Subversion 会记住树在更改之前的样子。以及之前更改之前的状态。以及之前状态。等等,一直追溯到(并且略微超越)您第一次开始将内容添加到文件系统的时刻。
您对树进行的所有修改都在 Subversion 事务的上下文中完成。以下是修改文件系统的简化一般例程
开始 Subversion 事务。
进行更改(添加、删除、属性修改等)。
提交您的事务。
一旦提交事务,您的文件系统修改就会被永久存储为历史工件。每个循环都会生成您树的单个新修订版,并且每个修订版都会永远作为“过去的样子”的不可变快照访问。
文件系统接口提供的大多数功能都是对文件系统路径上发生的操作。也就是说,从文件系统外部来看,描述和访问文件的各个修订版以及目录的主要机制是使用路径字符串,例如 /foo/bar
,就像您通过您最喜欢的 shell 程序寻址文件和目录一样。您可以通过将它们的路径传递到正确的 API 函数来添加新的文件和目录。您可以通过相同的机制查询有关它们的信息。
不过,与大多数文件系统不同,路径本身不足以识别 Subversion 中的文件或目录。将目录树视为一个二维系统,其中节点的同级表示一种左右运动,而下降到子目录表示一种向下运动。图 8.1“文件和目录在二维空间中” 展示了树的典型表示,就是这样。
当然,Subversion 文件系统有一个大多数文件系统都没有的第三个维度——时间![42] 在文件系统接口中,几乎每个具有 path
参数的函数也需要一个 root
参数。这个 svn_fs_root_t 参数描述了修订版或 Subversion 事务(通常只是一个将要发生的修订版),并提供了理解修订版 32 中 /foo/bar
与修订版 98 中的相同路径之间差异所需的第三维上下文。 图 8.2,“版本控制时间——第三个维度!” 将修订历史显示为 Subversion 文件系统宇宙的附加维度。
正如我们之前提到的,libsvn_fs API 看起来和感觉起来都像其他任何文件系统,除了它有这个奇妙的版本控制功能。它的设计目的是供任何对版本控制文件系统感兴趣的程序使用。并非巧合的是,Subversion 本身对该功能感兴趣。但是,虽然文件系统 API 应该足以满足基本的文件和目录版本控制支持,但 Subversion 需要更多——这就是 libsvn_repos 的用武之地。
Subversion 仓库库 (libsvn_repos) 基本上是围绕文件系统功能的包装库。该库负责创建仓库布局,确保底层文件系统已初始化,等等。libsvn_repos 还实现了一组钩子——当特定操作发生时由仓库代码执行的脚本。这些脚本对于通知、授权或仓库管理员所需的任何目的都有用。这种类型的功能以及仓库库提供的其他实用程序与实现版本控制文件系统没有直接关系,这就是为什么它被放置到自己的库中的原因。
希望使用 libsvn_repos API 的开发人员会发现它不是围绕文件系统接口的完整包装器。也就是说,只有文件系统活动一般周期中的一些主要事件被仓库接口包装。其中一些包括 Subversion 事务的创建和提交,以及修订版属性的修改。这些特定事件被仓库层包装,因为它们与它们相关的钩子。将来,其他事件可能会被仓库 API 包装。但是,所有剩余的文件系统交互将继续通过 libsvn_fs API 直接进行。
例如,以下代码段说明了如何使用仓库和文件系统接口来创建文件系统的新修订版,其中添加了一个目录。请注意,在本例中(以及本书中的所有其他示例),SVN_ERR()
宏只是检查它包装的函数的非成功错误返回,如果存在,则返回该错误。
示例 8.1. 使用仓库层
/* Create a new directory at the path NEW_DIRECTORY in the Subversion repository located at REPOS_PATH. Perform all memory allocation in POOL. This function will create a new revision for the addition of NEW_DIRECTORY. */ static svn_error_t * make_new_directory (const char *repos_path, const char *new_directory, apr_pool_t *pool) { svn_error_t *err; svn_repos_t *repos; svn_fs_t *fs; svn_revnum_t youngest_rev; svn_fs_txn_t *txn; svn_fs_root_t *txn_root; const char *conflict_str; /* Open the repository located at REPOS_PATH. */ SVN_ERR (svn_repos_open (&repos, repos_path, pool)); /* Get a pointer to the filesystem object that is stored in REPOS. */ fs = svn_repos_fs (repos); /* Ask the filesystem to tell us the youngest revision that currently exists. */ SVN_ERR (svn_fs_youngest_rev (&youngest_rev, fs, pool)); /* Begin a new transaction that is based on YOUNGEST_REV. We are less likely to have our later commit rejected as conflicting if we always try to make our changes against a copy of the latest snapshot of the filesystem tree. */ SVN_ERR (svn_fs_begin_txn (&txn, fs, youngest_rev, pool)); /* Now that we have started a new Subversion transaction, get a root object that represents that transaction. */ SVN_ERR (svn_fs_txn_root (&txn_root, txn, pool)); /* Create our new directory under the transaction root, at the path NEW_DIRECTORY. */ SVN_ERR (svn_fs_make_dir (txn_root, new_directory, pool)); /* Commit the transaction, creating a new revision of the filesystem which includes our added directory path. */ err = svn_repos_fs_commit_txn (&conflict_str, repos, &youngest_rev, txn, pool); if (! err) { /* No error? Excellent! Print a brief report of our success. */ printf ("Directory '%s' was successfully added as new revision " "'%ld'.\n", new_directory, youngest_rev); } else if (err->apr_err == SVN_ERR_FS_CONFLICT) { /* Uh-oh. Our commit failed as the result of a conflict (someone else seems to have made changes to the same area of the filesystem that we tried to modify). Print an error message. */ printf ("A conflict occurred at path '%s' while attempting " "to add directory '%s' to the repository at '%s'.\n", conflict_str, new_directory, repos_path); } else { /* Some other error has occurred. Print an error message. */ printf ("An error occurred while attempting to add directory '%s' " "to the repository at '%s'.\n", new_directory, repos_path); } /* Return the result of the attempted commit to our caller. */ return err; }
在前面的代码段中,对仓库和文件系统接口都进行了调用。我们也可以使用 svn_fs_commit_txn()
提交事务。但是,文件系统 API 对仓库库的钩子机制一无所知。如果您希望您的 Subversion 仓库在每次提交事务时自动执行一组非 Subversion 任务(例如,向您的开发人员邮件列表发送一封描述该事务中所做所有更改的电子邮件),您需要使用 libsvn_repos 包装的该函数版本——svn_repos_fs_commit_txn()
。该函数实际上会首先运行 pre-commit
钩子脚本(如果存在),然后提交事务,最后将运行 post-commit
钩子脚本。钩子提供了一种特殊的报告机制,它不属于核心文件系统库本身。(有关 Subversion 仓库钩子的更多信息,请参见 名为“钩子脚本”的部分。)
钩子机制需求只是将单独的仓库库与文件系统代码的其他部分抽象出来的原因之一。libsvn_repos API 为 Subversion 提供了许多其他重要实用程序。其中包括以下功能:
创建、打开、销毁和对 Subversion 仓库以及该仓库中包含的文件系统执行恢复步骤。
描述两个文件系统树之间的差异。
查询与文件系统中一组文件修改的所有(或部分)修订版相关的提交日志消息。
生成文件系统的人类可读“转储”,即文件系统中修订版的完整表示。
解析该转储格式,将转储的修订版加载到另一个 Subversion 仓库中。
随着 Subversion 的不断发展,仓库库将与文件系统库一起发展,以提供更多功能和可配置选项支持。
如果 Subversion 仓库层位于“线的另一端”,那么仓库访问层就是这条线本身。该层负责在客户端库和仓库之间传递数据,包括 libsvn_ra 模块加载器库、RA 模块本身(目前包括 libsvn_ra_dav、libsvn_ra_local 和 libsvn_ra_svn),以及一个或多个这些 RA 模块需要的任何其他库,例如 libsvn_ra_dav 通信的 mod_dav_svn Apache 模块或 libsvn_ra_svn 的服务器 svnserve。
由于 Subversion 使用 URL 来标识其仓库资源,因此 URL 架构的协议部分(通常为 file:
、http:
、https:
或 svn:
)用于确定哪个 RA 模块将处理通信。每个模块都会注册它知道的“说”的协议列表,以便 RA 加载器可以在运行时确定使用哪个模块来完成手头的任务。您可以通过运行 svn --version 来确定哪些 RA 模块可用于 Subversion 命令行客户端,以及它们声称支持哪些协议。
$ svn --version svn, version 1.2.3 (r15833) compiled Sep 13 2005, 22:45:22 Copyright (C) 2000-2005 CollabNet. Subversion is open source software, see http://subversion.tigris.org/ This product includes software developed by CollabNet (http://www.Collab.Net/). The following repository access (RA) modules are available: * ra_dav : Module for accessing a repository via WebDAV (DeltaV) protocol. - handles 'http' scheme - handles 'https' scheme * ra_svn : Module for accessing a repository using the svn network protocol. - handles 'svn' scheme * ra_local : Module for accessing a repository on local disk. - handles 'file' scheme
libsvn_ra_dav 库旨在供在与它们通信的服务器(特别是使用包含 http:
或 https:
协议部分的 URL 访问的服务器)不同的机器上运行的客户端使用。要了解此模块的工作原理,我们应该首先提到此特定仓库访问层配置中的另外两个关键组件——强大的 Apache HTTP Server 和 Neon HTTP/WebDAV 客户端库。
Subversion 的主要网络服务器是 Apache HTTP Server。Apache 是一个久经考验的可扩展开源服务器进程,已准备好投入使用。它可以承受高网络负载并在许多平台上运行。Apache 服务器支持许多不同的标准身份验证协议,并且可以通过使用模块扩展以支持许多其他协议。它还支持优化,例如网络流水线和缓存。通过使用 Apache 作为服务器,Subversion 免费获得了所有这些功能。而且,由于大多数防火墙已经允许 HTTP 流量通过,因此系统管理员通常甚至不必更改其防火墙配置以允许 Subversion 工作。
Subversion 使用 HTTP 和 WebDAV(带 DeltaV)与 Apache 服务器通信。您可以在本章的 WebDAV 部分中阅读更多相关内容,但简而言之,WebDAV 和 DeltaV 是标准 HTTP 1.1 协议的扩展,它们使您可以通过网络共享和版本控制文件。Apache 2.0 及更高版本附带 mod_dav,这是一个了解 DAV 对 HTTP 扩展的 Apache 模块。但是,Subversion 本身提供了 mod_dav_svn,它是另一个与 mod_dav 协同工作的 Apache 模块(实际上是 mod_dav 的后端),以提供 Subversion 对 WebDAV 和 DeltaV 的特定实现。
当通过 HTTP 与仓库通信时,RA 加载器库会选择 libsvn_ra_dav 作为合适的访问模块。Subversion 客户端对通用 RA 接口进行调用,libsvn_ra_dav 将这些调用(体现了相当大规模的 Subversion 操作)映射到一组 HTTP/WebDAV 请求。使用 Neon 库,libsvn_ra_dav 将这些请求传输到 Apache 服务器。Apache 接收这些请求(与您的网络浏览器可能发出的通用 HTTP 请求完全相同),注意到这些请求指向配置为 DAV 位置的 URL(使用 <Location>
指令在 httpd.conf
中),并将请求传递给自己的 mod_dav 模块。当配置正确时,mod_dav 知道对任何与文件系统相关的需求使用 Subversion 的 mod_dav_svn,而不是 Apache 附带的通用 mod_dav_fs。因此,最终,客户端正在与 mod_dav_svn 通信,后者直接绑定到 Subversion 仓库层。
但这只是对实际发生的交换的简化描述。例如,Subversion 仓库可能受 Apache 的授权指令的保护。这会导致最初尝试与仓库通信因授权原因而被 Apache 拒绝。此时,libsvn_ra_dav 从 Apache 获取通知,表明提供的身份验证信息不足,并回调到客户端层以获取一些更新的身份验证数据。如果数据提供正确,并且用户具有 Apache 需要的权限,则 libsvn_ra_dav 下一次自动尝试执行原始操作将被授予,一切都会好起来。如果无法提供足够的身份验证信息,请求最终将失败,客户端会将失败报告给用户。
通过使用 Neon 和 Apache,Subversion 在其他几个复杂领域也获得了免费功能。例如,如果 Neon 找到 OpenSSL 库,它允许 Subversion 客户端尝试使用 SSL 加密通信与 Apache 服务器通信(其自身的 mod_ssl 可以“说这种语言”)。此外,Neon 本身和 Apache 的 mod_deflate 都可以理解“deflate”算法(与 PKZIP 和 gzip 程序使用的算法相同),因此可以跨网络以更小、更压缩的块发送请求。Subversion 希望将来支持的其他复杂功能包括自动处理服务器指定的重定向(例如,当仓库已移动到新的规范 URL 时)以及利用 HTTP 流水线。
除了标准的 HTTP/WebDAV 协议外,Subversion 还提供了一个使用自定义协议的 RA 实现。libsvn_ra_svn 模块实现了自己的网络套接字连接,并与托管仓库的机器上的独立服务器——svnserve
程序——通信。客户端使用 svn://
架构访问仓库。
这种 RA 实现缺乏上一节中提到的 Apache 的大多数优点;但是,它可能仍然对某些系统管理员有吸引力。它配置和运行起来非常容易;设置一个 svnserve
进程几乎是即时的。它也比 Apache 小得多(从代码行数来看),因此更易于审计,无论是出于安全原因还是其他原因。此外,一些系统管理员可能已经有了 SSH 安全基础设施,并且希望 Subversion 使用它。使用 ra_svn 的客户端可以轻松地通过 SSH 隧道协议。
并非所有与 Subversion 仓库的通信都需要强大的服务器进程和网络层。对于只想访问本地磁盘上的仓库的用户,他们可以使用 file:
URL 和 libsvn_ra_local 提供的功能。此 RA 模块直接与仓库和文件系统库绑定,因此完全不需要网络通信。
Subversion 要求作为 file:
URL 部分包含的服务器名称为 localhost
或为空,并且没有端口规范。换句话说,您的 URL 应类似于 file://127.0.0.1/path/to/repos
或 file:///path/to/repos
。
另外,请注意 Subversion 的 file:
URL 无法像典型的 file:
URL 那样在普通 Web 浏览器中使用。当您尝试在普通 Web 浏览器中查看 file:
URL 时,它会通过直接检查文件系统来读取并显示该位置的文件内容。但是,Subversion 的资源存在于虚拟文件系统中(参见 名为“仓库层”的部分),而您的浏览器将无法理解如何读取该文件系统。
在客户端,Subversion 工作副本是所有操作发生的地方。客户端库实现的大部分功能都旨在管理工作副本——包含文件和其他子目录的目录,这些目录充当一个或多个仓库位置的本地、可编辑的“反映”——并将更改传播到仓库访问层和从仓库访问层传播。
Subversion 的工作副本库 libsvn_wc 负责管理工作副本中的数据。为了实现这一点,该库在每个工作副本目录中一个特殊的子目录中存储有关每个工作副本目录的管理信息。这个名为 .svn
的子目录存在于每个工作副本目录中,并包含各种其他文件和目录,这些文件和目录记录状态并为管理操作提供私有工作区。对于熟悉 CVS 的人来说,这个 .svn
子目录在目的上类似于 CVS 工作副本中找到的 CVS
管理目录。有关 .svn
管理区域的更多信息,请参见本章中 名为“工作副本管理区域内部”的部分。
Subversion 客户端库 libsvn_client 承担最广泛的责任;它的工作是将工作副本库的功能与仓库访问层的功能结合起来,然后为想要执行一般版本控制操作的任何应用程序提供最高级的 API。例如,函数 svn_client_checkout()
以 URL 作为参数。它将此 URL 传递给 RA 层,并打开与特定仓库的已认证会话。然后,它向仓库请求某个树,并将此树发送到工作副本库,然后工作副本库将完整的副本写入磁盘(.svn
目录以及所有)。
客户端库旨在供任何应用程序使用。虽然 Subversion 源代码包含一个标准命令行客户端,但编写任意数量的基于客户端库的 GUI 客户端应该非常容易。Subversion 的新 GUI(或任何新的客户端)不必是包含的命令行客户端的笨拙包装器——它们可以通过 libsvn_client API 完全访问命令行客户端使用的相同功能、数据和回调机制。