本手册旨在介绍 Subversion 1.4。如果您运行的是更新版本的 Subversion,我们强烈建议您访问 https://svnbook.subversion.org.cn/ 并查阅适合您的 Subversion 版本的书籍。
针对 Subversion 库 API 开发应用程序非常直观。Subversion 主要是一组 C 库,其头文件(.h)位于源代码树的 subversion/include
目录中。在从源代码构建和安装 Subversion 本身时,这些头文件会被复制到您的系统位置(例如,/usr/local/include
)。这些头文件表示 Subversion 库用户可以访问的所有函数和类型的总和。Subversion 开发人员社区非常重视确保公共 API 文档齐全——请直接参考头文件以获取这些文档。
在检查公共头文件时,您可能会注意到 Subversion 的数据类型和函数是命名空间受保护的。也就是说,每个公共 Subversion 符号名称都以 svn_
开头,后跟定义符号的库的简短代码(例如 wc
、client
、fs
等),然后是一个下划线 (_
),最后是符号名称的其余部分。半公共函数(在给定库的源代码文件之间使用,但不在该库之外的代码中使用,并在库目录本身中找到)与其命名方案不同,它们在库代码后使用两个下划线 (__
),而不是单个下划线。特定源代码文件中私有的函数没有特殊的前缀,并且被声明为 static
。当然,编译器不关心这些命名约定,但它们有助于阐明给定函数或数据类型的范围。
关于针对 Subversion API 进行编程的另一个信息来源是该项目的自身黑客指南,该指南可以在 http://subversion.tigris.org/hacking.html 中找到。本文档包含有用的信息,虽然它针对的是 Subversion 本身的开发人员和潜在开发人员,但也同样适用于将 Subversion 视为一组第三方库进行开发的人员。 [54]
除了 Subversion 自己的数据类型之外,您还会看到许多对以 apr_
开头的 datatypes 的引用——来自 Apache Portable Runtime (APR) 库的符号。APR 是 Apache 的可移植性库,最初从其服务器代码中提取出来,旨在将代码的 OS 特定部分与 OS 独立部分分离。结果是一个库,它提供了一个通用的 API 来执行在不同的操作系统之间略有不同(或完全不同)的操作。虽然 Apache HTTP Server 显然是 APR 库的第一个用户,但 Subversion 开发人员立即认识到使用 APR 的价值。这意味着 Subversion 本身几乎没有 OS 特定代码。此外,这意味着 Subversion 客户端可以在 Apache HTTP Server 本身运行的任何地方编译和运行。目前,此列表包括所有类型的 Unix、Win32、BeOS、OS/2 和 Mac OS X。
除了提供跨操作系统一致的系统调用实现之外,[55] APR 立即为 Subversion 提供了许多自定义数据类型,例如动态数组和哈希表。Subversion 广泛使用这些类型。但也许最普遍的 APR 数据类型是在几乎所有 Subversion API 原型中找到的 apr_pool_t——APR 内存池。Subversion 在内部使用池来满足其所有内存分配需求(除非外部库需要其 API 传递数据的不同内存管理机制),[56] 并且虽然针对 Subversion API 编码的人员不需要这样做,但他们 确实 需要为需要它们的 API 函数提供池。这意味着 Subversion API 的用户也必须链接到 APR,必须调用 apr_initialize()
初始化 APR 子系统,然后必须创建和管理用于 Subversion API 调用的池,通常使用 svn_pool_create()
、svn_pool_clear()
和 svn_pool_destroy()
。
由于 Subversion 存在的全部意义是远程版本控制操作,因此对国际化 (i18n) 支持进行了一些关注是合理的。毕竟,虽然“远程”可能意味着“跨办公室”,但它也可能意味着“跨越全球。”为了促进这一点,Subversion 的所有接受路径参数的公共接口都期望这些路径被规范化并以 UTF-8 编码。例如,这意味着任何驱动 libsvn_client 接口的新客户端二进制文件都需要首先将路径从特定于语言环境的编码转换为 UTF-8,然后将这些路径传递给 Subversion 库,然后将 Subversion 的任何结果输出路径重新转换为语言环境的编码,然后再将这些路径用于非 Subversion 目的。幸运的是,Subversion 提供了一套函数(请参见 subversion/include/svn_utf.h
),任何程序都可以使用这些函数进行这些转换。
此外,Subversion API 要求所有 URL 参数都被正确地 URI 编码。因此,您不是将 file:///home/username/My File.txt
作为名为 My File.txt
的文件的 URL 传递,而是需要传递 file:///home/username/My%20File.txt
。同样,Subversion 提供了您的应用程序可以使用的辅助函数——svn_path_uri_encode()
和 svn_path_uri_decode()
,分别用于 URI 编码和解码。
如果您有兴趣将 Subversion 库与除 C 程序以外的其他程序一起使用——例如 Python 或 Perl 脚本——Subversion 通过简化包装器和接口生成器 (SWIG) 提供了一些支持。Subversion 的 SWIG 绑定位于 subversion/bindings/swig
中。它们仍在不断成熟,但可以使用。这些绑定允许您间接调用 Subversion API 函数,使用将您的脚本语言的本机数据类型转换为 Subversion 的 C 库所需的数据类型的包装器。
为 Python、Perl 和 Ruby 创建功能性 SWIG 生成的绑定已经付出了巨大的努力。在某种程度上,为这些语言准备 SWIG 接口文件所做的工作可以在为 SWIG 支持的其他语言生成绑定时重复使用(包括 C#、Guile、Java、MzScheme、OCaml、PHP 和 Tcl 等版本)。但是,需要一些额外的编程来弥补 SWIG 需要一些帮助才能在不同语言之间进行转换的复杂 API。有关 SWIG 本身的更多信息,请参见该项目的网站 http://www.swig.org/.
Subversion 还为 Java 提供了语言绑定。JavaJL 绑定(位于 Subversion 源代码树的 subversion/bindings/java
中)不是基于 SWIG 的,而是 javah 和手工编码的 JNI 的混合体。JavaHL 大部分涵盖了 Subversion 客户端 API,专门针对 Java 基于 Subversion 客户端和 IDE 集成的实现者。
Subversion 的语言绑定往往缺乏对核心 Subversion 模块的开发人员关注程度,但通常可以被视为可用于生产环境。今天,许多脚本和应用程序、替代 Subversion GUI 客户端和其他第三方工具正在成功地使用 Subversion 的语言绑定来完成他们的 Subversion 集成。
值得注意的是,还有其他方法可以使用其他语言与 Subversion 交互:Subversion 的替代绑定,这些绑定根本不是由 Subversion 开发人员社区提供的。您可以在 Subversion 项目的链接页面(位于 http://subversion.tigris.org/links.html)上找到这些替代绑定的链接,但我们认为有一些特别值得注意的热门绑定。首先,Barry Scott 的 PySVN 绑定 (http://pysvn.tigris.org/) 是与 Python 绑定的一个热门选择。PySVN 拥有比 Subversion 自身的 Python 绑定提供的更类似 C 的 API 更类似 Python 的接口。对于那些希望寻找 Subversion 的纯 Java 实现的人,请查看 SVNKit (http://svnkit.com/),它是从头开始用 Java 重写的 Subversion。但是,您应该谨慎行事——因为 SVNKit 没有使用核心 Subversion 库,所以它不能保证其行为与 Subversion 本身一致。
示例 8.1,“使用仓库层” 包含一个代码段(用 C 语言编写),说明了我们一直在讨论的一些概念。它使用仓库和文件系统接口(可以通过函数名称的 svn_repos_
和 svn_fs_
前缀确定),以创建一个新的修订版,其中添加了一个目录。您可以看到 APR 池的使用,该池被传递以用于内存分配。此外,代码揭示了关于 Subversion 错误处理的有点模糊的事实——所有 Subversion 错误都必须被明确处理,以避免内存泄漏(在某些情况下,应用程序也会失败)。
示例 8.1. 使用仓库层
/* Convert a Subversion error into a simple boolean error code. * * NOTE: Subversion errors must be cleared (using svn_error_clear()) * because they are allocated from the global pool, else memory * leaking occurs. */ #define INT_ERR(expr) \ do { \ svn_error_t *__temperr = (expr); \ if (__temperr) \ { \ svn_error_clear(__temperr); \ return 1; \ } \ return 0; \ } while (0) /* 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. Return zero if the operation completes * successfully, non-zero otherwise. */ static int 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. */ INT_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. */ INT_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. */ INT_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. */ INT_ERR(svn_fs_txn_root(&txn_root, txn, pool)); /* Create our new directory under the transaction root, at the path * NEW_DIRECTORY. */ INT_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); } INT_ERR(err); }
请注意,在 示例 8.1,“使用仓库层” 中,代码也可以使用 svn_fs_commit_txn()
提交事务。但文件系统 API 对仓库库的钩子机制一无所知。如果您希望 Subversion 仓库在每次提交事务时自动执行一些非 Subversion 任务(例如,向您的开发人员邮件列表发送一封描述该事务中所有更改的电子邮件),您需要使用该函数的 libsvn_repos 包装版本,它添加了钩子触发功能——在本例中,为 svn_repos_fs_commit_txn()
。(有关 Subversion 仓库钩子的更多信息,请参阅 名为“实现仓库钩子”的部分。)
现在让我们切换语言。 示例 8.2,“使用 Python 的仓库层” 是一个示例程序,它使用 Subversion 的 SWIG Python 绑定递归地遍历最年轻的仓库修订版,并打印遍历期间到达的各种路径。
示例 8.2. 使用 Python 的仓库层
#!/usr/bin/python """Crawl a repository, printing versioned object path names.""" import sys import os.path import svn.fs, svn.core, svn.repos def crawl_filesystem_dir(root, directory): """Recursively crawl DIRECTORY under ROOT in the filesystem, and return a list of all the paths at or below DIRECTORY.""" # Print the name of this path. print directory + "/" # Get the directory entries for DIRECTORY. entries = svn.fs.svn_fs_dir_entries(root, directory) # Loop over the entries. names = entries.keys() for name in names: # Calculate the entry's full path. full_path = directory + '/' + name # If the entry is a directory, recurse. The recursion will return # a list with the entry and all its children, which we will add to # our running list of paths. if svn.fs.svn_fs_is_dir(root, full_path): crawl_filesystem_dir(root, full_path) else: # Else it's a file, so print its path here. print full_path def crawl_youngest(repos_path): """Open the repository at REPOS_PATH, and recursively crawl its youngest revision.""" # Open the repository at REPOS_PATH, and get a reference to its # versioning filesystem. repos_obj = svn.repos.svn_repos_open(repos_path) fs_obj = svn.repos.svn_repos_fs(repos_obj) # Query the current youngest revision. youngest_rev = svn.fs.svn_fs_youngest_rev(fs_obj) # Open a root object representing the youngest (HEAD) revision. root_obj = svn.fs.svn_fs_revision_root(fs_obj, youngest_rev) # Do the recursive crawl. crawl_filesystem_dir(root_obj, "") if __name__ == "__main__": # Check for sane usage. if len(sys.argv) != 2: sys.stderr.write("Usage: %s REPOS_PATH\n" % (os.path.basename(sys.argv[0]))) sys.exit(1) # Canonicalize the repository path. repos_path = svn.core.svn_path_canonicalize(sys.argv[1]) # Do the real work. crawl_youngest(repos_path)
在 C 中,同一个程序需要处理 APR 的内存池系统。但是 Python 自动处理内存使用,Subversion 的 Python 绑定遵循该约定。在 C 中,您将使用自定义数据类型(例如 APR 库提供的那些)来表示条目哈希和路径列表,但 Python 具有哈希(称为“字典”)和列表作为内置数据类型,并提供丰富的函数集来操作这些类型。因此 SWIG(在 Subversion 的语言绑定层的某些自定义的帮助下)负责将这些自定义数据类型映射到目标语言的本机数据类型。这为该语言的用户提供了更直观的界面。
Subversion Python 绑定也可以用于工作副本操作。在本节前面,我们提到了 libsvn_client
接口,以及它存在的唯一目的是简化编写 Subversion 客户端的过程。 示例 8.3,“Python 状态爬虫” 是一个简短的示例,说明如何通过 SWIG Python 绑定访问该库来重新创建 svn status 命令的缩减版本。
示例 8.3. Python 状态爬虫
#!/usr/bin/env python """Crawl a working copy directory, printing status information.""" import sys import os.path import getopt import svn.core, svn.client, svn.wc def generate_status_code(status): """Translate a status value into a single-character status code, using the same logic as the Subversion command-line client.""" code_map = { svn.wc.svn_wc_status_none : ' ', svn.wc.svn_wc_status_normal : ' ', svn.wc.svn_wc_status_added : 'A', svn.wc.svn_wc_status_missing : '!', svn.wc.svn_wc_status_incomplete : '!', svn.wc.svn_wc_status_deleted : 'D', svn.wc.svn_wc_status_replaced : 'R', svn.wc.svn_wc_status_modified : 'M', svn.wc.svn_wc_status_merged : 'G', svn.wc.svn_wc_status_conflicted : 'C', svn.wc.svn_wc_status_obstructed : '~', svn.wc.svn_wc_status_ignored : 'I', svn.wc.svn_wc_status_external : 'X', svn.wc.svn_wc_status_unversioned : '?', } return code_map.get(status, '?') def do_status(wc_path, verbose): # Calculate the length of the input working copy path. wc_path_len = len(wc_path) # Build a client context baton. ctx = svn.client.svn_client_ctx_t() def _status_callback(path, status, root_path_len=wc_path_len): """A callback function for svn_client_status.""" # Print the path, minus the bit that overlaps with the root of # the status crawl text_status = generate_status_code(status.text_status) prop_status = generate_status_code(status.prop_status) print '%s%s %s' % (text_status, prop_status, path[wc_path_len + 1:]) # Do the status crawl, using _status_callback() as our callback function. svn.client.svn_client_status(wc_path, None, _status_callback, 1, verbose, 0, 0, ctx) def usage_and_exit(errorcode): """Print usage message, and exit with ERRORCODE.""" stream = errorcode and sys.stderr or sys.stdout stream.write("""Usage: %s OPTIONS WC-PATH Options: --help, -h : Show this usage message --verbose, -v : Show all statuses, even uninteresting ones """ % (os.path.basename(sys.argv[0]))) sys.exit(errorcode) if __name__ == '__main__': # Parse command-line options. try: opts, args = getopt.getopt(sys.argv[1:], "hv", ["help", "verbose"]) except getopt.GetoptError: usage_and_exit(1) verbose = 0 for opt, arg in opts: if opt in ("-h", "--help"): usage_and_exit(0) if opt in ("-v", "--verbose"): verbose = 1 if len(args) != 1: usage_and_exit(2) # Canonicalize the repository path. wc_path = svn.core.svn_path_canonicalize(args[0]) # Do the real work. try: do_status(wc_path, verbose) except svn.core.SubversionException, e: sys.stderr.write("Error (%d): %s\n" % (e[1], e[0])) sys.exit(1)
与 示例 8.2,“使用 Python 的仓库层” 中的情况一样,该程序是无池的,并且大部分使用正常的 Python 数据类型。对 svn_client_ctx_t()
的调用具有欺骗性,因为公共 Subversion API 没有这样的函数——这恰好是 SWIG 的自动语言生成稍微泄露出来的地方(该函数是 Python 版本的相应复杂 C 结构的一种工厂函数)。还要注意,传递给该程序的路径(与最后一个一样)将通过 svn_path_canonicalize()
运行,因为不这样做会导致触发底层 Subversion C 库关于此类事情的断言,从而转化为相当直接和突然的程序中止。