使用API
使用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()
。
使用内存池编程
几乎每一个使用过C语言的开发者曾经感叹令人畏缩的内存管理,分配足够的内存,并且追踪内存的分配,在不需要时释放内存—这个任务会非常复杂。当然,如果没有正确地做到这一点会导致程序毁掉自己,或者更加严重一点,把电脑搞瘫。
另一方面高级语言使开发者完全摆脱了内存管理,[53]Java和Python之类的语言使用垃圾收集原理,在需要的时候分配对象内存,在不使用时进行清理。
APR提供了一种叫做池基础的中等的内存管理方法,允许开发者以一种低分辨率的方式控制内存—每块(或池“pool”)的内存,而不是每个对象。不是使用malloc()
和其他按照对象分配内存的方式,你要求APR从内存创建一段内存池,当你结束使用在池中创建的对象,你销毁池,可以有效地取消其中的对象消耗的内存。通过池,你不需要跟踪每个对象的内存释放,你的程序只需要跟踪这些对象,将对象分配到池中,而池的生命周期(池的创建和删除之间的时间)满足所有对象的需要。
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库的断言,也就是防止导致程序立刻随意退出。
[50] 当然,Subversion使用Subversion的API。
[51] Subversion使用尽可能多ANSI系统调用和数据类型。
[52] Neon和Berkeley DB就是这种库的例子。
[53] 或仅仅是在紧密地程序优化中玩弄什么东西。