Apache Subversion 1.15 源码深度剖析-第 7 章 客户端库、认证与基础设施

2 阅读1小时+

Apache Subversion 1.15 源码深度剖析-第 7 章 客户端库、认证与基础设施

本系列前六章沿着「编辑器 → 增量传输 → FSFS → RA → 网络协议 → 工作副本」一路下沉,看清了数据是怎么从磁盘一路搬到线上的。这一章要回头把「客户端」这条链路补齐:libsvn_client 作为一切命令行/图形客户端共用的门面,它如何把一次 svn checkout 拆成几十个函数调用;libsvn_subr 这座「基础设施公共底座」提供了认证、缓存、APR 池、错误链、路径与编码、CLI 参数解析、流装饰器等所有上层模块都要用的零件。理解这两块,才算真正看懂「一个 Subversion 客户端为什么这样组织」。

本章涉及的主要文件

  • 客户端公开门面:subversion/include/svn_client.h(svn_client_ctx_t、_checkout*/_update*/_commit*/_merge* 接口族)
  • 客户端内部契约:subversion/libsvn_client/client.h(svn_client__* 双下划线内部 API)
  • 客户端上下文实现与调度:subversion/libsvn_client/checkout.c、update.c、ra.c
  • 合并跟踪:subversion/include/svn_mergeinfo.h、subversion/include/svn_props.h、subversion/libsvn_client/mergeinfo.c、mergeinfo.h、merge.c
  • 认证:subversion/include/svn_auth.h、subversion/libsvn_subr/auth.c、auth.h、win32_crypto.c、macos_keychain.c、gpg_agent.c
  • 缓存:subversion/include/svn_cache_config.h、subversion/libsvn_subr/cache-config 相关、cache.c、cache-inprocess.c、cache-membuffer.c、cache-memcache.c、cache-null.c
  • 池 / 错误 / 路径 / 编码 / 流:subversion/include/svn_pools.h、svn_error.h、svn_error_codes.h、svn_types.h、svn_dirent_uri.h、libsvn_subr/dirent_uri.c、svn_utf.h、svn_io.h、svn_subst.h
  • CLI 框架:subversion/include/svn_opt.h、subversion/libsvn_subr/opt.c、opt_subcommand.c、subversion/svn/cl.h
  • 语言绑定:subversion/bindings/swig/、subversion/bindings/javahl/
  • 能力协商与演进:subversion/include/svn_ra.h(SVN_RA_CAPABILITY_*)、CHANGES

读完本章你将能够

  1. 说清 svn_client_ctx_t 里每一个字段的用途与版本来源,并据此写出一个最小可用的 svn_client_ctx_t 初始化流程;
  2. 解释「数字后缀 = 接口代数」这套 ABI 兼容策略,明白为什么 svn_client_update4() 是 1.15 里最新的一代、而任务清单里幻想的 _update5 在本树里并不存在;
  3. 从 svn_client_checkout4() 出发,逐层追到 svn_ra_open5() 与 svn_client__update_internal(),画出一次 checkout 的完整调用链;
  4. 读懂 svn:mergeinfo 的语法(revisionrange / revisioneelement / rangelist / revisionline / top)并能手工解析一段真实的 mergeinfo 字符串(含 * 不可继承标记);
  5. 复现认证系统的「责任链」:一次认证失败后,libsvn_auth 如何沿 provider 次序换凭据,SVN_AUTH_PARAM_* 又是怎么贯穿其中的;
  6. 掌握 svn_cache_config_t 三个字段与 inprocess / membuffer / memcache 三后端的选择逻辑;理解池的 result_pool/scratch_pool 约定、错误链的包装规则、四套路径工具族的边界,以及 svn_stream_t 装饰器的叠加顺序。

7.1 libsvn_client:客户端库总览

libsvn_client 是 Subversion 的「门面库(facade)」。它不做底层脏活——真正的文件读写交给 libsvn_wc,网络会话交给 libsvn_ra,仓库访问交给 libsvn_repos/libsvn_fs,字符串与池交给 libsvn_subr。它的职责是编排:把「用户点了一下 checkout」翻译成一串对 libsvn_ra 与 libsvn_wc 的调用,并在中间维护「客户端上下文」这一堆回调与缓存。

