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.

使用 API

基于 Subversion 函数库 API 开发应用程序是一件相对比较直截了当 的事. Subversion 主要由 C 函数库组成, 它们的头文件 (.h) 在源代码包的 subversion/include 目录内. 如果你从源代码编译 安装了 Subversion, 这些头文件就会被复制到你的系统目录中 (例如 /usr/local/include). 这些头文件代表了能够被 用户访问到的 Subversion 函数库的全部函数与类型. Subversion 开发 社区非常注重 API 的文档—头文件里已经包含了关于如何使用 API 的 完整文档.

浏览头文件时, 你注意到的第一件事可能是 Subversion 的数据类型 和函数是被名字空间保护起来的, 详细地说, 每一个公开的 Subversion 符号 名都以 svn_ 开始, 然后是表示该符号定义所在的 函数库的编码 (例如 wc, client, fs 等), 然后是一个下划线 (_), 然后是符号名剩下的部分. 半公开的函数 (只被单个函数库的源文件们使用, 在该函数库外无法使用, 而且这些半公开函数的定义位于能使用它们的库函数 目录内) 使用不同的命名模式, 在函数库编码后面是两个连续的下划线 (_ _), 而非一个下划线. 源文件内的私有函数 没有特定的前缀, 它们都被声明为 static. 当然, 这些 命名规范对编译器没什么意义, 但却有助于开发人员理解函数和数据类型的 作用域.

