☰
EdenFS 文件通配(Globbing)机制解析:Ignore 文件与 globFiles Thrift 接口的完整指南
2026/10/7 9:27:51 网站建设 项目流程
  • 开发工具
  • CLI
  • 后端

【免费下载链接】sapling

A Scalable, User-Friendly Source Control System.

项目地址:https://gitcode.com/gh_mirrors/sa/sapling
点击查看免费下载

EdenFS(Sapling 的虚拟文件系统守护进程)在内部提供了两套 glob 能力:一是用于getScmStatus状态查询的 ignore 文件匹配,二是暴露给客户端(如hg status、edenfsctl glob、prefetch 工具)的globFilesThrift API。本文以 EdenFS File Globs 文档 为骨架,结合 eden.thrift、GlobNodeImpl、ThriftGlobImpl 与 GlobNodeTest 等源码,系统讲解 glob 模式的语法语义、编译与匹配的底层实现、Thrift 参数含义、符号链接的特殊行为,以及命令行与测试验证方法。

EdenFS 中的两类 Glob 接口

EdenFS 支持 glob 模式的入口共有两个,二者面向的场景不同:

接口用途典型调用方
Ignore 文件(如.gitignore)在getScmStatusThrift API 中排除文件hg status等状态查询
globFilesThrift API显式按模式查询匹配路径edenfsctl glob、prefetchFiles、集成客户端

前者用于“告诉 EdenFS 哪些文件不该被报告”,后者用于“按模式枚举仓库中的文件/目录”。两条路径共享同一套模式语法:EdenFS 的 glob 语法与 Git 版本控制系统的gitignore模式格式兼容,即便 EdenFS checkout 底层是 Mercurial 仓库也是如此。

从源码看,这份兼容性承诺落实在 GlobMatcher.h 的实现注释中:

"GlobMatcher performs matching of filename glob patterns. This aims to be100% compatible with the syntax used in gitignore files."

也就是说,你在.gitignore里能写的模式,在globFiles里同样成立;反过来,glob 模式中出现的特殊字符在 ignore 文件中语义也一致。

Ignore 文件:状态查询中的排除规则

工作方式与优先级

EdenFS 使用 ignore 文件在getScmStatusThrift API(例如hg status的底层调用)中排除文件。ignore 文件的语法与 Git 的gitignore完全兼容,即使 checkout 的仓库类型是 Mercurial。

在源码层面,单个 ignore 文件的解析与匹配由 GitIgnore.h 中的GitIgnore类承担,它返回四种匹配结果:

enum MatchResult { EXCLUDE, // 该路径被 ignore 规则显式排除 INCLUDE, // 该路径被规则显式包含(重新包含) NO_MATCH, // 未命中任何规则,可继续向低优先级规则查询 HIDDEN, // 特殊隐藏路径(如 .hg、.eden),完全不报告 };

注释明确指出:对于一条完整路径,通常需要按“优先级从高到低”依次检查多个GitIgnore对象,这一步由GitIgnoreStack完成:

