共计 10866 个字符,预计需要花费 28 分钟才能阅读完成。
使用 API
使用 Subversion 库 API 开发应用看起来相当的直接,所有的公共头文件放在源文件的 subversion/include
目录,从源代码编译和安装 Subversion 本身,需要这些头文件拷贝到系统位置。这些头文件包括了所有用户和 Subversion 库可以访问的功能和类型。Subversion 开发者社区仔细的确保所有的公共 API 有完好的文档—直接引用头文件的文档。
你首先应该注意 Subversion 的数据类型和方法是命名空间保护的,每一个公共 Subversion 对象名以 svn_
开头,然后紧跟一个这个对象定义 (如wc
、client
和fs
等等)所在的库的简短编码,然后是一个下划线 (_
) 和后面的对象名称。半公开的方法 (库使用,但是但库之外代码不可以使用并且只可以在库自己的目录看到) 与这个命名模式不同,并不是库代码之后紧跟一个下划线,他们是用两个下划线 (__
)。给定源文件的私有方法没有特殊前缀,使用static
声明。当然,一个编译器不会关心命名习惯,只是用来区分给定方法或数据类型的应用范围。
关于 Subversion 的 API 编程的另一个好的资源是 hacking 指南,可以在 http://subversion.tigris.org/hacking.html
找到,这个文档包含了有用的信息,同时满足 Subversion 本身的开发者和将 Subversion 作为第三方库的开发者。[50]
Apache 可移植运行库
伴随 Subversion 自己的数据类型,你会看到许多 apr
开头的数据类型引用—来自 Apache 可移植运行库 (APR) 的对象。APR 是 Apache 可移植运行库,源自为了服务器代码的多平台性,尝试将不同的操作系统特定字节与操作系统无关代码隔离。结果就提供了一个基础 API 的库,只有一些适度区别—或者是广泛的—来自各个操作系统。Apache HTTP 服务器很明显是 APR 库的第一个用户,Subversion 开发者立刻发现了使用 APR 库的价值。意味着 Subversion 没有操作系统特定的代码,也意味着 Subversion 客户端可以在 Server 存在的平台编译和运行。当前这个列表包括,各种类型的 Unix、Win32、OS/ 2 和 Mac OS X。
除了提供了跨平台一致的系统调用,[51]APR 给 Subversion 对多种数据类型有快速的访问,如动态数组和哈希表。Subversion 在代码中广泛使用这些类型,但是 Subversion 的 API 原型中最常见的 APR 类型是 apr_pool_t—APR 内存池,Subversion 使用内部缓冲池用来进行内存分配(除非外部库在 API 传递参数时需要一个不同的内存管理模式),[52] 而且一个人如果针对 Subversion 的 API 编码不需要做同样的事情,他们可以在需要时给 API 提供缓冲池,这意味着 Subversion 的 API 使用者也必须链接到 APR,必须调用 apr_initialize()
来初始化 APR 子系统,而且在使用 Subversion API 时必须创建和管理池,通常是使用 svn_pool_create()
、svn_pool_clear()
和svn_pool_destroy()
。
URL 和路径需求
因为分布式版本控制操作是 Subversion 存在的重点,有意义来关注一下国际化 (i18n) 支持。毕竟,当“分布式 ”或许意味着“ 横跨办公室 ”,它也意味着“ 横跨全球”。为了更容易一点,Subversion 的所有公共接口只接受路径参数, 这些参数是传统的,使用 UTF- 8 编码。这意味着,举个例子,任何新的使用 libsvn_client 接口客户端库,在把这些参数传递给 Subversion 库前,需要首先将路径从本地代码转化为 UTF- 8 代码,然后将 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 的编码和解码。
使用 C 和 C++ 以外的语言
除 C 语言以外,如果你对使用其他语言结合 Subversion 库感兴趣—如 Python 脚本或是 Java 应用—Subversion 通过简单包裹生成器 (SWIG) 提供了最初的支持。Subversion 的 SWIG 绑定位于subversion/bindings/swig
,并且慢慢的走向成熟进入可用状态。这个绑定允许你直接调用 Subversion 的 API 方法,使用包裹器会把脚本数据类型转化为 Subversion 需要的 C 语言库类型。
非常不幸,Subversion 的语言绑定缺乏对核心 Subversion 模块的关注,但是,花了很多力气处理创建针对 Python、Perl 和 Ruby 的功能绑定,在一定程度上,在这些接口上的工作量可以在其他语言的 SWIG(包括 C#、Guile、Java、MzScheme、OCaml、PHP、Tcl 等等)接口上得到重用。然而,为了完成复杂的 API,一些 SWIG 接口仍然需要额外的编程工作,关于 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 项目链接页里 (http://subversion.tigris.org/links.html
) 找到其他绑定的链接,但是有一些流行的绑定我觉得应该特别留意。首先是 Python 的流行绑定,Barry Scott 的 PySVN(http://pysvn.tigris.org/
)。PySVN 鼓吹它们提供了更多 Python 样式的接口,而不像 Subversion 自己的 Python 绑定的 C 样式接口。对于希望寻求 Subversion 纯 Java 实现的人,可以看看 SVNKit(http://svnkit.com/
),也就是从头使用 Java 编写的 Subversion。你必须要小心,SVNKit 没有采用 Subversion 的核心库,其行为方式没有确保与 Subversion 匹配。
代码样例
例 8.1“使用版本库层”包含了一段 C 代码 (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 包裹的功能版本—这个功能会实际上首先运行一个如果存在的pre-commit
钩子脚本,然后提交事务,最后会运行一个 post-commit
钩子脚本。钩子提供了一种特别的报告机制,不是真的属于核心文件系统库本身。(关于 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 语言中,为表示路径和条目的 hash 需要处理自定义的数据类型 (例如 APR 提供的库),但是 Python 有 hash(叫做“dictionaries”),并且是内置数据类型,而且还提供了一系列操作这些类型的函数,所以 SWIG(通过 Subversion 的语言绑定层的自定义帮助) 要小心的将这些自定义数据类型映射到目标语言的数据类型,这为目标语言的用户提供了一个更加直观的接口。
Subversion 的 Python 绑定也可以用来进行工作拷贝的操作,在本章前面的小节中,我们提到过 libsvn_client
接口,它存在的目的就是简化编写 Subversion 客户端的难度,例 8.3“一个 Python 状态爬虫”是一个例子,讲的是如何使用 SWIG 绑定创建一个扩展版本的 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 自动语言生成中的一点问题 (这是对应复杂 C 结构的一种工厂方法)。也需要注意传递给程序的路径(象最后一个) 是通过 svn_path_canonicalize()
执行的,因为要防止触发 Subversion 底层 C 库的断言,也就是防止导致程序立刻随意退出。