1.使用API

Posted on Posted in 9.嵌入svn

使用API

使用Subversion库API开发应用看起来相当的直接,所有的公共头文件放在源文件的subversion/include目录,从源代码编译和安装Subversion本身,需要这些头文件拷贝到系统位置。这些头文件包括了所有用户和Subversion库可以访问的功能和类型。Subversion开发者社区仔细的确保所有的公共API有完好的文档—直接引用头文件的文档。

你首先应该注意Subversion的数据类型和方法是命名空间保护的,每一个公共Subversion对象名以svn_开头,然后紧跟一个这个对象定义(如wcclientfs等等)所在的库的简短编码,然后是一个下划线(_)和后面的对象名称。半公开的方法(库使用,但是但库之外代码不可以使用并且只可以在库自己的目录看到)与这个命名模式不同,并不是库代码之后紧跟一个下划线,他们是用两个下划线(__)。给定源文件的私有方法没有特殊前缀,使用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] 或仅仅是在紧密地程序优化中玩弄什么东西。