这一点从目录结构就看得出来:subversion/libsvn_client/ 下没有 editor.c 之外的文件格式解析,也很少直接碰磁盘格式;它大量使用的是 svn_ra_*、svn_wc_*、svn_client__* 这些更高层的动词。因此它是「依赖方向最高」的一个库,几乎所有 CLI 子命令(subversion/svn/*.c)都直接调用它。

7.1.1 客户端上下文 svn_client_ctx_t 逐字段表

svn_client_ctx_t 定义在 svn_client.h 第 958–1086 行。头文件开头的注释明确写着:为了避免向后兼容问题,客户端应当用 svn_client_create_context() 而不是自己 malloc 这个结构——这正是后面「数字后缀」策略的另一种体现(结构体只增不改)。

下面是逐字段对照表(字段名 → 用途 → 引入版本),全部来自本次实际读取的 svn_client.h:

字段类型用途引入
auth_batonsvn_auth_baton_t *主认证 baton,认证责任链的入口1.0
notify_funcsvn_wc_notify_func_t旧通知回调(@deprecated,1.1 API)1.0
notify_batonvoid *notify_func 的 baton(@deprecated)1.0
log_msg_funcsvn_client_get_commit_log_t旧日志消息回调(@deprecated,1.2 API)1.0
log_msg_batonvoid *log_msg_func 的 baton(@deprecated)1.0
configapr_hash_t *配置文件名的哈希 → svn_config_t *,键如 "config";为 NULL 时用内建默认1.0
cancel_funcsvn_cancel_func_t客户端取消(中断)回调1.0
cancel_batonvoid *传给取消回调的 baton1.0
notify_func2svn_wc_notify_func2_t新通知回调,默认转发到 notify_func1.2
notify_baton2void *notify_func2 的 baton1.2
log_msg_func2svn_client_get_commit_log2_t日志消息回调,NULL 表示退回 log_msg_func1.3
log_msg_baton2void *log_msg_func2 的 baton1.3
progress_funcsvn_ra_progress_notify_func_t网络进度通知(可为 NULL)1.3
progress_batonvoid *progress_func 的 baton1.3
log_msg_func3svn_client_get_commit_log3_t最新日志消息回调,NULL 时依次退回 log_msg_func2、log_msg_func1.5
log_msg_baton3void *log_msg_func3 的 baton1.5
mimetypes_mapapr_hash_t *MIME 类型映射(用于 svn:mime-type 自动判定的排除表)1.5
conflict_funcsvn_wc_conflict_resolver_func_t旧冲突解决回调(@deprecated)1.5
conflict_batonvoid *conflict_func 的 baton1.5
client_nameconst char *自定义客户端名(写进仓库的 svn:client 语义、User-Agent 等),可为 NULL1.5
conflict_func2svn_wc_conflict_resolver_func2_t新冲突解决回调,NULL 时退回 conflict_func1.7
conflict_baton2void *conflict_func2 的 baton1.7
wc_ctxsvn_wc_context_t *工作副本上下文;由 svn_client_create_context() 初始化,绝不为 NULL1.7
check_tunnel_funcsvn_ra_check_tunnel_func_t判断某个隧道是否应交给 open_tunnel_func1.9
open_tunnel_funcsvn_ra_open_tunnel_func_t为 ra_svn 连接自建隧道,覆盖配置文件定义(仅 ra_svn 使用)1.9
tunnel_batonvoid *check_tunnel_func / open_tunnel_func 的 baton1.9

从这张表能读出三条设计原则:

  1. 只增不改:新字段永远追加在尾部(client_name 之后是 check_tunnel_func),旧字段保留并标 @deprecated,用「NULL 即退回旧回调」的写法维持兼容。例如 log_msg_func3 为 NULL 时「依次退回 log_msg_func2、log_msg_func」——这是一条显式的降级链,写在字段注释里。
  2. 可空性有语义:config 与 mimetypes_map 为 NULL 表示「用内建默认」,而 wc_ctx 注释里斩钉截铁地说「should never be NULL」,因为它是所有 svn_wc_* 调用的载体。
  3. 回调成对出现:每个回调函数都紧跟一个 *_baton,这是 Subversion 全局的 C 风格约定(见 svn_auth_provider_t、svn_txdelta_* 等所有地方)。

字段的版本标注(1.0 → 1.9)本身就是一部「客户端能力编年史」:1.2 加 notify_func2,1.3 加 progress_func,1.5 加 log_msg_func3/mimetypes_map/client_name,1.7 加 wc_ctx/conflict_func2,1.9 加隧道回调。到 1.15 为止,这个结构体再没有新增字段——这从侧面说明客户端上层的接口已经趋于稳定。

7.1.2 「数字后缀 = 接口代数」:ABI 兼容策略

Subversion 最容易被误解、也最能体现其工程纪律的一处设计,是公开 API 的命名后缀:svn_client_checkout4、svn_client_update4、svn_client_commit6、svn_client_merge5、svn_client_diff7……这些数字不是版本号,而是接口代数(generation)。

规则很简单:当某个函数的参数列表需要变化(增加新参数、改变参数语义、改返回值类型)时,不原地修改旧函数,而是新增一个 N+1 的新函数,把旧函数标记为 SVN_DEPRECATED 并保留其实现(通常在 deprecated.c 里转发到新函数)。这样:

  • 已编译链接到旧 ABI 的第三方程序不会因为「结构体/函数签名变了」而崩溃;
  • 源码级仍可调用旧函数,只是会收到编译期 deprecation 警告;
  • 新旧函数可以共存,直到某个大版本再决定裁剪。

我们本次实际读到的 svn_client.h 里,同一族函数的代数关系如下(行号为实测):

函数族代数序列(新 → 旧)最新一代位置
checkout_checkout4 → _checkout3 → _checkout2 → _checkoutcheckout4 @1272
update_update4 → _update3 → _update2 → _updateupdate4 @1425
commit_commit6 → _commit5 → _commit4 → _commit3 → _commit2 → _commitcommit6 @2272
merge_merge5 → _merge4 → _merge3 → _merge2 → _mergemerge5 @3834
diff_diff7 → _diff6 → _diff5 → _diff4 → _diff3 → _diff2 → _diffdiff7 @3233

这里要纠正一个流传较广的想当然:1.15 里 update 的最新一代是 svn_client_update4,并不存在 svn_client_update5。我们在 svn_client.h 中 grep svn_client_update5 无任何命中,grep svn_client_update4 命中 第 1425 行,其实现位于 libsvn_client/update.c 第 727 行。同样地,merge 停在第 5 代、commit 停在第 6 代、diff 停在第 7 代。所谓「代数」是按需要递增,不是每版 +1。

以 checkout 为例,四代的差异非常典型地展示了「新参数如何逐步引入」(以下签名全部来自实测读取):

/* libsvn_client/checkout.c:285 —— 1.15 最新一代 */
svn_error_t *
svn_client_checkout4(svn_revnum_t *result_rev,
                     const char *URL,
                     const char *path,
                     const svn_opt_revision_t *peg_revision,
                     const svn_opt_revision_t *revision,
                     svn_depth_t depth,
                     svn_boolean_t ignore_externals,
                     svn_boolean_t allow_unver_obstructions,
                     const svn_version_t *wc_format_version,   /* 新增 */
                     svn_tristate_t store_pristine,            /* 新增 */
                     svn_client_ctx_t *ctx,
                     apr_pool_t *pool);

对比它「降一级」的前代 svn_client_checkout3,差别恰恰就是 wc_format_version(工作副本格式版本,配合 1.15 的「多工作副本格式」特性)与 store_pristine(pristineless 模式开关)两个参数:旧 API 里把它们「钉死」为 NULL 与 svn_tristate_unknown,注释原文是「Similar to svn_client_checkout4() but with @a wc_format_version set to @c NULL and @a store_pristine set to #svn_tristate_unknown」。再往前的 checkout2 干脆没有 depth/ignore_externals 之外的深度概念,而是用 recurse(TRUE→svn_depth_infinity,FALSE→svn_depth_files);checkout 连 peg_revision 都没有,固定 svn_opt_revision_unspecified。

这套「新函数 + 旧壳转发」的模式在 C ABI 层面极为稳健:符号名 svn_client_checkout3 永远指向一段调用约定不变的代码,谁都不会因为升级库而链接失败。

7.1.3 内部 API:svn_client__* 双下划线约定

公开头文件里是「代数」函数,而库内部共享的、不承诺 ABI 稳定的接口,则统一使用双下划线前缀 svn_client__。这是 Subversion 跨文件的命名规约:单下划线(svn_client_)是公开 API,双下划线(svn_client__)是内部 API,且只在 libsvn_client/*.c 之间共享,声明集中在 client.h。

我们实测 grep 到的内部 API 里,与 checkout/update 最相关的几个是:

/* client.h:503 —— 所有 update 内部实现的公共入口 */
svn_error_t *
svn_client__update_internal(svn_revnum_t *result_rev,
                            svn_boolean_t *timestamp_sleep,
                            const char *local_abspath,
                            const svn_opt_revision_t *revision,
                            svn_depth_t depth,
                            svn_boolean_t depth_is_sticky,
                            svn_boolean_t ignore_externals,
                            svn_boolean_t allow_unver_obstructions,
                            svn_boolean_t adds_as_modification,
                            svn_boolean_t make_parents,
                            svn_boolean_t innerupdate,
                            svn_ra_session_t *ra_session,
                            svn_client_ctx_t *ctx,
                            apr_pool_t *pool);

注意它比公开的 svn_client_update4 多出 timestamp_sleep(是否需要为了时间戳一致性而 sleep)、innerupdate(是否是一次「内部」更新,避免无限递归到 externals)等参数——这些是「给同库其他文件用」的信息,不适合暴露给最终用户,所以放进内部 API。

client.h 中其它实测到的 svn_client__ 成员覆盖极广,可粗略分为几组:

  • 仓库定位:svn_client__repos_locations、svn_client__repos_location、svn_client__repos_location_segments、svn_client__get_youngest_common_ancestor、svn_client__calc_youngest_common_ancestor;
  • RA 会话:svn_client__open_ra_session_internal、svn_client__ensure_ra_session_url、svn_client__ra_provide_base、svn_client__ra_provide_props、svn_client__ra_get_copysrc_kind、svn_client__ra_make_cb_baton;
  • 提交准备:svn_client__harvest_committables、svn_client__get_copy_committables、svn_client__condense_commit_items/2、svn_client__do_commit、svn_client__get_log_msg、svn_client__ensure_revprop_table;
  • 属性与外部:svn_client__get_paths_auto_props、svn_client__get_all_auto_props、svn_client__handle_externals、svn_client__export_externals、svn_client__get_inheritable_props、svn_client__iprop_relpaths_to_urls;
  • 更新/检出/切换:svn_client__update_internal、svn_client__checkout_internal、svn_client__switch_internal;
  • 合并:svn_client__merge_source_t、svn_client__merge_source_create、svn_client__merge_source_dup、svn_client__make_merge_conflict_error、svn_client__merge_locked;
  • 差异与冲突:svn_client__get_diff_editor2、svn_client__arbitrary_nodes_diff、svn_client__resolve_conflicts;
  • 文本基:svn_client__textbase_sync、svn_client__compatible_wc_version_optional_pristine。

双下划线 API 的存在,使得「公开面」可以保持极小且稳定,而库内部可以自由重构。这解释了为什么 svn_client.h 里一个 checkout 只有 4 代,而 client.h 里的内部函数数量却远多于公开函数。

7.2 一次 checkout 的自上而下调用链

要真正理解 libsvn_client 是「编排者」,最好的办法是把一次 svn checkout 从头到尾追一遍。下面这条链路中的每一个函数名,都是本次在真实源码里 grep/read 到的,不是凭记忆写的。

7.2.1 从命令表到公开 API

CLI 侧,subversion/svn/main.c 里有一张命令表,checkout 一项指向 svn_cl__checkout(位于 subversion/svn/checkout-cmd.c)。该函数把命令行参数(--revision、--depth、--ignore-externals、--force、--store-pristine、--compatible-version 等)翻译成对公开 API 的调用,也就是 svn_client_checkout4()。

7.2.2 svn_client_checkout4() 只是薄壳

svn_client_checkout4() 的实现极短,核心就三步:

/* libsvn_client/checkout.c:303-318(节选) */
SVN_ERR(svn_dirent_get_absolute(&local_abspath, path, pool));

err = svn_client__checkout_internal(result_rev, &sleep_here,
                                    URL, local_abspath,
                                    peg_revision, revision, depth,
                                    ignore_externals,
                                    allow_unver_obstructions,
                                    FALSE, /* settings_from_context */
                                    wc_format_version,
                                    store_pristine,
                                    NULL /* ra_session */,
                                    ctx, pool);
if (sleep_here)
  svn_io_sleep_for_timestamps(local_abspath, pool);
return svn_error_trace(err);

注意三点:第一,路径先经 svn_dirent_get_absolute() 变成绝对本地路径(内部一律用绝对 dirent);第二,工作副本设置参数 settings_from_context = FALSE,所以 wc_format_version / store_pristine 会「按传入值生效」;第三,返回前用 svn_error_trace() 包一层——这是 Subversion 里几乎每个返回错误的函数都会做的事(见 7.9 错误模型)。

7.2.3 svn_client__checkout_internal():把 URL 解析成 pathrev 并建会话

真正干活的是 svn_client__checkout_internal()(声明在 client.h:560)。它的内部步骤(实测行号):

  1. 决定目标工作副本格式。当 wc_format_version 指向一个「支持 pristineless 的格式」时,用 svn_client__compatible_wc_version_optional_pristine() 取到最低兼容版本,并用 svn_version__at_least() 比较、必要时上调,随后 svn_wc__format_from_version() 把版本翻译成工作副本格式号(checkout.c:141-157)。若格式不符则 fail_on_format_mismatch 决定是否报错。
  2. 复用或新建 RA 会话。若调用方已传入 ra_session,先用 svn_ra_reparent() 把会话重定位到目标 URL;重定位若因 URL 非法(SVN_ERR_RA_ILLEGAL_URL)失败,就清掉错误、把会话置空,走新建分支(checkout.c:161-182)。
  3. 新建会话。会话为空时调用 svn_client__ra_session_from_path2(),由它解出 pathrev(URL + 解析后的修订号)(checkout.c:184-189)。
  4. 解析 peg/operative 修订。旧分支里调用 svn_client__resolve_rev_and_url(),把 svn_opt_revision_t 形式的 peg_revision/revision 解析成具体的 revnum。
  5. 确认 URL 指向目录。调用 svn_ra_check_path(ra_session, "", pathrev->rev, &kind, ...)(checkout.c:191):若 kind == svn_node_none,报 SVN_ERR_RA_ILLEGAL_URL「URL 不存在」;若 kind == svn_node_file,报 SVN_ERR_UNSUPPORTED_FEATURE「URL 指向文件而非目录」(checkout.c:194-200)。
  6. 准备落地点。用 svn_io_check_path() 看本地路径是否存在:不存在则 svn_io_make_dir_recursively() 建目录并调 initialize_area()(一个 static 助手)写入一个「不完整」的工作副本根目录——注释原话是「Its entries file should only have an entry for THIS_DIR with a URL, revnum, and an 'incomplete' flag」(checkout.c:204-212);若已存在是目录,则用 svn_wc_check_wc2() 检查其格式;若存在但不是目录,报 SVN_ERR_WC_NODE_KIND_CHANGE(checkout.c:265-271)。
  7. 交给 update 收尾。最后一步是 svn_client__update_internal()(checkout.c:274-280),注释直白:「Have update fix the incompleteness.」

7.2.4 RA 会话是怎么打开的

svn_client__ra_session_from_path2() 先用 svn_client__open_ra_session_internal()(ra.c:322)把会话建起来,再由它调用 RA 层的 vtable 入口 svn_ra_open5()(ra.c:421、ra.c:466)。svn_ra_open5() 声明于 svn_ra.h:742,它的职责是根据 URL 的 scheme 选择具体 RA 实现(ra_local、ra_svn、ra_serf),并在其中完成 DAV 的 PROPFIND / OPTIONS 或 svn 协议的 repos-info 能力协商——这正是第 4、5 章讲过的内容。

把它串起来,一次 checkout 的调用树大致是:

svn (main.c 命令表)
└─ svn_cl__checkout                      (subversion/svn/checkout-cmd.c)
   └─ svn_client_checkout4               (libsvn_client/checkout.c:285)
      ├─ svn_dirent_get_absolute          (libsvn_subr/dirent_uri.c)
      └─ svn_client__checkout_internal    (checkout.c:74, client.h:560)
         ├─ svn_wc__format_from_version
         ├─ svn_client__ra_session_from_path2        (ra.c:523)
         │  └─ svn_client__open_ra_session_internal  (ra.c:322)
         │     └─ svn_ra_open5                        (ra.c:421 → svn_ra.h:742)
         │        ├─ [ra_local | ra_svn | ra_serf]
         │        └─ 能力协商 (SVN_RA_CAPABILITY_*)
         ├─ svn_client__resolve_rev_and_url
         ├─ svn_ra_check_path                          (checkout.c:191)
         ├─ svn_io_check_path / svn_io_make_dir_recursively
         ├─ initialize_area (static)
         └─ svn_client__update_internal                (update.c:607)
            └─ … 编辑器驱动的工作副本更新(见第 2、6 章)

这条链路的每一层都遵循同一个「池纪律」:调用方在最外层创建 pool,逐层向内传递,内层可以为「结果」申请自己的 result_pool,用 scratch_pool 做临时分配。我们会在 7.8 专讲这一点。

7.3 合并跟踪的数据结构与 svn:mergeinfo 文法

合并跟踪(merge tracking)是 1.5 引入、1.8 成熟、1.15 仍在演进的特性。它的核心是一段存在属性 svn:mergeinfo 里的文本,和三种内存表示。

7.3.1 三种内存结构,其实都是哈希

svn_mergeinfo.h:109-150 明确列出了四种表示(其中 (a) 是字符串本体):

/** An array of non-overlapping merge ranges (svn_merge_range_t *),
 *  sorted as said by svn_sort_compare_ranges(). */
typedef apr_array_header_t svn_rangelist_t;

/** A hash mapping merge source paths to non-empty rangelist arrays. */
typedef apr_hash_t *svn_mergeinfo_t;

/** A hash mapping paths (const char *) to svn_mergeinfo_t. */
typedef apr_hash_t *svn_mergeinfo_catalog_t;

关键之处在头文件里那段注释原文:「Both svn_mergeinfo_t and svn_mergeinfo_catalog_t are just typedefs for apr_hash_t *; there is no static type-checking, and you still use standard apr_hash_t functions to interact with them.」——也就是说:

  • svn_rangelist_t 是「元素为 svn_merge_range_t * 的有序数组」,比较/排序用 svn_sort_compare_ranges();
  • svn_mergeinfo_t 是「merge source path → svn_rangelist_t」的哈希:键是从仓库根算起的绝对路径(以 / 开头);
  • svn_mergeinfo_catalog_t 是「path → svn_mergeinfo_t」的哈希;其键可能相对于 RA 会话根而不是仓库根(注释专门提醒这一点);
  • 三者都没有静态类型检查,编译器不会阻止你把 catalog 塞进需要 mergeinfo 的地方——这是 C 的哈希类型别名带来的代价,读代码时要靠命名和注释人为区分。

约定上,NULL 哈希 = 没有 mergeinfo,空哈希 = 空 mergeinfo(empty mergeinfo),二者语义不同,头文件开篇就说「a path may have no mergeinfo, this is not the same as empty mergeinfo」。

7.3.2 属性名与文法

属性常量定义在 svn_props.h:452:

#define SVN_PROP_MERGEINFO SVN_PROP_PREFIX "mergeinfo"   /* => "svn:mergeinfo" */

SVN_PROP_PREFIX 即 "svn:",所以属性名就是 svn:mergeinfo。它的取值文法定义在 svn_mergeinfo.h:82-103,原文照抄:

Token             Definition
-----             ----------
revisionrange     REVISION1 "-" REVISION2
revisioneelement  (revisionrange | REVISION)"*"?
rangelist         revisioneelement (COMMA revisioneelement)*
revisionline      PATHNAME COLON rangelist
top               "" | (revisionline (NEWLINE revisionline))*

配合不可继承标记常量(svn_mergeinfo.h:107):

#define SVN_MERGEINFO_NONINHERITABLE_STR "*"

以及几条硬性约束(均来自同段注释):

  • PATHNAME 是该区间的合并来源路径;因为同一路径可能被重命名过,所以一次合并可能在同一个 svn:mergeinfo 里产生多条 revisionline(各自对应重命名前后的路径名);
  • rangelist 必须按修订号从低到高排序,且 revisionlistelement 不能重叠;
  • REVISION1 必须 小于 REVISION2(区间只能是正向的);
  • 允许连续的单个修订号「不合并成区间」——5,6,7,8,9-12 与 5-12 都合法;
  • top 为空串表示空 mergeinfo。

7.3.3 一个真实实例的逐步解析

考虑工作副本路径 branches/feature 上的一段 svn:mergeinfo(典型形态,来自合并 /trunk 与 /trunk/module 的结果):

/trunk:118-120,125,130-132*
/trunk/module:118-132

按文法解析:

  1. 整体是 top,含两条 revisionline,以换行符(NEWLINE)分隔。
  2. 第一行 /trunk:118-120,125,130-132*:PATHNAME = "/trunk",冒号后是 rangelist,由三个 revisioneelement 组成:
    • 118-120 是一个 revisionrange(REVISION1=118, REVISION2=120),可继承;
    • 125 是单个 REVISION,可继承;
    • 130-132* 是 revisionrange + 后缀 *,即 SVN_MERGEINFO_NONINHERITABLE_STR:这个区间只作用于设置它的这条路径本身,不会被子路径继承。
  3. 第二行 /trunk/module:118-132:来自路径 /trunk/module 的一整段可继承区间。
  4. 排序检查:118-120 < 125 < 130-132,递增且不重叠,合法。

解析由 svn_mergeinfo_parse() 完成,头文件写明它会:把无序区间排序、把同继承性的重叠区间合并、把相对来源路径转成绝对路径;而遇到「语法错误、继承性不同的区间重叠、start >= end、或路径映射到空 rangelist」则返回 SVN_ERR_MERGEINFO_PARSE_ERROR(svn_error_codes.h:1379)。注意「* 标记」在内存里对应的就是 svn_merge_range_t 的 inheritable 字段为 FALSE。

7.3.4 继承与消隐:两条方向相反的规则

头文件开篇用两大段文字规定了 mergeinfo 的两条核心规则,这也是整个合并跟踪最烧脑的部分:

规则一:消隐(elision) ——「if the SVN_PROP_MERGEINFO for a path is equivalent to the SVN_PROP_MERGEINFO for its parent, then the SVN_PROP_MERGEINFO on the path will elide (be removed) from the path as a post step to any merge.」即:如果某路径的 mergeinfo 与它父路径的完全相同(等价),那么合并完成后该路径上的属性会被删除——因为继承已经能表达同样的信息,留着是冗余。若父路径本身没有 mergeinfo,就一路向上找到最近的「grand-parent / great-grand-parent」中「等价」的那个来消隐。

规则二:继承(inheritance) ——「If a path has no SVN_PROP_MERGEINFO of its own, it inherits mergeinfo from its nearest parent that has SVN_PROP_MERGEINFO set.」即:没有显式 mergeinfo 的路径,从最近的、有 mergeinfo 的父路径继承。唯一的例外就是带 * 的不可继承区间——「These non-inheritable ranges apply only to the path which they are set on.」(这些区间只作用于它们被设置的路径本身。)

这两条规则在实现里分别落在(实测):

  • 消隐:libsvn_client/mergeinfo.c 的静态函数 should_elide_mergeinfo()(:803)与 elide_mergeinfo()(:862);当判定「可以消隐」时,它调用 svn_wc_prop_set4(..., SVN_PROP_MERGEINFO, NULL, ...) 把属性删掉,并发出 svn_wc_notify_merge_elide_info 通知(mergeinfo.c:877-900)。文件顶部的注释还给出判定细节:空 mergeinfo 会消隐到「空 mergeinfo 或 nothing」,而非空 mergeinfo 永不消隐到空 mergeinfo(mergeinfo.c:816-823)。
  • 继承:svn_client__get_wc_mergeinfo()(mergeinfo.c:202)沿工作副本目录树向上攀爬:它从 local_abspath 逐级 svn_dirent_dirname() 上溯,同时记录走过的相对路径 walk_relpath,直到遇到「工作副本根 / switched 路径 / 达到 limit_abspath」为止(mergeinfo.c:230-298)。参数 inherit 有三个取值(定义于 svn_mergeinfo.h:600-614):
枚举值含义
svn_mergeinfo_explicit只要显式 mergeinfo
svn_mergeinfo_inherited显式优先,缺失时取最近祖先继承来的
svn_mergeinfo_nearest_ancestor不管自己有没有显式,一律取最近祖先的

7.4 合并跟踪的实现细节

理解了数据结构和文法之后,我们看合并跟踪是怎么落到代码上的。核心在两个文件:libsvn_client/mergeinfo.c(mergeinfo 的读写与消隐)和 libsvn_client/merge.c(合并算法本身)。

7.4.1 mergeinfo.c:mergeinfo 的读、写、消隐

本次实测 grep 出 mergeinfo.c 里这批函数,可分为读、写、消隐三类:

  • 写:svn_client__record_wc_mergeinfo()(:106)、svn_client__record_wc_mergeinfo_catalog()(:153);
  • 读:svn_client__parse_mergeinfo()(:84)、svn_client__get_wc_mergeinfo()(:203)、svn_client__get_wc_mergeinfo_catalog()(:373)、svn_client__get_repos_mergeinfo()(:461)、svn_client__get_repos_mergeinfo_catalog()(:493)、svn_client__get_wc_or_repos_mergeinfo()(:555)、svn_client__get_wc_or_repos_mergeinfo_catalog()(:593);
  • 历史与消隐:svn_client__get_history_as_mergeinfo()(:741)、svn_client__elide_mergeinfo()(:908)、svn_client__elide_mergeinfo_catalog()(:1105)。

读的最底层是 svn_client__parse_mergeinfo(),它只有三行有效逻辑(实测):

/* libsvn_client/mergeinfo.c:84-103 */
svn_error_t *
svn_client__parse_mergeinfo(svn_mergeinfo_t *mergeinfo,
                            svn_wc_context_t *wc_ctx,
                            const char *local_abspath,
                            apr_pool_t *result_pool,
                            apr_pool_t *scratch_pool)
{
  const svn_string_t *propval;

  *mergeinfo = NULL;
  SVN_ERR(svn_wc_prop_get2(&propval, wc_ctx, local_abspath, SVN_PROP_MERGEINFO,
                           scratch_pool, scratch_pool));
  if (propval)
    SVN_ERR(svn_mergeinfo_parse(mergeinfo, propval->data, result_pool));
  return SVN_NO_ERROR;
}

先 *mergeinfo = NULL(「没有 mergeinfo」),再从工作副本读属性;属性存在才调 svn_mergeinfo_parse()。这与 7.3.1 约定一致:属性不存在 → NULL 哈希,属性为空串 → 空哈希。

写的高层是 svn_client__record_wc_mergeinfo()(实测 :105):先把内存 mergeinfo 用 svn_mergeinfo_to_string() 转回文本,再 svn_wc_prop_set4(ctx->wc_ctx, local_abspath, SVN_PROP_MERGEINFO, mergeinfo_str, svn_depth_empty, TRUE /* skip checks */, ...) 写入工作副本。注意它还会调用 svn_client__mergeinfo_status() 判断变更,并发出 svn_wc_notify_merge_record_info 通知(mergeinfo.c:121-147)。

写目录级的 svn_client__record_wc_mergeinfo_catalog() 则展示了一个经典的每轮清池写法(实测 mergeinfo.c:157-194):

apr_pool_t *iterpool = svn_pool_create(scratch_pool);
if (apr_hash_count(result_catalog)) {
  apr_array_header_t *sorted_cat =
    svn_sort__hash(result_catalog, svn_sort_compare_items_as_paths, scratch_pool);
  for (i = 0; i < sorted_cat->nelts; i++) {
    svn_sort__item_t elt = APR_ARRAY_IDX(sorted_cat, i, svn_sort__item_t);
    svn_pool_clear(iterpool);
    err = svn_client__record_wc_mergeinfo(elt.key, elt.value, TRUE, ctx, iterpool);
    if (err && err->apr_err == SVN_ERR_ENTRY_NOT_FOUND)
      svn_error_clear(err);   /* authz 导致的缺失,容忍 */
    else
      SVN_ERR(err);
  }
}
svn_pool_destroy(iterpool);

这里有两个值得注意的点:其一是 svn_pool_clear(iterpool) 放在循环体内,每轮释放上一轮的临时内存,避免大目录下内存随条目数线性增长(这正是 7.8.3 要讲的池纪律);其二是它特判 SVN_ERR_ENTRY_NOT_FOUND——注释解释,若某路径在工作副本里根本不存在,多半是提交时服务端知道它但客户端因 authz 拿不到,因此「clear 错误并继续」,其余错误照常抛出。

消隐的入口是 svn_client__elide_mergeinfo()(实测 :908)。它配合谓词 should_elide_mergeinfo() 工作:判定为「可消隐」时,用 svn_wc_prop_set4(..., SVN_PROP_MERGEINFO, NULL, ...) 把属性值写为 NULL(即删除),并发 svn_wc_notify_merge_elide_info + svn_wc_notify_update_update 两条通知(mergeinfo.c:877-900)。谓词的边界条件是:空 mergeinfo 可消隐到「空 mergeinfo / nothing」,非空 mergeinfo 永不消隐到空。

7.4.2 svn_client__merge_path_t:合并的「逐路径工作单元」

合并算法要为「合并目标树里每一个需要关心的路径」维护一堆状态。这个状态就打包在 svn_client__merge_path_t(定义在 mergeinfo.h,不是 client.h)。逐字段(实测):

字段类型含义
abspathconst char *绝对工作副本路径
missing_childsvn_boolean_t有直接子节点缺失、且未 switched
switched_childsvn_boolean_t有直接子节点是 switched 的
switchedsvn_boolean_t本路径本身是 switched
has_noninheritablesvn_boolean_t本路径上的 svn:mergeinfo 含不可继承区间
absentsvn_boolean_t本路径因 authz 限制从工作副本中「缺失」
child_of_noninheritablesvn_boolean_t自身无显式 mergeinfo,但父路径有不可继承 mergeinfo
remaining_rangessvn_rangelist_t *尚待合并的区间;正向合并时遵守排序规则,反向合并时「younger start 在前」;absent 为真时可为 NULL
pre_merge_mergeinfosvn_mergeinfo_t合并前该路径上的显式或继承 mergeinfo(可为 NULL)
implicit_mergeinfosvn_mergeinfo_t合并前的「隐式 mergeinfo」(可为 NULL)
inherited_mergeinfosvn_boolean_tpre_merge_mergeinfo 是显式还是继承来的
immediate_child_dirsvn_boolean_t是合并目标的直接子目录、合并前无显式 mergeinfo、且操作深度为 svn_depth_immediates
record_mergeinfosvn_boolean_t需要在该路径记录 mergeinfo 来描述本次合并
record_noninheritablesvn_boolean_t需要记录不可继承 mergeinfo(蕴含 record_mergeinfo 为真)

一组 svn_client__merge_path_t * 构成的数组,就是注释里反复提到的「THE CHILDREN_WITH_MERGEINFO ARRAY」——合并算法遍历它来决定「哪些路径要合并哪些区间、哪些要记录什么样的 mergeinfo」。

7.4.3 pre_merge_mergeinfo 与 implicit_mergeinfo 的来源

所谓「显式 mergeinfo」是工作副本属性里真实存在的 svn:mergeinfo;「隐式 mergeinfo」(implicit mergeinfo)则是从版本历史推断出来的——例如某路径是从 /trunk 复制(copy)过来的,那么历史上 /trunk 的修订某种意义上已经被「合并」进这个分支了,尽管从未有人执行过 svn merge。

merge.c 里有一个静态函数 get_full_mergeinfo(),它就是同时取这两者的入口(实测签名):

/* libsvn_client/merge.c:4317(签名节选) */
static svn_error_t *
get_full_mergeinfo(svn_mergeinfo_t *recorded_mergeinfo,
                   svn_mergeinfo_t *implicit_mergeinfo,
                   svn_boolean_t *inherited,
                   svn_mergeinfo_inheritance_t inherit,
                   svn_ra_session_t *ra_session,
                   const char *target_abspath,
                   svn_revnum_t start, svn_revnum_t end,
                   svn_client_ctx_t *ctx,
                   apr_pool_t *result_pool, apr_pool_t *scratch_pool)

其内部:recorded_mergeinfo 非空时调 svn_client__get_wc_or_repos_mergeinfo(..., inherit, ra_session, target_abspath, ...)(从工作副本或仓库取真实 mergeinfo,并回报 inherited);implicit_mergeinfo 非空时,它先断言 SVN_IS_VALID_REVNUM(start) && SVN_IS_VALID_REVNUM(end) && (start > end),再去取节点的 original_*(copy origin),并调 svn_client__get_history_as_mergeinfo() 把历史转成 mergeinfo(merge.c:4342-4386)。

在 merge.c 里还有一对围绕隐式 mergeinfo 的静态助手:

  • inherit_implicit_mergeinfo_from_parent()(:4408):子节点若无 implicit_mergeinfo,先确保父节点的隐式 mergeinfo,再用 svn_mergeinfo_dup() 拷给它;
  • ensure_implicit_mergeinfo()(:4464):确保某个 child 的隐式 mergeinfo 已就绪,必要时从父继承,否则用 get_full_mergeinfo() 现算。

合并目标树的状态填充集中在 get_mergeinfo_paths() 一带(merge.c:5074-5261),其中对每个 child 调 get_full_mergeinfo(&child->pre_merge_mergeinfo, &child->implicit_mergeinfo, ...),并在 merge.c:5231 处按需补上 pre_merge_mergeinfo/implicit_mergeinfo。这些字段随后在真正的合并过程中(如 merge.c:6568-6573)被读取,用来计算 remaining_ranges。

7.4.4 能力协商与降级:SVN_RA_CAPABILITY_MERGEINFO

合并跟踪依赖服务端与客户端的能力协商。常量定义在 svn_ra.h:2220-2289,实测的完整清单是:

常量字符串值
SVN_RA_CAPABILITY_DEPTH"depth"
SVN_RA_CAPABILITY_MERGEINFO"mergeinfo"
SVN_RA_CAPABILITY_LOG_REVPROPS"log-revprops"
SVN_RA_CAPABILITY_PARTIAL_REPLAY"partial-replay"
SVN_RA_CAPABILITY_COMMIT_REVPROPS"commit-revprops"
SVN_RA_CAPABILITY_ATOMIC_REVPROPS"atomic-revprops"
SVN_RA_CAPABILITY_INHERITED_PROPS"inherited-props"
SVN_RA_CAPABILITY_EPHEMERAL_TXNPROPS"ephemeral-txnprops"
SVN_RA_CAPABILITY_GET_FILE_REVS_REVERSE"get-file-revs-reversed"
SVN_RA_CAPABILITY_LIST"list"

协商发生在 svn_ra_open5() 打开会话之后:客户端通过 svn_ra_has_capability(session, &has, SVN_RA_CAPABILITY_MERGEINFO, pool) 询问服务端能力。降级逻辑是这样的:服务端版本 ≥1.5 一般都会宣告 "mergeinfo";但如果对端是 1.4 及更早的旧服务端(不宣告该能力),客户端就必须退回「非 mergeinfo 感知(non-mergeinfo-aware)」的合并——即退化为「普通 diff-and-apply」,不会读写 svn:mergeinfo,也不会做继承/消隐。这种情况下若用户仍显式要求合并跟踪,客户端可能报 SVN_ERR_CLIENT_INVALID_MERGEINFO_NO_MERGETRACKING(定义于 svn_error_codes.h:1276)。

svn_ra_get_mergeinfo()(svn_ra.h:1244)则是「从仓库取 mergeinfo」的 RA 层动词,svn_client__get_repos_mergeinfo() 包装的就是它。少了这个能力,整个合并跟踪的「仓库侧数据源」就不存在了。

7.4.5 1.15 的跨仓库 UUID 警告(r1888474, issue #4874)

CHANGES 第 33 行原文:「Warn on merges from foreign repositories if UUIDs match but URLs differ (r1888474 et al, issue #4874)」。这条修改针对一个隐蔽的坑:当用户从一个「外来仓库(foreign repository)」合并时,如果两仓库的 UUID 相同但 URL 不同(典型场景:同一仓库换了域名、或镜像/备份关系被误判为同一仓库),旧版本会把它当成「同仓库」而错误地继承/比较 mergeinfo,可能产生看似合理实则错误的合并结果。1.15 增加了警告,提醒用户这可能不是同一个仓库。这是合并跟踪「跨仓库身份判定」问题(issue #4874)的一次温和修补——不改变行为,只提示风险。


7.5 认证系统:provider 责任链

认证是 libsvn_subr 里最优雅的一处设计。它把「怎么拿到一个凭据」抽象成一个可插拔的 provider 虚表,再由 auth baton 按注册顺序串成一条责任链。

7.5.1 三层抽象:baton、provider、iteration state

svn_auth.h 开头用大段注释交代了模型,核心概念有三个:

  • svn_auth_baton_t(:87):不透明的前向声明类型。它内部持有「按凭据类型分组、按注册顺序排列」的 provider 列表,以及一张运行期参数哈希表;
  • svn_auth_provider_object_t(:158-163):一对 { const svn_auth_provider_t *vtable; void *provider_baton; },即「虚表指针 + 实例 baton」;
  • svn_auth_iterstate_t(:90):前向声明,表示「一次凭据尝试的迭代状态」——调用方拿着它就能在失败后请求「下一组凭据」。

7.5.2 svn_auth_provider_t 三方法虚表

虚表本体在 svn_auth.h:94-153,只有三个函数指针:

typedef struct svn_auth_provider_t
{
  /* 这个 provider 认识的凭据类型 */
  const char *cred_kind;

  /* 取初始凭据:设置 *credentials,并可选地设置 *iter_baton 供后续 next 调用 */
  svn_error_t * (*first_credentials)(void **credentials, void **iter_baton,
                                     void *provider_baton, apr_hash_t *parameters,
                                     const char *realmstring, apr_pool_t *pool);

  /* 取另一组凭据;若无更多则把 *credentials 置 NULL;
     若 provider 只有一组凭据,此指针应为 NULL */
  svn_error_t * (*next_credentials)(void **credentials, void *iter_baton,
                                    void *provider_baton, apr_hash_t *parameters,
                                    const char *realmstring, apr_pool_t *pool);

  /* 保存凭据;*saved 置 TRUE 表示确实保存了,FALSE 表示未保存;
     若 provider 从不保存,此指针应为 NULL */
  svn_error_t * (*save_credentials)(svn_boolean_t *saved, void *credentials,
                                    void *provider_baton, apr_hash_t *parameters,
                                    const char *realmstring, apr_pool_t *pool);
} svn_auth_provider_t;

四个形参在三个方法里保持一致语义:provider_baton(provider 实例状态)、parameters(运行期参数哈希,即 SVN_AUTH_PARAM_* 的容器)、realmstring(凭据所属的「领域」字符串)。注释原文对 provider 的三种能力概括得很清楚:「can fetch initial credentials / can retry the fetch (or try to fetch something different) / can store the credentials for future use」。

7.5.3 五类凭据常量与对应结构体

凭据「种类」由字符串常量标识(不透明类型,具体结构体由 provider 与调用方约定)。实测 svn_auth.h 里的五类:

常量字符串值位置对应结构体(关键字段)
SVN_AUTH_CRED_SIMPLE"svn.simple":186svn_auth_cred_simple_t:username、password、may_save
SVN_AUTH_CRED_USERNAME"svn.username":210svn_auth_cred_username_t:username、may_save
SVN_AUTH_CRED_SSL_CLIENT_CERT"svn.ssl.client-cert":236svn_auth_cred_ssl_client_cert_t:cert_file(证书绝对路径)、may_save
SVN_AUTH_CRED_SSL_CLIENT_CERT_PW"svn.ssl.client-passphrase":271svn_auth_cred_ssl_client_cert_pw_t:password、may_save
SVN_AUTH_CRED_SSL_SERVER_TRUST"svn.ssl.server":300svn_auth_cred_ssl_server_trust_t:may_save、accepted_failures(位掩码)

SVN_AUTH_CRED_SSL_SERVER_TRUST 还配套一个证书信息结构 svn_auth_ssl_server_cert_info_t(:305-319),字段为 hostname(主 CN)、fingerprint(ASCII 指纹)、valid_from、valid_until、issuer_dname、ascii_cert(Base-64 编码的 DER)。accepted_failures 的取值来自一组织掩码常量(:411-421):

位值含义
SVN_AUTH_SSL_NOTYETVALID0x00000001证书尚未生效
SVN_AUTH_SSL_EXPIRED0x00000002证书已过期
SVN_AUTH_SSL_CNMISMATCH0x00000004CN 与主机名不匹配
SVN_AUTH_SSL_UNKNOWNCA0x00000008签发 CA 未知(不受信任)
SVN_AUTH_SSL_OTHER0x40000000其它未归类失败

头文件特意标注这些位值「stored in the on disk auth cache」,所以语义绝不可更改——它们要跨版本、跨进程持久化。

7.5.4 SVN_AUTH_PARAM_* 全集对照表

运行期参数存放在 auth baton 的哈希里,键统一以 SVN_AUTH_PARAM_PREFIX(:568)开头:

#define SVN_AUTH_PARAM_PREFIX "svn:auth:"

实测的完整参数清单(svn_auth.h:568-649):

宏展开后的键用途
SVN_AUTH_PARAM_DEFAULT_USERNAMEsvn:auth:username应用默认用户名(如 --username)
SVN_AUTH_PARAM_DEFAULT_PASSWORDsvn:auth:password应用默认密码(如 --password)
SVN_AUTH_PARAM_NON_INTERACTIVEsvn:auth:non-interactive存在即禁止任何 provider 弹提示
SVN_AUTH_PARAM_DONT_STORE_PASSWORDSsvn:auth:dont-store-passwords存在即禁止把密码写盘
SVN_AUTH_PARAM_STORE_PLAINTEXT_PASSWORDSsvn:auth:store-plaintext-passwords值为 SVN_CONFIG_TRUE/FALSE/ASK(1.6+)
SVN_AUTH_PARAM_DONT_STORE_SSL_CLIENT_CERT_PPsvn:auth:dont-store-ssl-client-cert-pp存在即禁止把客户端证书口令写盘(1.6+)
SVN_AUTH_PARAM_STORE_SSL_CLIENT_CERT_PP_PLAINTEXTsvn:auth:store-ssl-client-cert-pp-plaintext口令是否可明文写盘(1.6+)
SVN_AUTH_PARAM_NO_AUTH_CACHEsvn:auth:no-auth-cache存在即禁止任何凭据写盘
SVN_AUTH_PARAM_SSL_SERVER_FAILURESsvn:auth:ssl:failures指向 apr_uint32_t 的失败位掩码
SVN_AUTH_PARAM_SSL_SERVER_CERT_INFOsvn:auth:ssl:cert-info指向 svn_auth_ssl_server_cert_info_t
SVN_AUTH_PARAM_CONFIG_CATEGORY_CONFIGsvn:auth:config-category-config指向 config 类别的 svn_config_t
SVN_AUTH_PARAM_CONFIG_CATEGORY_SERVERSsvn:auth:config-category-servers指向 servers 类别的 svn_config_t
SVN_AUTH_PARAM_CONFIG同 ..._SERVERS@deprecated,1.5 API 的别名
SVN_AUTH_PARAM_SERVER_GROUPsvn:auth:server-group当前 server group 名
SVN_AUTH_PARAM_CONFIG_DIRsvn:auth:config-dir覆盖默认 ~/.subversion 的配置目录

注:SVN_AUTH_PARAM_CONFIG 的注释是「@deprecated Provided for backward compatibility with the 1.5 API.」,其定义直接等于 SVN_AUTH_PARAM_CONFIG_CATEGORY_SERVERS,即旧名与新名指向同一个键值。

另有两条不属于 svn:auth: 前缀的 GNOME Keyring 专用参数(:1017、:1022):SVN_AUTH_PARAM_GNOME_KEYRING_UNLOCK_PROMPT_FUNC("gnome-keyring-unlock-prompt-func")与 SVN_AUTH_PARAM_GNOME_KEYRING_UNLOCK_PROMPT_BATON("gnome-keyring-unlock-prompt-baton"),且都标注为「Only used by old libgnome-keyring implementation」。

7.5.5 realmstring(领域字符串)

realmstring 是三个 provider 方法都要接收的参数,也是凭据缓存/持久化的一级键。它的作用是:让「同一台服务器上、不同子路径或不同凭据类型」的凭据彼此隔离,从而既不会误用,也能在提示时给用户足够上下文。例如 simple 凭据的 realm 通常形如 <realm>:<URL 的部分>,而 SSL 客户端证书口令的 realm 则「must be a name that makes it possible for the user to identify the certificate」(:258-259)。持久化时,磁盘缓存正是以「凭据类型 + realmstring」为索引存储(这也是为何 SSL 失败位掩码的数值被冻结——它要写进同一个缓存)。

7.5.6 平台 provider 名与工厂函数(Grep 核对)

svn_auth_get_platform_specific_provider()(:837)把「名字 + 类型」映射到具体工厂函数,注释给出的合法名为 provider_name ∈ {gnome_keyring, keychain, kwallet, gpg_agent, windows}、provider_type ∈ {simple, ssl_client_cert_pw, ssl_server_trust}。其分派实现位于 libsvn_subr/auth.c:501-604。本次对 libsvn_subr 目录 grep svn_auth__get_*_provider 得到的全部内部工厂函数(全为双下划线内部符号):

函数(声明处)平台 / 编译条件
svn_auth__get_windows_simple_provider(auth.h:64,实现 win32_crypto.c:214)Windows CryptoAPI
svn_auth__get_windows_ssl_client_cert_pw_provider(auth.h:85,win32_crypto.c:332)Windows
svn_auth__get_windows_ssl_server_trust_provider(auth.h:101,win32_crypto.c:496)Windows
svn_auth__get_windows_ssl_server_authority_provider(auth.c:600 引用,win32_crypto.c:514)Windows
svn_auth__get_keychain_simple_provider(auth.h:119,macos_keychain.c:320)macOS Keychain
svn_auth__get_keychain_ssl_client_cert_pw_provider(auth.h:134,macos_keychain.c:330)macOS Keychain
svn_auth__get_gpg_agent_simple_provider(auth.h:155,gpg_agent.c:728)GnuPG agent
svn_auth__get_dummmy_simple_provider(auth.h:166,auth.c:901)保底「哑」provider(注意函数名里 dummmy 有三个 m,是源码里的拼写)

注意 gnome_keyring / kwallet 两类没有直接的静态工厂,而是通过 svn_dso_load() 动态加载外部库 libsvn_auth_gnome_keyring-<soversion> 并 apr_dso_sym() 取 svn_auth_get_<name>_<type>_provider 符号(auth.c:505-555)——这是因为 GNOME/KDE 依赖不适合静态链接进核心库。

svn_auth_get_platform_specific_client_providers()(auth.c:607)则按 servers 组的 password-stores 配置项(默认值常量 SVN__DEFAULT_AUTH_PROVIDER_LIST = "gnome-keyring,kwallet,keychain,gpg-agent,windows-cryptoapi",auth.c:620-621)依次构建 provider 数组。

7.5.7 责任链推演:一次认证失败后怎么换凭据

把这套机制「演一遍」最能说明问题。假设一次 svn checkout https://svn.example.org/repo,客户端注册了一个典型的 provider 序列(按凭据类型分组,simple 类下依次为):

  1. svn_auth_get_simple_provider2() —— 从 ~/.subversion/auth/ 磁盘缓存读密码(并可保存);
  2. 平台专属的 keychain / windows-cryptoapi provider —— 从系统钥匙串读;
  3. svn_auth_get_simple_prompt_provider(..., retry_limit = 3, ...) —— 从终端提示用户输入。

认证流程的入口是 svn_auth_first_credentials(&creds, &state, SVN_AUTH_CRED_SIMPLE, realmstring, auth_baton, pool)(:663)。它返回第一组凭据,并带回一个 state。若这组凭据被服务端拒绝,RA 层捕获后调用 svn_auth_next_credentials(&creds, state, pool)(:681)索取下一组。

libsvn_auth 的「遍历」规则(头文件 :62-67 原话)是:「If the initial credentials fail to authenticate, the caller keeps requesting new credentials. Under the hood, libsvn_auth effectively 'walks' over each provider (in order of registry), one at a time, until all the providers have exhausted all their retry options.」具体推演:

  1. 第一轮:first_credentials 分发给「simple 类」下第 1 个 provider(磁盘缓存)。它读到缓存密码(若 SVN_AUTH_PARAM_NO_AUTH_CACHE 存在则跳过缓存、直接返回 NULL),设置 *credentials 与 *iter_baton。
  2. 服务端拒绝:调用方调 next_credentials。第 1 个 provider 的 next_credentials 若有更多组就继续给(如缓存里有多条),否则把 *credentials 置 NULL——表示「这个 provider 弹尽粮绝」。
  3. 责任链推进:libsvn_auth 看到「当前 provider 返回 NULL」就切到第 2 个 provider(keychain),再走一遍 first_credentials/next_credentials。
  4. 走到交互 provider:第 3 个是 prompt provider。它的 first_credentials 返回「默认用户名/密码」——若 SVN_AUTH_PARAM_DEFAULT_USERNAME/DEFAULT_PASSWORD 已在 baton 里(用户命令行传了 --username/--password),就直接返回这一组;若 SVN_AUTH_PARAM_NON_INTERACTIVE 存在则不弹提示、直接结束;否则提示用户,失败后经 next_credentials 再提示,最多 retry_limit(此处 3)次后清空。
  5. 全部耗尽:所有 provider 的所有重试都返回 NULL,libsvn_auth 让 svn_auth_next_credentials 也返回 NULL,RA 层据此报「认证失败」并结束操作。

保存发生在成功之后:调用方调 svn_auth_save_credentials(state, pool)(:694),由产生这组凭据的 provider的 save_credentials 写入——写盘与否受 may_save、SVN_AUTH_PARAM_DONT_STORE_PASSWORDS、STORE_PLAINTEXT_PASSWORDS 等参数共同约束。

还有一个 1.8 引入的「遗忘」接口:svn_auth_forget_credentials(auth_baton, cred_kind, realmstring, pool)(:715),用于让内存缓存失效、强制下次重新走 provider——注意它的注释强调「does not affect persisted authentication credential storage at all」,只影响内存缓存。

7.5.8 1.15 的认证变更:明文存储恢复与 Keychain 重写

CHANGES 记录了 1.15 两处认证相关变更:

  • 第 21 行:「Re-enable plaintext credential store by default on Unix (r1909351)」——在 Unix 上恢复「默认允许明文存储凭据」。这与 1.6 引入的 plaintext_prompt_func 机制相呼应:svn_auth_get_simple_provider2()(:800)在写盘前会回调 svn_auth_plaintext_prompt_func_t;若该回调为 NULL,头文件注释说「the answer is assumed to be TRUE」,并称这是「matches the deprecated behaviour of storing unencrypted passwords by default, and is only done this way for backward compatibility reasons」(:774-787)。r1909351 即让「默认行为」重新回到「明文可存」。
  • 第 49 行:「Reimplement macOS Keychain auth provider with SecItem* API (r1926564)」——把 macOS 的 Keychain provider 从旧的 Keychain API 换成 SecItem* 系列。对应实现在 macos_keychain.c(svn_auth__get_keychain_simple_provider @320、svn_auth__get_keychain_ssl_client_cert_pw_provider @330)。

7.6 缓存框架

Subversion 的仓库侧(FSFS/FSX)性能高度依赖缓存:节点修订、表示(rep)、全文解码结果都需要缓存,否则一次 svn log 会反复解压、反复读同一批 rev/rep。缓存框架分两层:配置层(svn_cache_config.h,决定「有多大、几个文件句柄、是否单线程」)与后端层(多个 cache*.c,决定「缓存放在哪」)。

7.6.1 svn_cache_config_t 三个字段

定义在 svn_cache_config.h:49-64:

typedef struct svn_cache_config_t
{
  /* total cache size in bytes. ... only soft limit ... May be 0,
     resulting in default caching code being used. */
  apr_uint64_t cache_size;

  /* maximum number of files kept open */
  apr_size_t file_handle_count;

  /* is this application guaranteed to be single-threaded? */
  svn_boolean_t single_threaded;

  /* DON'T add new members here.  Bump struct and API version instead. */
} svn_cache_config_t;

三个字段与两条「不许扩展」的纪律(注释里明确写了 DON'T add new members,要扩就升结构体版本)之外,还有三条使用约束,全部写在 svn_cache_config_get()/svn_cache_config_set() 的文档里(svn_cache_config.h:66-84):

  1. 软上限:cache_size 「is only soft limit to the total application memory usage and will be exceeded due to temporary objects and other program state」。也就是说,它是个意图声明而非硬性内存护栏;临时对象、其它程序状态都会让实际占用超过它。
  2. 不保证改变正在使用的配置:svn_cache_config_set() 「may not change the actual configuration in use. Therefore, call it before reading data from any repo and call it only once.」——一旦缓存已实例化,再改就未必生效,所以必须在「读任何仓库数据之前、且只调一次」。
  3. 非线程安全:svn_cache_config_set() 「is not thread-safe. Therefore, it should be called from the processes' initialization code only.」——只能在进程初始化阶段调用。

默认值(cache_config.c:34-65)是:cache_size = 0x1000000(16 MB)、file_handle_count = 16、single_threaded 在多线程构建下为 FALSE(注释:「This simply activates proper synchronization between threads, it is a safe default」),在无线程构建下为 TRUE。注释还给出调优建议:单进程服务器可把 cache_size 调到 RAM 的 50%+;file_handle_count 大多数 OS 上限约 1000,500+ 可减少 I/O 与系统开销。

7.6.2 三个后端:inprocess / membuffer / memcache

本次 Glob 实证了后端实现的文件名(subversion/libsvn_subr/):

文件工厂函数缓存位置适用场景
cache-inprocess.csvn_cache__create_inprocess(:647)进程内,按页(page)划分无 membuffer 时的保底;单进程
cache-membuffer.csvn_cache__create_membuffer_cache(:3345)进程内的大块连续内存(membuffer)单进程、追求高命中率
cache-memcache.csvn_cache__create_memcache(:405/:520)外部 memcached 服务多进程共享缓存
cache-null.csvn_cache__create_null(:145)空实现(永远未命中)关闭缓存
cache.csvn_cache__get/set/has_key/iter/get_partial/set_partial/get_info/format_info、svn_cache__set_error_handler、svn_cache__is_cachable——统一门面,转发到具体后端

create_cache() 在 libsvn_fs_fs/caching.c:292-347(FSX 侧同构,见 libsvn_fs_x/caching.c:344-362)里做后端优先级选择,实测逻辑是「memcache > membuffer > inprocess > NULL」:

/* libsvn_fs_fs/caching.c:315-342(节选) */
if (memcache) {
  /* 有外部 memcached 就用它,错误处理改为 warn-and-continue */
  SVN_ERR(svn_cache__create_memcache(cache_p, memcache, serializer, deserializer,
                                     klen, prefix, result_pool));
  error_handler = no_handler ? NULL : warn_and_continue_on_cache_errors;
}
else if (membuffer) {
  /* 用进程内大块内存;带 namespace 的缓存视为短生命周期 */
  SVN_ERR(svn_cache__create_membuffer_cache(cache_p, membuffer, serializer,
              deserializer, klen, prefix, priority, FALSE, has_namespace, ...));
}
else if (pages) {
  /* 退到按页划分的 inprocess 缓存 */
  SVN_ERR(svn_cache__create_inprocess(cache_p, serializer, deserializer, klen,
              pages, items_per_page, FALSE, prefix, result_pool));
}
else {
  *cache_p = NULL;   /* 什么都不做 */
}

优先级背后的取舍很清楚:memcache 支持多进程/多机共享,但错误只能降级为警告(warn_and_continue_on_cache_errors);membuffer 是单进程内最省事的大缓存;inprocess 是受限环境下的保底;三者皆无则干脆不缓存。

7.6.3 缓存配置的进程级单例

cache_config.c 里的 cache_settings 是一个进程级单例静态变量(static svn_cache_config_t cache_settings = {...}),svn_cache_config_get() 直接返回它的地址,svn_cache_config_set() 覆盖它。svn_cache_config.c 里通过 svn_cache__get_global_membuffer_cache()(cache_config.c:158-160)用「原子初始化(svn_atomic__init_once)」在第一次访问时惰性创建这块全局 membuffer,并按 MIN(cache_settings.cache_size, ...) 决定实际大小、按 ! svn_cache_config_get()->single_threaded 决定是否加锁(cache_config.c:87-128)。这正好对应约束 2「不保证改变正在用的配置」——membuffer 一旦建好,尺寸就定了。

7.6.4 磁盘缓存与 rep-cache.db

上面讲的是内存缓存。FSFS 还有一处磁盘级缓存:rep-cache.db。它的名字常量定义在 libsvn_fs_fs/rep-cache.h:35:

#define REP_CACHE_DB_NAME        "rep-cache.db"

它是一张 SQLite 数据库,schema 来自 libsvn_fs_fs/rep-cache-db.sql(经生成器转成 rep-cache-db.h)。structure 文件把它描述为「SQLite database mapping rep checksums to locations」——用「表示(rep)的校验和」映射到「它在仓库里的存储位置」。它让 FSFS 在提交时能去重(相同内容的文件只存一份 delta 链),代价是需要一个可写的 SQLite(因此 fsfs 对它的可用性有运行时检测与降级)。


7.7 APR 池:内存生命周期的唯一真理

Subversion 没有 malloc/free,只有 APR 池(pool):一块按「区域」整体分配、整体释放的内存。svn_pools.h 里把 APR 的池 API 包了一层(:73-87):

#define svn_pool_create(parent_pool) svn_pool_create_ex(parent_pool, NULL)
#define svn_pool_clear  apr_pool_clear
#define svn_pool_destroy apr_pool_destroy

注意 svn_pool_clear / svn_pool_destroy 只是 APR 同名函数的重命名(注释:「This define ... exists for completeness / symmetry」),而 svn_pool_create 是 svn_pool_create_ex 的宏,后者最终走 svn_pool_create_ex_debug(..., APR_POOL__FILE_LINE__)——APR_POOL__FILE_LINE__ 让每个池都记住创建它的文件与行号,便于调试期追踪内存泄漏。

7.7.1 result_pool / scratch_pool 约定

Subversion 内部(尤其是 1.7 之后的新 API)普遍采用「双池」签名:

  • result_pool:存放「函数返回给调用方的东西」——它必须活得比函数调用更久,所以归调用方所有;
  • scratch_pool:存放「函数执行过程中的临时对象」——函数返回后调用方不再需要,所以函数可以随意在它里面分配,甚至清空它。

这套约定的好处是:调用方只需关心「结果放哪」,无需为「临时物」单独准备池;被调方也无需自己创建/销毁临时池——除非它要做大量迭代。

我们在 7.4.1 已经看到 svn_client__record_wc_mergeinfo_catalog() 里的迭代池实践:apr_pool_t *iterpool = svn_pool_create(scratch_pool); 在循环内 svn_pool_clear(iterpool),最后 svn_pool_destroy(iterpool)。这是长循环的标准写法:

apr_pool_t *iterpool = svn_pool_create(scratch_pool);
for (i = 0; i < n; i++) {
  svn_pool_clear(iterpool);      /* 每轮释放上一轮的临时内存 */
  do_one_thing(i, iterpool);     /* 只用 iterpool 做临时分配 */
}
svn_pool_destroy(iterpool);

若不这样做,循环里累积的临时对象会全部挂在 scratch_pool 上,直到外层函数返回才释放——对一个遍历整棵树的合并来说,内存会随路径数线性增长。

7.7.2 顶层池与 svn_client_ctx_t 里的存活对象

一次 CLI 调用栈的顶层池通常由 main() 创建,一直活到进程结束。但有几类对象的生命期跨越单次 API 调用,必须挂在长寿池上:

  • svn_client_ctx_t 本身:由调用方在长寿池里创建,整个会话期间复用;
  • ctx->wc_ctx(svn_wc_context_t):1.7 起是客户端上下文的一部分,承载所有工作副本操作;
  • ctx->config 里的 svn_config_t:配置解析结果,全会话复用;
  • auth_baton:持有凭据内存缓存与运行期参数,跨多次操作复用;
  • RA 会话 svn_ra_session_t:一次操作内复用;svn_client__checkout_internal() 会接收外部传入的 ra_session 以避免重复建连(见 7.2.3)。

换句话说,池的树形结构里,越靠近顶层的对象活得越久。这也是为什么 svn_client_ctx_t 与 wc_ctx 被设计成「由调用方创建、通过 ctx 传递」——它们天然属于长寿池。


7.8 错误模型:svn_error_t 链

Subversion 用单一返回类型 + 错误链取代 C 的 errno/异常。任何可能失败的函数都返回 svn_error_t *,成功时返回 SVN_NO_ERROR(即 0)。

7.8.1 svn_error_t 结构

定义在 svn_types.h:176-218:

typedef struct svn_error_t
{
  apr_status_t apr_err;      /* APR 错误值,可能是 SVN_ 自定义码 */
  const char *message;       /* 错误的细节描述 */
  struct svn_error_t *child; /* 被"包裹"的下层错误,可为 NULL —— 即错误链 */
  apr_pool_t *pool;          /* 分配该错误对象的池(含整条链) */
  const char *file;          /* 出错源文件(仅 SVN_DEBUG) */
  long line;                 /* 出错行号(仅 SVN_DEBUG) */
} svn_error_t;

要点:

  • child 构成链:新错误「包裹」旧错误,于是错误从底层向上层逐级堆叠。file/line 仅在 SVN_DEBUG 下有效——这就是为什么 release 版的错误栈看不到源码位置。
  • message 可能为空:注释说「if this error was generated by Subversion's API, you'll probably want to use svn_err_best_message()」,因为「Subversion's API functions sometimes add many links to the error chain that lack details (used only to produce virtual stack traces)」。这些「只负责堆栈、不带信息」的链节就是 svn_error_trace() 产生的。
  • 错误必须显式释放:svn_error_clear()(:252)的注释警告「errors are managed explicitly; you MUST clear an error if you are ignoring it, or you are leaking memory」,同时给出惯用法 svn_error_clear(svn_foo(...))。

7.8.2 包装而非丢弃:svn_error_createf 一族

创建与包装错误的 API(svn_error.h:137-233):

函数作用
svn_error_create(apr_err, child, message)造一个错误,可包裹 child;「bottommost」时 child 传 NULL
svn_error_createf(apr_err, child, fmt, ...)同上,但消息用 apr_psprintf 格式化
svn_error_wrap_apr(status, fmt, ...)包装一个 APR status,把 APR 错误消息追加在 ": " 之后
svn_error_quick_wrap(child, new_msg)若 child 为空则返回空;否则在链首插一层新消息(错误码继承自 child)
svn_error_quick_wrapf(child, fmt, ...)同上,带格式化(1.9+)
svn_error_compose(chain, new_err)把 new_err 附到 chain 链尾
svn_error_compose_create(err1, err2)组合两个错误,err2 排在 err1 之后
svn_error_root_cause(err)找链上最后一个错误(根因)
svn_error_find_cause(err, apr_err)找链上第一个码为 apr_err 的错误
svn_error_dup(err)深拷贝整条链

「包装而非丢弃」是这个模型的核心信条:当函数 A 调用 B 失败时,A 不应把 B 的错误丢掉换成自己的,而应 svn_error_createf(某码, err, "A 在做 X 时失败") 或 svn_error_quick_wrap(err, "..."),把 B 的错误作为 child 保留。这样最终打印出的错误栈能同时包含「各层上下文」与「底层根因」。svn_handle_error2(error, stream, fatal, prefix)(:292)正是把这个链完整打印出来的默认处理器。

7.8.3 SVN_ERR 宏

SVN_ERR 是使用这些 API 的语法糖,典型用法:

SVN_ERR(svn_io_check_path(local_abspath, &kind, scratch_pool));

它展开为「调用表达式;若返回值非空则 return 该错误」。在 SVN_ERR__TRACING(由 SVN_DEBUG 打开)下,它还会通过 svn_error__locate(__FILE__, __LINE__) 记录源码位置——这解释了 7.8.1 里 file/line「仅 SVN_DEBUG」的由来。与之配套的还有 SVN_ERR_ASSERT(expr)(断言,失败即致命错误),我们在 svn_client__record_wc_mergeinfo()、svn_client__get_wc_mergeinfo()、get_full_mergeinfo() 里都见过它的身影。

7.8.4 错误码分区

所有 SVN 错误码分配在 APR 的用户错误空间 APR_OS_START_USERERR 之上,按**类别(category)**划分为若干 100 个一组的区段。分类起始偏移定义在 svn_error_codes.h:109-154,格式为 APR_OS_START_USERERR + (N * SVN_ERR_CATEGORY_SIZE)。实测(部分)偏移如下:

类别偏移 N前缀
SVN_ERR_WC_CATEGORY_START7SVN_ERR_WC_*
SVN_ERR_FS_CATEGORY_START8SVN_ERR_FS_*
SVN_ERR_REPOS_CATEGORY_START9SVN_ERR_REPOS_*
SVN_ERR_RA_CATEGORY_START10SVN_ERR_RA_*
SVN_ERR_RA_DAV_CATEGORY_START11SVN_ERR_RA_DAV_*
SVN_ERR_RA_LOCAL_CATEGORY_START12SVN_ERR_RA_LOCAL_*
SVN_ERR_CLIENT_CATEGORY_START15SVN_ERR_CLIENT_*
SVN_ERR_RA_SVN_CATEGORY_START18SVN_ERR_RA_SVN_*
SVN_ERR_AUTHN_CATEGORY_START19SVN_ERR_AUTHN_*
SVN_ERR_RA_SERF_CATEGORY_START22SVN_ERR_RA_SERF_*
SVN_ERR_X509_CATEGORY_START24SVN_ERR_X509_*

每条错误用 SVN_ERRDEF(NAME, CATEGORY_START + offset, "英文描述") 声明。本次实测到的几个代表(含行号):

  • SVN_ERR_WC_LOCKED(:396);
  • SVN_ERR_FS_CORRUPT(:613)、SVN_ERR_FS_NO_SUCH_REVISION(:621);
  • SVN_ERR_REPOS_HOOK_FAILURE(:928);
  • SVN_ERR_RA_NOT_AUTHORIZED(:982);
  • SVN_ERR_MERGEINFO_PARSE_ERROR(:1379)。

客户端类别(SVN_ERR_CLIENT_*)从 第 1180 行 起密集排列,实测前缀示例:

码偏移含义
SVN_ERR_CLIENT_VERSIONED_PATH_REQUIRED+0需要版本化路径
SVN_ERR_CLIENT_BAD_REVISION+2修订号非法(checkout 文档里点名会返回它)
SVN_ERR_CLIENT_IS_BINARY_FILE+4对二进制文件做了不允许的操作
SVN_ERR_CLIENT_MODIFIED+6目标已修改
SVN_ERR_CLIENT_UNRELATED_RESOURCES+12资源不相关
SVN_ERR_CLIENT_NO_VERSIONED_PARENT+15无版本化父路径
SVN_ERR_CLIENT_NOT_READY_TO_MERGE+16尚未准备好合并
SVN_ERR_CLIENT_INVALID_MERGEINFO_NO_MERGETRACKING+21无合并跟踪却要求合并信息

类别前缀的价值在于可判别性:上层代码可以只按「前缀/区段」或具体码做判定,例如 mergeinfo.c 里就明确 err->apr_err == SVN_ERR_ENTRY_NOT_FOUND 才容忍(见 7.4.1),checkout.c 里则判定 SVN_ERR_RA_ILLEGAL_URL 来决定是否重建会话(见 7.2.3)。


7.9 路径与编码

Subversion 有一个铁律:内部字符串一律 UTF-8。无论操作系统、文件系统用什么编码,进入库边界时都要转成 UTF-8,出去时再转回本地编码。

7.9.1 四套路径工具

由于「本地路径」和「URL」语义不同,Subversion 用四套前缀把路径工具分开,全部在 libsvn_subr/dirent_uri.c 实现(本次实测的函数族):

前缀表示样例函数
svn_dirent_*本地绝对/相对文件系统路径(用本地分隔符的「internal style」)svn_dirent_dirname(:1236)、svn_dirent_basename、svn_dirent_split、svn_dirent_is_absolute(:1600)、svn_dirent_get_absolute(:1626)、svn_dirent_is_root(:953)、svn_dirent_canonicalize(:1782)、svn_dirent_internal_style(:896)、svn_dirent_local_style(:916)
svn_relpath_*相对于某根的路径(仓库根或 WC 根),无前导 /svn_relpath_join(:1210)、svn_relpath_dirname(:1291)、svn_relpath_basename、svn_relpath_prefix(:1337)、svn_relpath_skip_ancestor(:1530)、svn_relpath_canonicalize(:1714)
svn_uri_*URL(含 scheme),如 http://…svn_uri_is_root(:996)、svn_uri_dirname(:1360)、svn_uri_basename、svn_uri_canonicalize(:1677)、svn_uri__is_ancestor(:1593)、svn_uri_get_dirent_from_file_url(:2472)、svn_uri_get_file_url_from_dirent(:2592)
svn_fspath_*仓库根起的绝对路径,以 / 开头(fs = filesystem path)svn_fspath__is_canonical(:2637)、svn_fspath__canonicalize(:2644)、svn_fspath__is_root(:2656)、svn_fspath__skip_ancestor(:2664)、svn_fspath__dirname(:2675)、svn_fspath__basename、svn_fspath__join(:2717)、svn_fspath__get_longest_ancestor(:2737)

注意 svn_fspath__* 全部带双下划线——它们是内部工具(它对应本系列第 4、5 章讲的「仓库根相对路径」,正是 mergeinfo 键的形态);而 dirent/relpath/uri 三族是公开 API,在 svn_dirent_uri.h 里声明。

这四族的差别不是「随手命名」,而是各自对应一种「根」:

  • dirent 的根由操作系统决定(/ 或 C:\);它要处理本地分隔符,所以有 svn_dirent_internal_style() / svn_dirent_local_style() 双向转换。实测 svn_dirent_local_style() 会把「空串」显示成 "."(用户视角),并把内部的 / 换成本地分隔符(dirent_uri.c:920-931)。
  • relpath 的根是调用方语境(可能是仓库根,也可能是 WC 根);
  • uri 有自己的 scheme,比较时需要 svn_uri__is_ancestor();
  • fspath 的根永远是仓库根。

把这些区分开,最大的收益是避免把「本地路径」当「URL」拼接——这是许多 VCS 工具的历史顽疾,而 Subversion 用类型化前缀从 API 层面强制区分。

7.9.2 本地路径 vs URI 的转义

本地路径与 URI 之间不是简单字符串替换。svn_uri_get_file_url_from_dirent()(:2592)把本地 dirent 变成 file:// URL,过程中要对路径里的特殊字符做百分号转义;反向的 svn_uri_get_dirent_from_file_url()(:2472)则要解码。两者的存在本身就说明「本地路径」与「URI」是两种不同的表示,必须经过规范化的转换函数,而不是 apr_pstrcat("file://", path)。

7.9.3 编码转换

UTF-8 与本地编码的转换入口在 svn_utf.h:

svn_utf_string_to_utf8(const svn_string_t **dest, const svn_string_t *src, ...); /* :101 */
svn_utf_cstring_to_utf8(const char **dest, const char *src, apr_pool_t *pool);     /* :110 */
svn_utf_cstring_to_utf8_ex2(...);                                                  /* :121 */
svn_utf_cstring_to_utf8_ex(...);                                                   /* :134,带 convset_key */

svn_utf_cstring_to_utf8() 把本地 C 字符串(按 APR_LOCALE_CHARSET)转成 UTF-8;_ex2 / _ex 变体支持显式指定字符集(charset 以及转换期使用的 convset_key)。库内部几乎只在边界调用它们:读命令行参数时、读文件系统路径时、以及与用户交互时。用 svn_utf_cstring_to_utf8() 统一入口的好处是「乱码问题被收敛到一处」。


7.10 流装饰器:svn_stream_t 的叠加艺术

svn_stream_t 是 Subversion 的 I/O 抽象:一个「可以读、可以写、可以关」的对象,通过设置函数指针来定制行为(svn_io.h:934-1011):

svn_stream_t *svn_stream_create(void *baton, apr_pool_t *pool);   /* :934 */
void svn_stream_set_baton(svn_stream_t *stream, void *baton);     /* :939 */
void svn_stream_set_read2(svn_stream_t *stream, ...);             /* :949 */
void svn_stream_set_read(svn_stream_t *stream, ...);              /* :961 */
void svn_stream_set_skip(svn_stream_t *stream, ...);              /* :969 */
void svn_stream_set_write(svn_stream_t *stream, ...);             /* :974 */
void svn_stream_set_close(svn_stream_t *stream, ...);             /* :979 */
void svn_stream_set_mark(svn_stream_t *stream, ...);              /* :987 */
void svn_stream_set_seek(svn_stream_t *stream, ...);              /* :995 */
void svn_stream_set_data_available(svn_stream_t *stream, ...);    /* :1003 */
void svn_stream_set_readline(svn_stream_t *stream, ...);          /* :1011 */

这与认证的 vtable 思路如出一辙:结构 + 函数指针 = 可组合的接口。由此衍生出一系列「装饰器(decorator)」——每个装饰器接收一个流,返回一个「行为被包装过的新流」:

装饰器/构造器位置作用
svn_stream_from_aprfile2:1105把 APR 文件句柄包成流(可选是否在 close 时关闭文件)
svn_stream_open_readonly / open_writable / open_unique:1041/:1060/:1085打开文件为流
svn_stream_compressed:1229「读时解压、写时压缩」(zlib);1.4 起压缩支持始终编译进去
svn_stream_buffered:1216带缓冲的流(1.8+)
svn_stream_checksummed2 / svn_stream_checksummed:1255/:1271边读边算校验和
svn_stream_disown:1026让流不被「关闭上层时连带关闭」
svn_txdelta_apply2 / svn_txdelta_applysvn_delta.h:509/:525把 svndiff 窗口应用到目标流(delta 解码)
svn_txdelta_to_svndiff_streamsvn_delta.h:593把 txdelta 流编码成 svndiff 字节流
svn_subst_stream_translated / _to_normal_formsvn_subst.h:359/:385关键字替换、行结束符转换
svn_subst_read_specialfilesvn_subst.h:407读 svn:special 文件

把装饰器叠起来,就是仓库侧「从磁盘字节到全文」的经典管线:

磁盘文件流  (svn_stream_from_aprfile2 / svn_stream_open_readonly)
   │
   ▼  装饰:svn_stream_compressed(zlib 解压,若该 rep 是压缩的)
zlib 解码后的流
   │
   ▼  装饰:svn_txdelta_apply2(把 svndiff 窗口应用到 source/target)
svndiff 解码后的全文流
   │
   ▼  装饰:svn_stream_checksummed2(读取时计算 MD5/校验和)
   │
   ▼  消费者(写工作副本文件 / 发往网络 / 交给 diff)

这套「洋葱式」组合的妙处在于:每一层只关心自己那一段转换,不知道也不需知道上下层。同一个 svn_stream_t 接口贯穿全程,因此 svn_txdelta_apply 的 source 可以是「另一个文件的解码流」,target 可以是「任意可写流」。注意 svn_txdelta_apply2() 的注释警告「both @a source and @a target will be closed when the window handler is given a null window」——若不想被连带关闭,用 svn_stream_disown() 保护。

7.11 CLI 框架

命令行客户端 svn 的骨架不在某个库里,而是由 libsvn_subr 的 opt.c / opt_subcommand.c 加 subversion/svn/ 下的每命令一个 .c 文件组成。

7.11.1 子命令的本质:一个函数指针

每个子命令在 C 层面就是一个函数,其类型定义在 svn_opt.h:64-65:

typedef svn_error_t *(svn_opt_subcommand_t)(apr_getopt_t *os, void *baton, apr_pool_t *pool);

apr_getopt_t 是 APR 的「已解析参数游标」,子命令从 os->argv / os->ind 里取剩余位置参数;baton 由调用方自由使用(svn 里传的是 svn_cl__cmd_baton_t);pool 用于分配。三个参数、单一返回错误,与整个库的风格一致。

7.11.2 命令表:svn_opt_subcommand_desc3_t

命令表元素定义在 svn_opt.h:88-112:

typedef struct svn_opt_subcommand_desc3_t
{
  const char *name;                                     /* 命令全名 */
  svn_opt_subcommand_t *cmd_func;                       /* 对应的处理函数 */
  const char *aliases[SVN_OPT_MAX_ALIASES];             /* 别名(如 "up" for "update") */
  const char *help[SVN_OPT_MAX_PARAGRAPHS];             /* 多段帮助文本 */
  int valid_options[SVN_OPT_MAX_OPTIONS];               /* 该命令接受的选项枚举列表 */
  struct { int optch; const char *desc; } desc_overrides[SVN_OPT_MAX_OPTIONS];
} svn_opt_subcommand_desc3_t;

上限常量(svn_opt.h:69-81)为 SVN_OPT_MAX_ALIASES = 3、SVN_OPT_MAX_OPTIONS = 50、SVN_OPT_MAX_PARAGRAPHS = 100(1.11 新增),以及「无短选项的选项从 SVN_OPT_FIRST_LONGOPT_ID = 256 起编号」的约定。第 2 代 svn_opt_subcommand_desc2_t(:120-144)的差别只有一个 help 字段(单段串 vs 多段数组)——这又是一处「数字后缀 = 代数」的小案例:1.11 把帮助文本从单段升级为多段,于是 desc3_t 取代 desc2_t。

围绕命令表的一组工具函数实现在 libsvn_subr/opt_subcommand.c:svn_opt_get_canonical_subcommand3()(:49,按名字或别名查表)、svn_opt_subcommand_takes_option4()(:184,判断某选项是否被该命令接受)、svn_opt_print_generic_help3()(:403,打印通用帮助)、svn_opt_subcommand_help4()(:446,打印单命令帮助)。旧代数(2/3)的实现保留在 deprecated.c 里转发。

而选项解析本体(-- 前后、@ peg 分隔、修订号解析等)在 libsvn_subr/opt.c:svn_opt_parse_path()(:137)会用 svn_opt__split_arg_at_peg_revision()(:360)把 TARGET@PEG 拆开;svn_opt__arg_canonicalize_url()(:400)与 svn_opt__arg_canonicalize_path()(:438)分别规范化 URL 与路径——这正对应 7.9 里「URI 与 dirent 必须分开处理」的思想。

7.11.3 subversion/svn/ 的每命令一个文件

Glob subversion/svn/*.c 可看到约 50 个文件,命名规约清晰:每个命令对应一个 *-cmd.c(checkout-cmd.c、update-cmd.c、commit-cmd.c、merge-cmd.c、diff-cmd.c、propget-cmd.c、mergeinfo-cmd.c、upgrade-cmd.c…),公共设施集中在 cl.h、util.c、notify.c、conflict-callbacks.c、cl-conflicts.c、svn.c。命令表本身在 svn.c(即 main.c 的实体)里。

7.11.4 选项状态:svn_cl__opt_state_t

所有解析出的选项汇聚到 svn_cl__opt_state_t(定义于 subversion/svn/cl.h:230-280 一带)。任务点名关注的三个字段(实测行号):

  • vacuum_pristines(:270):svn_boolean_t,注释「remove unreferenced pristines」——对应 --vacuum-pristines,清理工作副本中不再被引用的 pristine 内容;
  • compatible_version(:278):svn_version_t *,注释「working copy compatibility version」——对应 --compatible-version,指定目标工作副本格式所兼容的客户端版本;
  • store_pristine(:279):svn_boolean_t,注释位置紧邻——对应 --store-pristine/--no-store-pristine,控制 pristineless 模式。

这三个选项正是 1.15「pristineless 模式」与「多工作副本格式」两大特性的命令行入口。它们经由 checkout-cmd.c 等翻译成 svn_client_checkout4() 的 wc_format_version / store_pristine 参数。

元数据里还有对应的枚举:svn_cl__longopt_t(:296 起)里可见 opt_vacuum_pristines、opt_compatible_version、opt_store_pristine 三项(实测 :376-380)——它们从 SVN_OPT_FIRST_LONGOPT_ID 起编号,因为都是无短选项的长选项。

7.12 语言绑定

libsvn_* 都是 C 库,其它语言的用户通过绑定层访问。

7.12.1 SWIG:Python / Perl / Ruby / PHP

subversion/bindings/swig/ 目录(本次 Glob 实证)下有这些 .i 接口文件:core.i、svn_client.i、svn_delta.i、svn_diff.i、svn_fs.i、svn_ra.i、svn_repos.i、svn_wc.i,外加 INSTALL、NOTES。思路是:用 SWIG 把 C 头文件里的声明机械地生成目标语言的包装,四个脚本语言共享同一批 .i(外加各语言子目录里的语言定制)。svn_auth.h 里那条注释「the gnome keyring unlock prompt related items below must be declared for all platforms in order to allow SWIG interfaces to be used regardless of the platform」(:989-991)正是为 SWIG 服务的:即使平台不支持 GNOME Keyring,这些类型也得在头文件里声明,否则 SWIG 生成不了对应语言的类型。

7.12.2 JavaHL:Java 绑定

subversion/bindings/javahl/ 提供手写的 Java 绑定(JavaHL),本次 Glob 到 Manifest.in、README(其余源码在子目录中)。与 SWIG 的自动生成不同,JavaHL 是一层显式的 JNI 胶水,设计目标是给 IDE(如 Eclipse Subversive/Subclipse)提供与 C 客户端等价的能力。它的存在解释了为什么 svn_client.h、svn_wc.h 的改动要格外小心——JavaHL 直接对 C 结构体做映射,任何签名变化都可能波及 Java 侧。

两个绑定层共同构成「同一套 C 核心,多语言出口」的架构,也再次强调了 7.1.2「数字后缀 + 结构体只增不改」的重要性:它们依赖的正是这套稳定的 ABI。


7.13 边界与演进

前面各节讲的是「机制」,这一节讲「机制的边界」以及 1.15 在客户端/基础设施上的演进。所有 r 号均来自本次实际读取的 CHANGES。

7.13.1 错误链的边界:何时可以「吞掉」错误

错误链虽好,但有两条边界必须清楚:

  1. 可容忍的错误要显式 svn_error_clear()。mergeinfo.c 里对 SVN_ERR_ENTRY_NOT_FOUND 的处理是范例——当某路径因 authz 从工作副本缺失时,这不是致命错误,于是「clear 错误并继续」(mergeinfo.c:178-187)。注意注释解释了根因:「we don't have access to the source due to authz restrictions. For now just clear the error and continue」。这类「有理由的静默」是允许的,但必须写清理由。
  2. 不可容忍的错误要「包装而非丢弃」。若把底层错误清掉、换成一句「操作失败」,最终用户永远看不到根因。svn_error_createf(apr_err, child, fmt, ...) 与 svn_error_quick_wrap(child, msg) 就是为此而生——把子错误挂进 child,让 svn_handle_error2() 打印出完整链。

另外,svn_error_find_cause(err, apr_err)(:233)允许从一条长链里「按码捞取」特定错误;svn_error_root_cause()(:222)取链尾根因。二者在 checkout.c 里都用得上:它判定 err->apr_err == SVN_ERR_RA_ILLEGAL_URL 来决定要不要清错重开会话(7.2.3 步骤 2)。

边界情形:SVN_DEBUG 关闭时,链上「只带堆栈」的链节没有 file/line,只剩 message 可能为空。头文件因此提醒用 svn_err_best_message() 取「最佳单行描述」而不是直接读 message。这解释了为什么很多发行版里错误信息看似「少了几层」——那是 svn_error_trace() 的链节在非 debug 下没有文件位置信息而已。

7.13.2 取消:贯穿始终的 cancel_func

长操作(checkout、update、commit、merge)必须可被中断。机制是一对字段:svn_client_ctx_t.callback 中的 cancel_func + cancel_baton(svn_client.h:992-997)。约定是:任何可能耗时的循环都应周期性调用 cancel_func(cancel_baton, pool),返回非空错误即中止。

取消的边界在于层与层之间必须传递同一对回调:CLI 的 cancel_func 挂在 ctx 上,随 ctx 一路传进 svn_client_checkout4() → svn_client__checkout_internal() → svn_client__update_internal() → 工作副本编辑器…每个可能长跑的循环都从这里取。若中间某层「忘了传」,中断就会在最需要它的地方失效——因此新写的内部函数几乎都把 ctx(承载 cancel_func)作为必备参数,而不是可选参数。这也是为什么 svn_client__update_internal()、svn_client__checkout_internal() 的签名里都带 svn_client_ctx_t *ctx。

7.13.3 混合修订下的 mergeinfo:继承与消隐都失效

这是合并跟踪最微妙的边界,svn_mergeinfo.h 开篇原话已经写得很清楚(:68-72):

Due to Subversion's allowance for mixed revision working copies, both elision and inheritance within the working copy presume the path between a path and its nearest parent with mergeinfo is at the same working revision. If this is not the case then neither inheritance nor elision can occur.

也就是说:工作副本允许「混合修订(mixed-revision)」——同一棵目录树里不同路径的 base revision 可以不同。而 mergeinfo 的继承与消隐都建立在「子路径与最近的有 mergeinfo 的父路径处于同一工作修订」这一假设上。一旦这个假设被打破(中间某一级路径的修订与子路径不一致),继承和消隐都不能发生。

这条限制在 svn_client__merge_path_t 里留下了痕迹:字段 pre_merge_mergeinfo 的注释是「Explicit or inherited mergeinfo on ABSPATH prior to a merge」,inherited_mergeinfo 是「Whether PRE_MERGE_MERGEINFO was explicit or inherited」——正因为在混合修订下「继承可能失败」,才需要单独记录「这份 mergeinfo 到底是不是继承来的」。

svn_client__get_wc_mergeinfo() 的文档(mergeinfo.h:108-137)给出了「什么条件下才向上找继承」的精确判据:

This function will search for inherited mergeinfo in the parents of LOCAL_ABSPATH only if the base revision of LOCAL_ABSPATH falls within the range of the parent's last committed revision to the parent's base revision (inclusive) or is LOCAL_ABSPATH is a local addition.

即:只有当子路径的 base revision「落在父路径 [最后提交修订, base 修订] 区间内」、或子路径是「本地新增」时,才会向父路径找继承 mergeinfo。实现里对应 svn_wc__node_get_base() 取 base_revision、svn_wc__node_get_changed_info() 取父路径的 parent_changed_rev 等一连串判断(mergeinfo.c:224-305)。这解释了为什么在实际使用中,「对 mixed-revision 的工作副本做 merge」常会看到更多 svn:mergeinfo 落在子路径上(因为消隐条件不满足)——这不是 bug,而是「为保持正确性放弃优化」的必然结果。

7.13.4 能力协商失败的降级:一个完整的边界例

把 7.4.4 与本节结合起来看,一次 merge 的「能力协商边界」是这样的:

  • 若服务端宣告 SVN_RA_CAPABILITY_MERGEINFO → 走「mergeinfo-aware merge」:读写 svn:mergeinfo、做继承/消隐、用 svn_ra_get_mergeinfo() 取仓库侧数据;
  • 若服务端未宣告 → 客户端必须降级为「non-mergeinfo-aware merge」:退化为普通 diff-and-apply,不碰 svn:mergeinfo;
  • 若用户在此情况下仍显式要求合并跟踪语义 → 报 SVN_ERR_CLIENT_INVALID_MERGEINFO_NO_MERGETRACKING(svn_error_codes.h:1276)。

这条「能力缺失 → 平滑降级,但若强求则显式报错」的模式,在整个客户端里反复出现(深度、原子 revprops、继承属性等能力都有对应降级路径),是理解「客户端为何要问一大堆 capability 问题」的钥匙。

7.13.5 1.15 客户端与基础设施相关变更(按 r 号串联)

CHANGES 中与本章主题相关、本次实测到的条目,按 r 号列出:

r 号 / issue类别内容
r1905955 (issue #525)重大新特性New 'pristineless' mode,消除 pristine 缓存开销
r1898187 (issue #4883)重大新特性Support for multiple working copy formats
r1909351认证Re-enable plaintext credential store by default on Unix
r1926564认证Reimplement macOS Keychain auth provider with SecItem* API
r1888474 et al (issue #4874)merge trackingWarn on merges from foreign repositories if UUIDs match but URLs differ
r1886490 et al客户端优化同时把数据流式写入 pristine 与工作文件(checkout/update/export),Windows 上减少文件重开次数
r1892645客户端用密码学校验和判断文件是否被修改
r1909127 (issue #4913)客户端 bugfixFix assertion on svn move [URL] [URL]/subdir
r1929793ra_serfFix segfault on unexpected server responses
r1878084ra_serfOptimize array filling for inherited properties
r1927953Windows CLIDisable best-fit Unicode mapping for Windows command-line arguments
r1926252CLIPrint character encoding in svn --version --verbose
r1882127 et alCLIShow new macOS versions in svn --version --verbose
r1910826CLI/文档Document trailing-@ trick to avoid peg-revision parsing

逐条与本章内容的呼应:

  • r1905955 / r1898187(pristineless 与多工作副本格式)直接催生了 svn_client_checkout4() 相对 _checkout3 新增的 wc_format_version(svn_version_t *)与 store_pristine(svn_tristate_t)两个参数,以及 cl.h 里 --compatible-version/--store-pristine 选项(见 7.1.2、7.11.4)。内部对应 svn_client__compatible_wc_version_optional_pristine() 与 svn_wc__format_from_version()(见 7.2.3 步骤 1)。
  • r1909351 对应 7.5.8 讲的明文凭据默认恢复;r1926564 对应 macOS Keychain provider 的 SecItem* 重写。
  • r1888474 对应 7.4.5 的跨仓库 UUID 警告。
  • r1927953(Windows 命令行参数禁用 best-fit Unicode 映射)落在 7.9 的编码边界上:Windows 的 argv 从前会做「best-fit」宽字符→字节映射,可能把命令行里的 Unicode 参数静默替换成「长得像」的字符,导致路径或 URL 出错。禁用后,参数保持原样、交由 svn_utf_cstring_to_utf8() 之类的显式转换处理——这正是「内部一律 UTF-8、只在边界显式转换」原则的又一处落实。
  • r1929793(ra_serf 对非预期服务端响应的段错误修复)属于 RA 客户端健壮性(第 4、5 章范畴),但因 ra_serf 是 svn_client 默认 HTTP 后端,也直接影响客户端稳定性。
  • r1926252(svn --version --verbose 打印字符编码)与 r1882127(显示新版 macOS 版本)都是 svn --version --verbose 诊断输出的改进——它们呼应 7.11 的 CLI 框架:诊断信息要能反映「客户端运行在什么编码/什么平台上」,便于排障。
  • r1886490(checkout/update/export 同时流式写入 pristine 与工作文件)与 7.10 的流装饰器管线直接相关:优化的正是「解码后的全文流如何同时喂给两个消费者」,从而不必先写 pristine 再写工作文件、避免二次读盘。
  • r1892645(用密码学校验和判断文件是否被修改)对应 7.10 里 svn_stream_checksummed2 的用途——从「比大小/比时间」升级为「比内容校验和」,减少误判。
  • r1910826(尾部 @ 技巧避免 peg 解析)落在 7.11.2 的 svn_opt__split_arg_at_peg_revision():由于 TARGET@PEG 用 @ 分隔,若路径本身含 @,就得用尾随 @ 来「终止」peg 解析。

7.13.6 演进的「方向感」

把本章的观察汇总,可以看出客户端/基础设施这几年的演进有三条明显的主线:

  1. ABI 恒定,能力递增:用「数字后缀代数」「结构体只增不改」「双下划线内部 API」三件套,让 libsvn_client 与 libsvn_subr 的能力不断加码,却几乎不破坏既有二进制接口。1.15 新增的 wc_format_version/store_pristine 走的就是这条「加参数、升代数」的老路。
  2. 跨平台差异收敛到显式边界:认证的 provider 名与平台工厂(7.5.6)、路径的 dirent/relpath/uri/fspath 四族(7.9.1)、编码的 svn_utf_* 转换(7.9.3)、Windows argv 的 Unicode 处理(r1927953)——平台差异全部被赶到「边界函数」里,库内部保持一致的 UTF-8/规范形式。
  3. 缓存与池的性能优先,但边界清晰:缓存配置是「进程级单例、只调一次、软上限」(7.6),池是「显式双池 + 长循环每轮 clear」(7.7)。这些约定明确写着「不保证改变正在用的配置」「非线程安全」等边界,把「性能」与「正确性」的责任划分得很干净。

7.14 本章小结

本章把客户端的「表层」(libsvn_client 门面与调用链)与「底座」(认证、缓存、池、错误、路径、流、CLI、绑定)都过了一遍。核心结论可以浓缩成几句:

  • svn_client_ctx_t 是客户端的「会话状态中心」,字段按代数递增、回调成对、可空性有语义;
  • 公开 API 用「数字后缀 = 代数」保持 ABI 稳定,内部用 svn_client__* 自由重构;
  • 一次 checkout 是 svn_client_checkout4 → svn_client__checkout_internal → svn_client__ra_session_from_path2 → svn_ra_open5 → … → svn_client__update_internal 的一条编排;
  • mergeinfo 有明确的文本文法(含 * 不可继承标记)与「继承/消隐」两条规则,且都受混合修订限制;
  • 认证是一条按注册顺序推进、以 realmstring 为索引的 provider 责任链,参数全集就是那 14 个 SVN_AUTH_PARAM_*;
  • 缓存分配置层与后端层(inprocess/membuffer/memcache),错误分链式模型与类别分区,路径分四族,编码只在边界转换,流靠装饰器叠加。

理解了这些,再看本系列后续章节里某个具体子命令(如 svn blame、svn patch)时,就能迅速定位它在 libsvn_client 里落到了哪个函数族、依赖了哪些基础设施。


7.15 补充:上下文创建与命令行目标规范化

在收尾之前,补两块与「上下文」和「CLI 目标」直接相关、前面略过但实测细节很值得记录的接口。

7.15.1 svn_client_create_context2():为什么必须用它

svn_client_ctx_t 只增不改的代价是「调用方无法自己 malloc」。因此头文件提供了两个创建函数(svn_client.h:1105-1118):

svn_error_t *
svn_client_create_context2(svn_client_ctx_t **ctx, apr_hash_t *cfg_hash, apr_pool_t *pool); /* 1.8 */

SVN_DEPRECATED
svn_error_t *
svn_client_create_context(svn_client_ctx_t **ctx, apr_pool_t *pool);   /* 1.7,转发 NULL 的 cfg_hash */

其文档把「为什么」写得一清二楚(:1094-1101):为了避开向后兼容问题,客户端必须用这个函数初始化并分配结构体,因为「the size of this structure may change in the future」。这正对应 7.1.1 里 client_name 之后仍可能追加字段的事实——预留结构体尺寸变化的余地。同时文档还交代了一个细节:cfg_hash 会被存入返回对象的 config 成员,且「should remain valid for the lifetime of the object」——即它的生命期要覆盖整个 context,这恰好是 7.7.2「config 属于长寿池对象」的来源。最后,文档提醒「current implementation never returns error, but callers should still check for error」——保留了未来「可能失败」的兼容空间。

create_context2 与 cfg_hash/config 的绑定,也解释了为什么 1.15 的 checkout-cmd.c 能在同一 ctx 上反复执行多个子命令:配置只解析一次、wc_ctx 只建一次、认证 baton 只建一次。

顺带一提,svn_client.h 里还有两个早已无人使用的历史常量(:1122-1131):

/* @deprecated Provided for backward compatibility. This constant was never
   used in released versions. */
#define SVN_CLIENT_AUTH_USERNAME            "username"
#define SVN_CLIENT_AUTH_PASSWORD            "password"

注释罕见地直言「was never used in released versions」——它们是早期设计遗留的化石,保留只为二进制兼容。

7.15.2 svn_client_args_to_target_array2():目标的五步规范化

CLI 把用户输入的「目标(targets)」交给客户端之前,要做一次统一规范化。这个函数是 svn_client_args_to_target_array2()(1.7+,对应 svn_opt__process_target_array 一类的内部实现)。文档(:1140-1174)列出了它按目标种类的处理方式:

  1. 先做 UTF-8 转换:把参数「converting them to UTF-8」——与 7.9.3 的边界转换一致;
  2. 仓库相对 URL(repository-relative URL):解析成完整 URL,必要时联系仓库(因此 ctx 是必需参数,「required for possible repository authentication」)再当完整 URL 处理;
  3. 完整 URL:做「some IRI-to-URI encoding and some auto-escaping」,然后 canonicalize——这正是 7.9.2 讲的「URI 转义」的集中落点;
  4. 本地路径:规范化大小写与路径分隔符(canonicalize case and path separators);
  5. 保留名检查:若某路径与 Subversion 的「工作副本管理目录」同名(如 .svn),返回 SVN_ERR_RESERVED_FILENAME_SPECIFIED;若有多个,返回一条全为同种错误的链(文档强调「Do not return this type of error in a chain with any other type of error」)。

此外文档还记录了一条重要的 peg 语法历史边界:仅由 peg 说明符构成的参数(如 "@abc")在 v1.6.5(r878062)之前会被当成字面路径,之后则含义变为歧义、直接报错。要表达「字面路径 @abc 且无 peg 修订」,得写成 "@abc@";要表达「空路径 + peg 修订 abc」,得写成 ".@abc"。注意这里的 r878062 是 Subversion 早期 rXXXXX 风格的修订号(远小于 1.15 的 190 万量级),它来自代码注释而非 CHANGES——本章的「r 号必须可回溯」原则同样适用于代码里的注释引用。

svn_client_args_to_target_array2() 的存在说明:「用户输入 → 规范化目标」这一步本身也是客户端库的职责,而不是散落在各子命令里。这也再次印证 7.11.2 里 svn_opt__arg_canonicalize_url() / svn_opt__arg_canonicalize_path() 的分工——CLI 层(opt.c)负责粗加工,客户端层(args_to_target_array2)负责最终规范化并处理 URL/路径的语义差异。

7.15.3 本节与全章的呼应

把这两节放回全章脉络:

  • svn_client_create_context2() 是「上下文生命周期」的入口,决定了 svn_client_ctx_t 能在多长寿的池上存活(呼应 7.1.1、7.7.2);
  • svn_client_args_to_target_array2() 是「用户输入 → 内部表示」的入口,把 UTF-8、URI 转义、路径规范化、保留名检查、peg 解析五件事一次性做完(呼应 7.9 与 7.11)。

两者一「出」一「入」:一个产出承载会话状态的 ctx,一个把裸参数翻译成库能理解的规范目标。理解它们,就理解了「客户端把命令行世界翻译成库内部世界」的两端边界。