  1. 路径所在目录的.gitignore;
  2. 逐级向上直到仓库根目录的每一层.gitignore;
  3. Eden 客户端全局 exclude 文件(systemIgnoreFile);
  4. 用户个人 exclude 文件(userIgnoreFile)。

每一层都可能返回“显式排除 / 显式包含 / 未命中”,一旦命中显式排除或包含就立即停止;全部未命中则视为隐式包含。此外还有一个重要规则:如果某个目录被 ignore,那么它内部的任何内容都被视为已忽略——即使显式 include 规则也不能“重新包含”被忽略目录内的未跟踪文件。

EdenFS 守护进程侧通过 ServerState.cpp 维护userIgnoreFileMonitor_与systemIgnoreFileMonitor_两个CachedParsedFileMonitor<GitIgnoreFileParser>,分别跟踪配置项userIgnoreFile与systemIgnoreFile对应的文件,文件变化后自动重新解析。

一个容易混淆的点

需要特别说明:ignore 文件只影响getScmStatus这类状态接口的过滤,不参与globFiles的结果过滤。edenfsctl glob的帮助文本明确写明了这一点(见 glob.rs):

"Print matching filenames. Glob patterns can be provided via a pattern file.This command does not do any filtering based on source control state or gitignore files."

因此,glob 结果可能包含被 ignore 的文件,这是设计使然,不是缺陷。

Glob 模式中的特殊字符语义

EdenFS 在 glob 模式中按如下规则解释特殊 token:

Token语义
**匹配零个、一个或多个路径分量(跨目录层级)
*匹配零个、一个或多个合法的路径分量字符(不跨目录分隔符)
?匹配恰好一个合法的路径分量字符
[匹配给定字符集合中的恰好一个路径分量字符,集合以]结束
[!、[^匹配不在给定字符集合中的恰好一个路径分量字符,集合以]结束

这些语义与 gitignore 模式一致。注意*与**的关键差别:*不能跨越/(目录分隔符),**则专门用于跨目录的递归匹配。

模式编译:把模式拆成“节点树”

理解这些 token 的最佳方式是看编译过程。 GlobNodeImpl.h 的类注释给出了核心设计:

将 glob 按路径分量拆分,构建一棵“名字匹配操作”的树。对非递归 glob 而言,这允许我们高效地边遍历目录树边比较。不含 glob 特殊字符的路径分量可以直接按名字在目录内容中做 ID 查找,而不是把模式与每个条目反复做模式匹配。

具体实现位于 GlobNodeImpl.cpp 的parse():模式被按/逐段 token 化,每段生成一个GlobNodeImpl子节点;普通子段进入children_,遇到**则进入recursiveChildren_(递归匹配会破坏大部分优化,因此**之后不再继续 token 化)。

每个节点有两个关键布尔标志(见 GlobNodeImpl.h):

  • hasSpecials_:该段是否含特殊字符。为false时走精确名字查找分支(lookupEntry,按路径分量做 hash 查找);为true时才遍历目录条目逐一调用GlobMatcher。这就是大目录下dir/sub/file.txt这类“全字面量”模式能保持高性能的原因。
  • isLeaf_:该节点是否是模式的最后一个分量(叶子),决定是否产出匹配结果。

大小写与点文件选项

编译每个节点时(GlobNodeImpl.cpp),GlobMatcher::create(pattern, options)会按以下逻辑组合选项:

  • includeDotfiles == true时使用GlobOptions::DEFAULT,否则使用GlobOptions::IGNORE_DOTFILES(默认忽略点文件);
  • 大小写不敏感(CaseSensitivity::Insensitive)时追加GlobOptions::CASE_INSENSITIVE;
  • 当includeDotfiles=true且模式就是**或*时,节点被标记为alwaysMatch_(无条件命中,跳过匹配器);
  • 模式编译失败会抛出携带EINVAL的std::system_error,给出失败原因。

顺带一提,GlobMatcher还内置了两道匹配保护:失败状态记忆化上限(默认 65,536)与回溯步数上限(默认 100,000),防止病态模式拖垮守护进程(见 GlobMatcher.h)。

globFiles Thrift API:参数与结果结构

globFiles的完整定义位于 eden.thrift:

Glob globFiles(1: GlobParams params) throws (1: EdenError ex);

请求参数结构GlobParams(eden.thrift)字段如下:

字段类型说明
mountPointPathString挂载点路径
globslist<string>要执行的 glob 模式列表
includeDotfilesbool是否把以.开头的条目纳入匹配
prefetchFilesbool若为 true,同时预取匹配文件的 blob
suppressFileListbool若为 true,不在Glob结果里填充 matchingFiles(通常与 prefetchFiles 配合)
wantDtypebool是否返回每个匹配条目的 dtype
revisionslist<ThriftRootId>要针对哪些提交(revision)求值;为空则只对当前 checkout revision 求值,列表中不应有重复项
prefetchMetadatabool已无实际效果(保留字段)
searchRootPathStringglob 从哪个目录开始求值,默认是仓库根目录
backgroundbool若为 true,预取在后台执行、不等结果
predictiveGloboptional PredictiveFetch已废弃、被守护进程忽略(仅保留 wire 兼容)
listOnlyFilesbool只返回文件、过滤目录(注意:为 false 时 matchingFiles 并不等同于实际喂给 backing store 的预取列表)
syncSyncBehavior本次 glob 查询是否同步工作副本

返回结构Glob(eden.thrift):

struct Glob { 1: list<GlobPathValue> matchingFiles; // 可能含重复值,且不保证有序 2: list<OsDtype> dtypes; // 每个匹配条目的文件类型 3: list<binary> originHashes; // 匹配文件所属提交的哈希 }

matchingFiles可能包含重复值(多个模式命中同一文件)、不保证有序;originHashes目前是匹配文件所属提交的 commit hash(若 EdenFS 未来放弃 commit hash,则可能改为根树哈希)。当传入多个revisions时,同一文件可能对应多个 origin hash。

内部实现:GlobTree 与 GlobNode 两条求值路径

globFiles的服务端实现位于 ThriftGlobImpl.cpp,它根据是否指定revisions选择两条不同的求值路径:

指定 revisions → GlobTree(基于 tree 元数据)

当revisions非空时,代码对每个 revision 取根树(co_getRootTree),沿searchRoot解析出子树,然后构造GlobTree针对存储层的原始 Tree 元数据求值(GlobTree.cpp)。这种方式不触碰工作副本 inode,可对任意历史提交做只读 glob。测试中的builder_.setAllReady()对应的正是“树已就绪、直接按元数据匹配”的场景。

无 revisions → GlobNode(基于工作副本 inode)

当revisions为空时,回退到当前 checkout:先通过edenMount->co_getInodeSlow(searchRoot, ...)拿到目录 inode,再构造GlobNode在工作副本上求值(ThriftGlobImpl.cpp)。GlobNode(GlobNode.cpp)会感知 overlay 中的物化(materialized)状态——测试 recursiveTxtWithChanges 证明:通过 overlay 添加的文件、符号链接以及被chmod物化后的文件都能被正确枚举出来。

两种路径共享同一套evaluateImpl/evaluateRecursiveComponentImpl模板(GlobNodeImpl.h),通过TreeInodePtrRoot(加锁遍历 inode 目录)与TreeRoot(直接遍历存储树)两个策略对象统一访问方式。**的递归分量由evaluateRecursiveComponentImpl处理:它会先收集所有递归子目录,释放目录锁后并发加载子树,并利用recursiveAsyncDepth(默认 3,由配置globRecursiveAsyncDepth控制)决定前几层递归是否强制切换到 folly executor 异步执行。并发聚合采用collectAllTryRange而非collectAllRange——注释明确指出后者会在首个失败时向兄弟任务发送协作取消,可能导致globResult/prefetchList写入不完整。

大小写敏感由配置驱动

值得注意:GlobNode 路径的匹配大小写并不是硬编码的,而是看配置项globUseMountCaseSensitivity(见 ThriftGlobImpl.cpp):

  • 为 true(默认启用时)→ 使用 mount 的 checkout 配置(getCaseSensitive());
  • 为 false → 强制大小写敏感(CaseSensitivity::Sensitive)。

结果结构:GlobResult

每次命中都会生成一条 GlobResult,包含name(相对路径)、dtype(条目类型,如dtype_t::Regular、dtype_t::Symlink、dtype_t::Dir)以及originId(该文件所属的 root id)。dtype_t::Symlink这一枚举直接支撑了下一节的符号链接行为。

与符号链接(Symlink)相关的行为

原文档对符号链接行为的规定如下:

  1. 模式精确命中符号链接本身:globFile返回该符号链接自身(而不是它的 target)作为匹配结果。
  2. 模式的前缀命中符号链接:globFile既不返回该符号链接,也不返回其 target,更不会解析符号链接去继续匹配模式剩余部分。

换句话说:符号链接可以被“整体命中”,但永远不会被“钻进去”当作目录遍历。测试 recursiveTxtWithChanges 对第一条规定给出了直接证据:挂载点中addSymlink("sym.txt", "root.txt")之后,用**/*.txt匹配,期望结果包含GlobResult("sym.txt"_relpath, dtype_t::Symlink, kZeroRootId)——返回的正是符号链接自身,且 dtype 明确标记为Symlink。

第二条规定对应实现中entryIsTree()判定:符号链接条目在遍历时不满足“是目录”的条件,因此不会被recurseIfNecessary继续下钻,也不会触发co_getOrLoadChildTree。这与VirtualInode.cpp中多处对dtype_t::Symlink的显式拒绝(如计算 blake3/SHA1 时拒绝符号链接)是一致的防御性设计。

命令行实战:edenfsctl glob 与 prefetch

glob 子命令

edenfsctl glob(glob.rs)是globFiles的直接 CLI 封装,常用参数:

edenfsctl glob [OPTIONS] <PATTERN>...
  • --mount-point <PATH>(别名--repo):指定挂载点,默认取当前目录所属仓库根;
  • --pattern-file <FILE>:从文件逐行读取模式;-表示从标准输入读取;
  • --include-dot-files:把以.开头的文件纳入匹配;
  • --list-only-files:只输出文件、过滤目录;
  • --dtype:输出每个匹配条目的文件类型(如 regular / symlink / directory);
  • --revision <HASH>:指定要搜索的 revision,可重复使用(多 revision 时结果会带 origin hash);
  • --list-origin-hash:显示匹配文件所属提交的哈希(仅当指定多个--revision时才有数据);
  • --json:以 JSON 格式输出;
  • --verbose:额外打印匹配文件数、dtypes 数、origin hashes 数。

模式参数相对仓库根目录解释,底层直接调用glob_files(mount_point, patterns, include_dot_files, ..., want_dtype, search_root, ..., list_only_files),与 Thrift 层的GlobParams一一对应。注意该命令不做基于源码控制状态或 gitignore 的过滤。

prefetch 子命令

glob 的另一个重要消费方是预取。PrefetchParams(eden.thrift)同样接收globs、revisions、searchRoot、directoriesOnly(只预取树、不取 blob)、background、preload/preloadProgress(预取完成后继续预热 OS 页缓存)等参数。Python 侧的实现位于 prefetch.py:它先把 checkout 与模式规范化,再调用client.globFiles(globs=..., prefetchFiles=..., ...)获得待预取文件,随后按prefetchBlobBatchSize分批调用store->prefetchBlobs(见 ThriftGlobImpl.cpp)。

在GlobNode::evaluate的签名中可以看到预取与 glob 的伴生关系(GlobNode.h):fileBlobsToPrefetch与globResult同时传入,命中文件时若条目未物化且非目录,就把对象 ID 追加到PrefetchList,实现“查一次、预取一次”。测试matchFilesByExtensionRecursively(GlobNodeTest.cpp)验证了**/*.txt会同时产出两个匹配与两个待预取对象 ID。

测试体系:如何验证 glob 行为

glob 的核心单元测试集中在 GlobNodeTest.cpp,它以参数化测试(TEST_P)同时覆盖“inode 路径 / 存储树路径”与“是否预取”两种维度。值得留意的用例:

  • starTxt/star/starExcludeDot:验证*的单层匹配以及includeDotfiles对点文件的开关作用(默认排除.eden、.watchmanconfig等点文件);
  • starStarExcludeDot、starStarRootExcludeDot、dotDirectoryStarExcludeDot:验证**递归匹配与点文件排除的组合行为;
  • matchFilesByExtensionRecursively:**/*.txt递归命中嵌套目录,并给出对应的预取对象 ID;
  • recursiveTxtWithChanges:覆盖 overlay 新增文件、符号链接、物化(chmod)条目的枚举,同时验证“物化条目不进入预取列表”(只有未物化的dir/sub/b.txt出现在expectIds);
  • matchGlobDirectoryAndDirectoryChild:同时注册dir/*与dir/*/*两条模式,验证模式树合并后目录与子目录条目都能正确命中。

结合这些用例与上文实现,可以总结出 EdenFS glob 的完整行为链:模式编译成节点树(含**递归分支)→ 按hasSpecials_选择精确查找或匹配器扫描 → 命中叶子节点时产出GlobResult并收集预取对象 → 对目录条目(排除受限目录与符号链接)并发下钻。理解这条链,无论是排查hg status的 ignore 过滤、编写edenfsctl glob模式,还是评估大规模仓库下的 glob 预取性能,都能事半功倍。

关键源码路径速查

  • 接口定义:eden.thrift(GlobParams/Glob)、eden.thrift(globFiles)
  • 服务端实现:ThriftGlobImpl.cpp(GlobTree/GlobNode 双路径、searchRoot、suppressFileList、预取分批)
  • 编译与匹配:GlobNodeImpl.h / GlobNodeImpl.cpp(token 化、children_/recursiveChildren_、hasSpecials_ 优化)
  • 匹配器:GlobMatcher.h(gitignore 兼容、CASE_INSENSITIVE/IGNORE_DOTFILES、回溯保护)
  • Ignore 文件:GitIgnore.h(EXCLUDE/INCLUDE/NO_MATCH/HIDDEN 与优先级栈)
  • 工作副本 inode 求值:GlobNode.cpp / GlobNode.h
  • 存储层 tree 求值:GlobTree.cpp
  • 结果结构:GlobResult.h
  • 测试:GlobNodeTest.cpp
  • CLI:glob.rs、common.rs、prefetch.py
  • 开发工具
  • CLI
  • 后端

【免费下载链接】sapling

A Scalable, User-Friendly Source Control System.

项目地址:https://gitcode.com/gh_mirrors/sa/sapling
点击查看免费下载

相关推荐

上一篇:JavaCV开源生态:相关库与工具集成指南
下一篇:dnSpyEx终极调试器插件架构:揭秘.NET逆向工程的完整可扩展方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询