本文档旨在描述 Subversion 1.1。如果您运行的是更新版本的 Subversion,我们强烈建议您访问 https://svnbooks.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_fs | Subversion 文件系统库 |
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 直接连接到它。
客户端本身也突出了 Subversion 设计中的模块化。虽然 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 文件系统有一个大多数文件系统没有的第三维——时间![35] 在文件系统接口中,几乎所有具有 path 参数的函数也需要一个 root 参数。此 svn_fs_root_t 参数描述了修订版本或 Subversion 事务(通常只是一个要修订的版本),并提供了理解以下内容之间的差异所需的第三维上下文/foo/bar在版本 32 中,路径与版本 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.0.1 (r9023) compiled Mar 17 2004, 09:31:13 Copyright (C) 2000-2004 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' schema - handles 'https' schema * ra_local : Module for accessing a repository on local disk. - handles 'file' schema * ra_svn : Module for accessing a repository using the svn network protocol. - handles 'svn' schema
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 协议的扩展,它们允许在 Web 上共享和版本化文件。 Apache 2.0 带有 mod_dav,这是一个了解 DAV 对 HTTP 的扩展的 Apache 模块。 不过,Subversion 本身提供了 mod_dav_svn,这是另一个与(实际上,作为 mod_dav 的后端)mod_dav 一起工作的 Apache 模块,以提供 Subversion 对 WebDAV 和 DeltaV 的特定实现。
在通过 HTTP 与仓库通信时,RA 加载器库会选择 libsvn_ra_dav 作为适当的访问模块。 Subversion 客户端对通用 RA 接口进行调用,libsvn_ra_dav 将这些调用(体现了相当大规模的 Subversion 操作)映射到一组 HTTP/WebDAV 请求。 使用 Neon 库,libsvn_ra_dav 将这些请求传输到 Apache 服务器。 Apache 接收这些请求(就像它接收你的 Web 浏览器可能发出的通用 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 客户端尝试与 Apache 服务器(其自身的 mod_ssl 可以“说同一种语言”)使用 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 仓库的通信都需要强大的服务器进程和网络层。 对于只想访问其本地磁盘上的仓库的用户,他们可以使用fileURL 和 libsvn_ra_local 提供的功能。 此 RA 模块直接与仓库和文件系统库绑定,因此根本不需要网络通信。
Subversion 要求作为fileURL 部分包含的服务器名称为localhost或者为空,并且没有端口规范。换句话说,您的 URL 应该看起来像以下两种情况之一:file://localhost/path/to/repos或file:///path/to/repos.
此外,请注意 Subversion 的fileURL 不能像普通fileURL 一样在普通 Web 浏览器中使用。当您尝试在普通 Web 浏览器中查看fileURL 时,它会通过直接检查文件系统来读取和显示该位置文件的原始内容。但是,Subversion 的资源存在于虚拟文件系统中(请参阅 名为“Repository Layer”的部分),您的浏览器将无法理解如何读取该文件系统。
在客户端方面,Subversion 工作副本是所有操作发生的地方。客户端库实现的大部分功能的存在是为了管理工作副本——包含文件和其他子目录的目录,它们充当一个或多个存储库位置的本地可编辑“镜像”——并将更改传播到存储库访问层和从存储库访问层传播。
Subversion 的工作副本库 libsvn_wc 负责管理工作副本中的数据。为了实现这一点,库在每个工作副本目录中一个特殊的子目录内存储有关每个工作副本目录的管理信息。这个子目录名为.svn,它存在于每个工作副本目录中,包含各种其他文件和目录,这些文件和目录记录状态并为管理操作提供私有工作区。对于熟悉 CVS 的用户,这个.svn子目录在目的上类似于CVS在 CVS 工作副本中找到的管理目录。有关.svn管理区域的更多信息,请参阅本章中的 名为“Inside the Working Copy Administration Area”的部分。
Subversion 客户端库 libsvn_client 承担最广泛的责任;它的工作是将工作副本库的功能与存储库访问层的功能混合在一起,然后为任何希望执行一般版本控制操作的应用程序提供最高级别的 API。例如,函数svn_client_checkout以 URL 作为参数。它将此 URL 传递给 RA 层,并与特定存储库建立经过身份验证的会话。然后它要求存储库提供某个树,并将此树发送到工作副本库,工作副本库随后将完整的副本写入磁盘(.svn目录和所有内容)。
客户端库旨在供任何应用程序使用。虽然 Subversion 源代码包含标准的命令行客户端,但编写任何数量的基于 GUI 的客户端都应该非常容易。Subversion 的新 GUI(或任何新的客户端,实际上)无需围绕包含的命令行客户端进行笨拙的包装——它们可以通过 libsvn_client API 完全访问命令行客户端使用的相同功能、数据和回调机制。