本文档尚未完善,内容可能发生较大变动,可能无法准确描述 Apache™ Subversion® 软件的任何已发布版本。将此页面设为书签或推荐他人访问此页面可能不是明智之举。请访问 https://svnbook.subversion.org.cn/ 获取本书的稳定版本。
针对 Subversion 库 API 开发应用程序相当简单。Subversion 主要是一组 C 库,带有位于源代码树 subversion/include
目录中的头 (.h
) 文件。在您从源代码构建并安装 Subversion 本身时,这些头文件将被复制到您的系统位置(例如,/usr/local/include
)。这些头文件代表了 Subversion 库用户可以访问的所有函数和类型的完整集合。Subversion 开发者社区一丝不苟地确保公共 API 文档齐全 - 请直接参考头文件以获取该文档。
在检查公共头文件时,您可能首先注意到 Subversion 的数据类型和函数是命名空间保护的。也就是说,每个公共 Subversion 符号名称都以 svn_
开头,后跟定义符号的库的简短代码(例如 wc
、client
、fs
等),然后是一个下划线 (_
),最后是符号名称的其余部分。半公共函数(在给定库的源文件之间使用,但不在该库外部的代码中使用,并且在库目录本身中找到)与这种命名方案不同,它们在库代码后面使用双下划线 (_ _
) 而不是单个下划线。特定源文件专用的函数没有特殊的前缀,并声明为 static
。当然,编译器对这些命名约定并不感兴趣,但它们有助于阐明给定函数或数据类型的范围。
有关针对 Subversion API 进行编程的另一个很好的信息来源是该项目的自身黑客指南,您可以在 https://subversion.org.cn/docs/community-guide/ 中找到它。本文档包含有用的信息,虽然它针对 Subversion 本身的开发人员和潜在开发人员,但同样适用于将 Subversion 作为一组第三方库进行开发的人员。[78]
除了 Subversion 自己的数据类型外,您还会看到许多对以 apr_
开头的數據类型的引用——来自 Apache 可移植运行时 (APR) 库的符号。APR 是 Apache 的可移植性库,最初是从其服务器代码中分离出来,试图将操作系统特定的位与代码的操作系统无关部分分离。结果是一个库,它为执行从操作系统到操作系统的操作提供了一个通用 API,这些操作的差异很小或很大。虽然 Apache HTTP 服务器显然是 APR 库的第一个用户,但 Subversion 开发人员立即认识到使用 APR 的价值。这意味着 Subversion 本身几乎没有操作系统特定的代码。此外,这意味着 Subversion 客户端可以在 Apache HTTP 服务器运行的任何地方编译和运行。目前,此列表包括所有版本的 Unix、Win32、BeOS、OS/2 和 Mac OS X。
除了提供跨操作系统一致的系统调用实现外,[79] APR 立即为 Subversion 提供了对许多自定义数据类型的访问,例如动态数组和哈希表。Subversion 广泛使用这些类型。但也许最普遍的 APR 数据类型是在几乎所有 Subversion API 原型中找到的 apr_pool_t
——APR 内存池。Subversion 在内部使用池来满足其所有内存分配需求(除非外部库要求通过其 API 传递数据的不同内存管理机制),[80] 虽然针对 Subversion API 进行编码的人员不需要这样做,但她 确实 需要为需要它们的 API 函数提供池。这意味着 Subversion API 的用户还必须链接到 APR,必须调用 apr_initialize()
来初始化 APR 子系统,然后必须创建和管理池供 Subversion API 调用使用,通常通过使用 svn_pool_create()
、svn_pool_clear()
和 svn_pool_destroy()
。
为了便于 “流式”(异步)行为并为 Subversion C API 的使用者提供钩子,以便以可自定义的方式处理信息,API 中的许多函数都接受一对参数:指向回调函数的指针,以及指向称为 baton 的内存块的指针,该内存块用于该回调函数的上下文信息。Baton 通常是 C 结构,其中包含回调函数需要的额外信息,但这些信息不是由驱动 API 函数直接提供给回调函数的。
由于 Subversion 存在的全部意义是远程版本控制操作,因此有必要关注国际化 (i18n) 支持。毕竟,虽然 “远程” 可能意味着 “跨办公室”,但它也可能意味着 “跨越地球”。为了便于此,所有接受路径参数的 Subversion 公共接口都希望这些路径是规范化的——最容易通过将它们传递给 svn_dirent_canonicalize()
或 svn_uri_canonicalize()
(分别取决于您要规范化本地系统路径还是 URL)来完成——并以 UTF-8 编码。例如,这意味着任何驱动 libsvn_client
接口的新客户端二进制文件都需要首先将路径从特定于区域设置的编码转换为 UTF-8,然后将这些路径传递给 Subversion 库,然后将 Subversion 的任何结果输出路径转换回区域设置的编码,然后将这些路径用于非 Subversion 目的。幸运的是,Subversion 提供了一套任何程序都可以用来进行这些转换的函数(参见 subversion/include/svn_utf.h
)。
此外,Subversion API 要求所有 URL 参数都正确地进行 URI 编码。因此,您需要传递 file:///home/username/My%20File.txt
而不是 file:///home/username/My File.txt
作为名为 My File.txt
的文件的 URL。同样,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 本身的更多信息,请参见该项目的网站:https://www.swig.org/.
Subversion 还具有针对 Java 的语言绑定。javahl 绑定(位于 Subversion 源代码树的 subversion/bindings/java
中)不是基于 SWIG 的,而是 Java 和手动编码的 JNI 的混合体。javahl 涵盖了大多数 Subversion 客户端 API,专门针对基于 Java 的 Subversion 客户端和 IDE 集成的实现者。
Subversion 的语言绑定往往缺乏对核心 Subversion 模块的开发人员关注程度,但通常可以作为生产就绪进行信任。许多脚本和应用程序、替代 Subversion GUI 客户端以及其他第三方工具今天正在成功地使用 Subversion 的语言绑定来完成其 Subversion 集成。
值得在这里注意的是,还有其他选择可以使用其他语言与 Subversion 交互:并非由 Subversion 开发者社区提供的其他 Subversion 绑定。我们认为有几个流行的选择特别值得注意。首先,Barry Scott 的 PySVN 绑定 (https://pysvn.sourceforge.io/) 是与 Python 绑定的一个流行选择。PySVN 吹嘘它比 Subversion 自身 Python 绑定提供的更像 C 的 API 具有更具 Python 风格的接口。如果您正在寻找 Subversion 的纯 Java 实现,请查看 SVNKit (https://svnkit.com/),它是从头开始用 Java 重写的 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, nonzero 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_repos_fs_begin_txn_for_commit2(&txn, repos, youngest_rev, apr_hash_make(pool), 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_dirent_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_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, prefix): # Build a client context baton. ctx = svn.client.svn_client_create_context() def _status_callback(path, status): """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) prefix_text = '' if prefix is not None: prefix_text = prefix + " " print '%s%s%s %s' % (prefix_text, text_status, prop_status, path) # Do the status crawl, using _status_callback() as our callback function. revision = svn.core.svn_opt_revision_t() revision.type = svn.core.svn_opt_revision_head svn.client.svn_client_status2(wc_path, revision, _status_callback, svn.core.svn_depth_infinity, verbose, 0, 0, 1, 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 Print working copy status, optionally with a bit of prefix text. Options: --help, -h : Show this usage message --prefix ARG : Print ARG, followed by a space, before each line of output --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", "prefix=", "verbose"]) except getopt.GetoptError: usage_and_exit(1) verbose = 0 prefix = None for opt, arg in opts: if opt in ("-h", "--help"): usage_and_exit(0) if opt in ("--prefix"): prefix = arg if opt in ("-v", "--verbose"): verbose = 1 if len(args) != 1: usage_and_exit(2) # Canonicalize the working copy path. wc_path = svn.core.svn_dirent_canonicalize(args[0]) # Do the real work. try: do_status(wc_path, verbose, prefix) except svn.core.SubversionException, e: sys.stderr.write("Error (%d): %s\n" % (e.apr_err, e.message)) sys.exit(1)
与 示例 8.2,“使用 Python 的库层” 一样,此程序是无池的,并且大部分使用普通的 Python 数据类型。
警告 | |
---|---|
在将用户提供的路径传递给其他 API 函数之前,请通过相应的规范化函数( |
对 Subversion API 的 Python 版本的用户来说,回调函数的实现尤其令人感兴趣。如前所述,Subversion 的 C API 大量使用了回调函数/baton 模式。在 C 语言中接受函数和 baton 对的 API 函数在 Python 中只接受回调函数参数。那么,调用方如何将任意上下文信息传递给回调函数?在 Python 中,这是通过利用 Python 的作用域规则和默认参数值来实现的。你可以在 示例 8.3,“Python 状态爬虫” 中看到它的实际应用。 svn_client_status2()
函数被传递了一个回调函数(_status_callback()
),但没有传递 baton——_status_callback()
可以访问用户提供的 prefix 字符串,因为该变量自动地处于函数的作用域内。
[78] 毕竟,Subversion 也使用 Subversion 的 API。
[79] Subversion 尽可能地使用 ANSI 系统调用和数据类型。
[80] Berkeley DB 是这类库的一个示例。
[81] 任何形式的再分发都必须附带有关如何获取使用 SVNKit 的软件以及任何使用使用 SVNKit 的软件的伴随软件的完整源代码的信息。有关详细信息,请参阅 https://svnkit.com/license.html。