- 开发工具
- CLI
- 后端
【免费下载链接】sapling
A Scalable, User-Friendly Source Control System.
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完成:
- 路径所在目录的
.gitignore; - 逐级向上直到仓库根目录的每一层
.gitignore; - Eden 客户端全局 exclude 文件(
systemIgnoreFile); - 用户个人 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)字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
mountPoint | PathString | 挂载点路径 |
globs | list<string> | 要执行的 glob 模式列表 |
includeDotfiles | bool | 是否把以.开头的条目纳入匹配 |
prefetchFiles | bool | 若为 true,同时预取匹配文件的 blob |
suppressFileList | bool | 若为 true,不在Glob结果里填充 matchingFiles(通常与 prefetchFiles 配合) |
wantDtype | bool | 是否返回每个匹配条目的 dtype |
revisions | list<ThriftRootId> | 要针对哪些提交(revision)求值;为空则只对当前 checkout revision 求值,列表中不应有重复项 |
prefetchMetadata | bool | 已无实际效果(保留字段) |
searchRoot | PathString | glob 从哪个目录开始求值,默认是仓库根目录 |
background | bool | 若为 true,预取在后台执行、不等结果 |
predictiveGlob | optional PredictiveFetch | 已废弃、被守护进程忽略(仅保留 wire 兼容) |
listOnlyFiles | bool | 只返回文件、过滤目录(注意:为 false 时 matchingFiles 并不等同于实际喂给 backing store 的预取列表) |
sync | SyncBehavior | 本次 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)相关的行为
原文档对符号链接行为的规定如下:
- 模式精确命中符号链接本身:
globFile返回该符号链接自身(而不是它的 target)作为匹配结果。 - 模式的前缀命中符号链接:
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.
相关推荐
xonsh 文件通配(Globbing)完全指南:普通通配、正则通配与 Match 通配实战
xonsh 文件通配(Globbing)完全指南:普通通配、正则通配与 Match 通配实战 本文是 xonsh(Python powered shell)官方
开发工具ruoyi-ai 完整上手指南:从零搭建 AI 对话平台到第一次流式对话
ruoyi ai 完整上手指南:从零搭建 AI 对话平台到第一次流式对话 自己搭一个 AI 对话平台,最耗时间的从来不是"接个模型",而是模型计费、流式推送、用
后端AI 应用大模型RAGApache Flink 文件系统通用配置指南:默认文件系统与连接限制机制详解
Apache Flink 文件系统通用配置指南:默认文件系统与连接限制机制详解 本文是 Apache Flink 文件系统通用配置(Common Configu
后端大数据流处理批处理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考