学习如何使用 Subversion API 进行开发的另一个信息来源是官网的 编程文档 (http://subversion.apache.org/docs/community-guide/). 这篇文档主要针对 Subversion 开发人员, 但对于把 Subversion 用作第 三方函数库的开发人员来说同样有用.[73]

Apache 可移植运行库

除了 Subversion 自己的数据类型, 你还会看到很多以 apr_ 开始的数据类型—这些类型来自 Apache 可移植运行库 (Apache Portable Runtime, 简称 APR). APR 是 Apache 开发的可移植库, 最初是为了将服务器代码中与操作系统 相关的代码和不相关的代码分离开, 最终产生了一个提供通用 API 的 函数库, 这些 API 隐藏了操作系统之间的差异. 虽然 Apache HTTP 服务 器是 APR 的第一个用户, 但 Subversion 开发团队很快就意识到了 APR 的价值. 使用 APR 库意味着在 Subversion 代码中不存在依赖操作系统版 本的代码, 同时还意味着只要操作系统能编译和运行 Apache HTTP 服务器, 那它就能够编译和运行 Subversion 客户端程序, 目前 APR 支持的操作系统 包括所有的 Unix 系统, Win32, BeOS, OS/2 和 Mac OS X.

除了为不同的操作系统提供一致的系统调用实现外,[74] APR 还提供了许多定制化的数据类型, 例如动态数 组和哈希表, 这些数据类型在 Subversion 中用得非常广泛, 其中出现 得最多的类型是 apr_pool_t—APR 内存池— 它几乎出现在每一个 Subversion API 函数原型中. Subversion 使用 内存池完成所有内部的内存分配, (除非外部的函数库为它的参数指定了 一个不同的内存管理机制 [75]) 不过并不要求 Subversion API 的用户 也要用 APR 内存池完成内存管理, 他只需要向 Subversion API 提供一 个内存池参数即可. 这就要求 Subversion API 用户必须在编译时链接 APR, 必须调用 apr_initialize() 初始化 APR 子 系统, 然后调用 svn_pool_create(), svn_pool_clear()svn_pool_destroy() 完成内存池的创建和管理.

函数与不透明数据

为了充分利用异步化的行为, 以及向 Subversion API 用户提供钩子函数, 以便按照定制化的方式处理数据, 许多函数都接受这样一对参数: 一个 指向回调函数的指针以及一个指向不透明数据 (称为 baton) 的指针, baton 携带了回调函数所需的 各种上下文信息. Baton 通常就是一个 C 语言结构体, 它带有回调函数所需 的额外信息, 而这些信息对于调用回调函数的代码来说是不透明的.

URL 和路径要求

由于远程版本控制操作是 Subversion 存在的最重要理由, 因此我们 需要注意对国际化 (i18n) 的支持. 毕竟 远程 不仅意味 着 跨越办公室, 它还可能意味着 跨越国界. 为了支持国际化, Subversion 所有接受路径参数的公共接口都要求这些路径 是规范化的—可通过调用函数 svn_dirent_canonicalize()svn_uri_canonicalize() 分别得到规范化的本地 文件系统路径和 URL—而且是 UTF-8 编码. 举个例子, 任意一个使用 libsvn_client 的客户端程序在把路径传递给 Subversion 函数库之前, 都要先把本地编码的路径转换成 UTF-8 编码. 在得到 Subversion 产生的路径之后, 要先把这些路径转换成本地编码, 然后再交给非 Subversion 函数进行处理. 幸运的是, Subversion 提供了 一套函数 (见 subversion/include/svn_utf.h) 用于完成这些 编码转换.

另外, Subversion API 要求所有的 URL 参数必须符合 URI 编码 规则. 比如说你不能把文件 My File.txt 的 URL 写成 file:///home/username/My File.txt, 而应该写成 file:///home/username/My%20File.txt. 同样, Subversion 提供了函数 svn_path_uri_encode()svn_path_uri_decode() 分别用于 URI 的编码 和解码.

使用除了 C 和 C++ 之外的语言

如果你希望使用除了 C 之外的程序—例如 Python 或 Perl 脚本—调用 Subversion 函数库, 对此, Subversion 通过 SWIG (Simplified Wrapper and Interface Generator) 提供了一些支持. Subversion 的 SWIG 绑定位于 subversion/bindings/swig, 虽然它们还在不断 成熟中, 但是现在已经是可用的了. 这些绑定通过封装脚本, 把脚本语言 的数据类型翻译成 Subversion C 函数库所需的数据类型, 从而允许你 间接调用 Subversion API.

Subversion 开发团队已经花了很多精力为 Python, Perl 和 Ruby 开发功能齐全的, 由 SWIG 生成的绑定. 在一定程度上, 为这些脚本语言 准备 SWIG 接口所做的准备工作可以重用到 SWIG 支持的其他语言上 (包括 C#, Guile, Java, MzScheme, OCaml, PHP, Tcl 等). 然而, 如果接口过于复杂, SWIG 在不同语言之间翻译还需要帮助时, 那么开发人员还需要付出额外的开发工作. 关于 SWIG 的详细信息, 见官网 http://www.swig.org/.

Subversion 还拥有针对 Java 的语言绑定. Javahl 绑定 (位于 subversion/bindings/java) 不是基于 SWIG, 而是 Java 和手工编写的 JNI 的混合物. Javahl 涵盖了 Subversion 客户端的大部分 API, 它主要面对基于 Java 的 Subversion 客户端实现 和 IDE 集成.

虽然语言绑定从开发人员那儿受到的关注度比不上 Subversion 的核心 模块, 但通常而言这些绑定已经是生产就绪的了. 有大量的脚本 和应用程序, Subversion GUI 客户端和其他第三方工具都已经成功地把 Subversion 语言绑定应用到它们的 Subversion 集成中.

为了使用其他语言与 Subversion 交互, 如果我们还能有其他一些选择, 那将会是一件非常有价值的事情, 例如不是由 Subversion 开发社区提供的 语言绑定, 其中有两个值得我们关注, 第一个是 Barry Scott 开发的 PySVN 绑定 (https://pysvn.sourceforge.io/), 一种很流行的 Python 绑定, 与 Subversion 所提供的 Python 相比, PySVN 呈现的接口 更具有 Python 风格; 其二, 如果你正在寻找一种纯 Java 实现的 Subversion, 可以试试 SVNKit (http://svnkit.com/).

代码示例

例 8.1 “使用仓库层” 展示了一 个用 C 语言编写的代码示例, 说明了我们已经介绍过的几个概念. 示例 同时使用了仓库和文件系统接口 (可以从函数名的 svn_repos_svn_fs_ 前缀 看出) 来创建一个新的版本号, 该版本号添加了一个新目录. 你可以从示例 里看到 APR 内存池的用法—它们只是作为参数被传递给 Subversion 库函数. 示例还展示了 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 自动处理内存的分配与释放. 在 C 语言里, 你需要考虑各种 定制化数据类型 (例如 APR 函数库提供的数据类型), 这些数据类型用于 表示哈希项和路径列表, 但是 Python 内建了用于表示哈希 (Python 将其 称为 字典) 和列表的数据类型, 而且提供了丰富的函数 用来管理这些数据结构. 于是 SWIG (在 Subversion 语言绑定层的某些 定制化修改的帮助下) 负责把这些定制化的数据类型映射到目标语言的本 地类型, 用户就能更加直观地使用目标语言.

Subversion 的 Python 绑定也能运用到工作副本的操作中. 在本章的 前一节里, 我们提到了 libsvn_client 接口, 以及 它存在的唯一目标就是简化 Subversion 客户端程序的开发. 例 8.3 “用 Python 实现 svn status” 展示了如何使用 SWIG Python 绑定访问 libsvn_client 函数库, 来 实现一个简化版的 svn status 命令.

例 8.3. 用 Python 实现 svn status

#!/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 内建的 数据类型.

[警告] 警告

先把用户提供的路径参数转化成规范化形式 (调用 svn_dirent_canonicalize()svn_uri_canonicalize()), 然后再传递给其他 API, 否则的话可能会导致 Subversion C 库函数断言失败, 引起程序 异常退出.

使用 Python 调用 Subversion API 的用户可能对回调函数在 Python 中的实现比较感兴趣. 前面已经说过, Subversion C API 对编程范式 —回调函数/baton—的使用非常广泛, C 函数如果接受一个回调 函数和 baton, 那么 在 Python 中将只接受一个回调函数, 那么主调函数如何向回调函数 传递任意的上下文信息呢? 在 Python 里, 这是通过作用域规则和参数的 默认值来实现的. 你可以从 例 8.3 “用 Python 实现 svn status” 看到具体的 例子, 函数 svn_client_status2() 得到了一个 回调函数 (_status_callback()), 但却没有 baton—函数 _status_callback() 能够访问 到用户提供的前缀字符串是因为变量 prefix 自动落 到了函数的作用域内.



[73] 毕竟 Subversion 也使用了 Subversion 的 API.

[74] Subversion 尽可能使用 ANSI 规定的系统调用和数据类型.

[75] Berkeley DB 就是这样一种 函数库.

[76] 如果软件用到了 SVNKit, 或者用到了使用了 SVNKit 的软件, 那么 任意形式的二次发布必须携带关于如何获取软件完整源代码的信息. 详细 的授权见 http://svnkit.com/license.html.