开发工具CLI后端【免费下载链接】saplingA Scalable, User-Friendly Source Control System.项目地址https://gitcode.com/gh_mirrors/sa/sapling点击查看免费下载本文基于 eden/fs/docs/slides/Checkout.md 这一内部技术简报展开深入剖析 SaplingEdenFS中hg update切换工作副本Checkout的完整实现三种 Checkout 模式的语义差异、只做必要工作的最小化算法、向操作系统内核FUSE / NFS / ProjectedFS发送缓存失效通知的三种后端实现以及TreeInode从对比 Tree、调度CheckoutAction到更新 Overlay 的完整调用链。读完本文你将理解 EdenFS 如何在不扫描全目录树的前提下完成一次快速且一致的版本切换并掌握关键源码位置以便深入阅读。文档背景与阅读指引Checkout.md是 EdenFS 团队 2023 年 8 月的一份 Marp 幻灯片风格内部文档提纲挈领地概括了 Checkout 的核心设计。本仓库中对应的真实实现散落在 eden/fs/inodes/TreeInode.cpp、eden/fs/inodes/CheckoutAction.h、eden/fs/inodes/CheckoutContext.h 等文件中本文将以该文档为骨架、以源码为佐证逐层还原其技术全貌。Checkout 的三种模式EdenFS 将一次 checkout 划分为三种模式由CheckoutMode枚举表示并通过 CheckoutContext 在整次操作中传递模式触发时机语义DRY_RUNhg update的第一步先于NORMAL仅探测冲突把结果汇报给 Mercurial不实际修改 inode 内容NORMAL第一次DRY_RUN之后执行真正的更新遇到冲突时保留本地内容并报告FORCEhg update -C冲突时无条件采用目标提交destination commit的文件内容在源码层面这三个语义体现在 CheckoutContext.h 的两个查询方法上isDryRun()返回checkoutMode_ CheckoutMode::DRY_RUN表示本次操作只查找冲突、不更新 inode 内容forceUpdate()返回checkoutMode_ CheckoutMode::FORCE且文档明确注释forceUpdate()只有在isDryRun()为 false 时才能为 true。进一步看 EdenMount.hEdenFS 对正在进行的 checkout 也有并发约束NoOngoingCheckout表示当前没有NORMAL或FORCE类型的 checkout 在进行此时可能有一个DRY_RUN在跑而CheckoutInProgress表示任意类型DRY_RUN、NORMAL或FORCE的 checkout 正在进行。此外 EdenMount.h 还对比了两种模式对对象保留的影响NORMAL模式下新 tree 或 blob 会被增量地加入 EdenFS 对象存储FORCE模式下操作完成后只保留新 tree旧的孤儿对象会被清理。算法核心只做最小必要工作文档用Gist of the algorithm一节概括了 Checkout 的性能秘诀只同步工作副本与目标提交之间真正需要同步的最小集合这意味着不会递归进入操作系统尚未感知的目录未加载、未被访问过的子树无需展开也不会递归进入工作副本与目标提交完全相同的目录——如果某个子目录在两棵 Tree 中指向同一个对象 ID整棵子树可以直接跳过连内容都不必比较。体现在代码上TreeInode::checkout对每个目录调用 computeCheckoutActions 生成动作列表它持有contents_锁对旧 Tree 与新 Tree 的目录项做归并比较双指针迭代只对新增、删除、两侧都存在但对象不同的条目生成CheckoutAction完全相同对象 ID 一致的条目不会产生任何动作因而也不会递归下去。同时文档强调凡是内容需要更新的文件/目录EdenFS 都会通知操作系统使其缓存失效——这正是下一节的核心。缓存失效Invalidation的两大原语EdenFS 将让内核忘记旧内容抽象成两个基本操作实现在 TreeInode.cppinvalidateChannelEntryCache(name, ino)通知 OS 目录下名为name的条目文件或子目录项发生了变化。它尤其必须被用于新文件以确保 OS 丢弃自己的 negative path cache即该路径不存在的缓存条目否则新建文件后内核可能仍然返回ENOENT。invalidateChannelDirCache()通知 OS整个目录的内容发生了变化。特别是当目录中新增或删除了文件目录项集合本身变了时必须调用它否则 readdir 的结果仍是旧的。两个原语共同作用条目级失效解决这个文件的内容变了目录级失效解决这个目录里有哪些条目变了。有趣的是在 PrjfsDispatcherImpl.cpp 的注释中可以看到invalidateChannelEntryCache会与 inode 的fsRefcount维护产生交互——失效时持有 contents 锁这保证了后续对同一 inode 的引用计数递增一定发生在其递减之后不存在竞态窗口。此外TreeInode.cpp 中两个失效原语都接入了 EdenFS 的**故障注入FaultInjector**框架可分别以invalidateChannelEntryCache为 fault 名称注入错误用于测试失效应答失败时的路径这与 InvalidationRequired 这个返回值枚举No/Yes配合使用调用方根据返回值决定是否必须自行执行失效。需要说明的是这两个原语是平台无关的统一抽象但落到每个具体文件系统后端时实现方式截然不同。ProjectedFSWindows上的失效Windows 上的 EdenFS 通过 ProjectedFSprjfs通道工作PrjfsChannel.cpp 中体现了文档描述的两条实现路径invalidateChannelEntryCache→ 调用PrjDeleteFile见 PrjfsChannel.cpp把磁盘上的占位文件placeholder或完整文件删除。此后对该文件的下一次请求会重新触发 EdenFS 的 lookup由 EdenFS 在磁盘上重建占位文件。调用时传入PRJ_UPDATE_ALLOW_DIRTY_METADATA | PRJ_UPDATE_ALLOW_DIRTY_DATA | PRJ_UPDATE_ALLOW_READ_ONLY | PRJ_UPDATE_ALLOW_TOMBSTONE等标志允许删除脏元数据、脏数据、只读文件并允许留下 tombstone墓碑标记。其中ERROR_REPARSE_POINT_ENCOUNTERED目标是目录、ERROR_FILE_NOT_FOUND/ERROR_PATH_NOT_FOUND目标未缓存等返回码会被静默忽略。invalidateChannelDirCache→ 调用PrjMarkDirectoryAsPlaceholder见 PrjfsChannel.cpp 与 L1721-L1722强制该目录的 listing 始终由 EdenFS 提供。文档强调目录被完整处理之后总是会调用一次。如果目录已经不在目标提交中PrjMarkDirectoryAsPlaceholder可能触发递归查找而失败EdenFS 也将其视为无害并忽略。值得注意的是Windows 上目录级失效可能阻塞要与 ProjectedFS 交互因此 TreeInode.cpp 将其调度到专门的InvalidationThreadPool线程池中异步执行避免拖垮 server 线程池。FUSELinux/macOS 主通道上的失效文档指出FUSE 是 EdenFS 最早的 FsChannel失效逻辑天然围绕 FUSE 语义构建EdenFS 的失效原语与 FUSE 失效 opcode 是 1:1 对应invalidateChannelEntryCache→ 向内核发送FUSE_NOTIFY_INVAL_ENTRYinvalidateChannelDirCache→ 向内核发送FUSE_NOTIFY_INVAL_INODE这两条消息的构造可以在 FuseChannel.cpp 中看到二者都运行在专门的失效线程中。特别值得注意的是文档强调FUSE 的失效通知是异步发送的。另外 TreeInode.cpp 中有一段精辟注释FUSE_NOTIFY_INVAL_ENTRY适用于条目被删除或修改的场景但当新条目被加入目录时inode 本身也必须被失效即还要发FUSE_NOTIFY_INVAL_INODE否则 readdir 缓存不会刷新——这也印证了新增文件时必须同时做两种失效的设计。NFSmacOS 备选通道上的失效NFS 是三者中最特殊的一个因为NFS 协议没有原生的告诉内核丢缓存机制。EdenFS 只能依靠 NFS 客户端观察文件/目录的mtime来决定是否失效自身缓存因此失效策略围绕mtime做文章。与 FUSE 一样NFS 失效也是异步发送的invalidateChannelEntryCache→什么也不做。代码依赖父目录被失效时携带更新的mtime客户端看到 mtime 变化后自然刷新整个目录缓存见 TreeInode.cpp 的注释For NFS, the entry cache is flushed when the directory mtime is changed. Directly invalidating an entry is not possible.。invalidateChannelDirCache→ 使用chmod(mode)强制发送一次无操作的SETATTR给 EdenFS借SETATTR更新目录 mtime。文档还揭示了这段历史早期 EdenFS 只是简单地打开再关闭文件利用 NFS 的close-to-open一致性来间接刷新缓存但macOS 不遵守 close-to-open 语义因此不得不改为显式chmod这一招。这一实现对应 TreeInode.cpp 的nfsInvalidateDirCacheLocked通过 NFSD channel 的invalidate()发送目标路径与 mode而 NfsDispatcher.h 中SetattrRes::noop字段的注释也确认了这一点——EdenFS 为失效 NFS 客户端目录缓存而发出的 chmod正是这种只请求文件已有属性的请求它不改变任何东西。三种后端失效方式对比后端invalidateChannelEntryCacheinvalidateChannelDirCache是否异步ProjectedFSPrjDeleteFile删除占位/完整文件保留 tombstonePrjMarkDirectoryAsPlaceholder目录 listing 改由 EdenFS 提供目录级失效调度到专用线程池FUSEFUSE_NOTIFY_INVAL_ENTRYFUSE_NOTIFY_INVAL_INODE异步NFS无操作依赖父目录 mtime 刷新chmod触发无操作SETATTR更新 mtime异步核心执行流程从 TreeInode 到 CheckoutAction文档用四个关键函数勾勒出单目录 checkout 的执行骨架全部落在 TreeInode.cpp 中TreeInode::checkout目录级入口签名定义在 TreeInode.h协程版本为 co_checkout。它接收fromTree当前检出的 Tree可能为空与toTree目标 Tree可能为空——表示该路径在目标提交中已删除。在 TreeInode.cpp 的实现中它会先做故障注入检查fault 名TreeInode::checkout与取消检查调用computeCheckoutActions比较新旧 Tree生成动作列表并发执行所有CheckoutAction全部完成后对该目录运行失效并更新 overlay。TreeInode::computeCheckoutActions在持有contents_锁的窗口内TreeInode.cpp对旧、新 Tree 的目录项做归并比较为每个需要更新的条目调用processCheckoutEntry。它还会根据挂载配置的getCaseSensitive()决定大小写不敏感平台上的比较行为TreeInode.cpp。TreeInode::processCheckoutEntry模板实现见processCheckoutEntryImpl同样运行在contents_锁内由 TreeInode.cpp 及 L5209 的processCheckoutEntryImpl实现。文档说它立即处理新增与删除把冲突检查延迟到CheckoutAction——如果某条目只出现在新 Tree 中纯新增直接在锁内创建只有涉及旧条目需要被替换/修改时才生成CheckoutAction交给异步阶段。对 CheckoutAction.h 的注释也印证了这一点新增文件/目录这类可以立即完成的工作在持有锁时直接做掉根本不会创建 CheckoutAction 对象。CheckoutAction动作执行器文档称其为简化Blobsha1 与Tree加载的包装类。核心要点见 CheckoutAction.h 的成员设计旧侧只下载旧 Blob 的 sha1因为只需要与本地内容比对但旧 Tree 需要下载完整树新侧根本不需要新 Blob 的数据只用一个newBlobMarker_布尔标记记录目标是一个 blob新旧 Tree 则都加载完整树子目录需要递归。加载完成后CheckoutAction::co_runCheckoutAction.cpp先做hasConflict()冲突判定再调用doAction()。冲突判定依赖 CheckoutAction.h 中的checkSyncConflict/classifyFileContentConflict/classifyFileDestinationConflict等分类函数必要时异步调用FileInode::isSameAs比对文件内容。关键逻辑在 CheckoutAction.cpp即使确定不会应用变更也必须先跑hasConflict()因为要借其副作用把冲突记录进 CheckoutContext而若conflictWasAddedToCtx !forceUpdate()则跳过应用并把该 inode 的全部内存后代计入已完成计数。TreeInode::checkoutUpdateEntry锁内落定在 Blob 与 Tree 都已加载后调用定义在 TreeInode.h实现在 TreeInode.cpp。它会重新获取contents_锁并重新校验确保自checkout释放锁以来没有出现新的冲突进行就地in place修改对于目录条目递归调用TreeInode::checkout下沉处理子树。返回的 CheckoutActionResult 携带invalidationRequired是否要求调用方做内核失效、hadConflicts与localOnlyRemains三个字段目录级结果CheckoutSubtreeResultCheckoutAction.h则记录整棵子树是否有冲突、以及 DRY_RUN 下是否有仅本地存在的条目会阻碍目录被文件替换。由此可以还原整条调用链的时序hg update └─ CheckoutContext携带 CheckoutMode └─ TreeInode::checkout(ctx, fromTree, toTree) // 目录入口 ├─ computeCheckoutActions() // 锁内归并比较新旧 Tree │ └─ processCheckoutEntry() // 立即处理新增/删除其余生成 CheckoutAction ├─ 并发执行所有 CheckoutAction // 加载 Tree / Blob sha1、判定冲突 │ └─ checkoutUpdateEntry() // 重新加锁、校验、就地修改目录则递归 checkout ├─ invalidateChannelEntryCache / DirCache // 按后端发内核失效 └─ saveOverlayPostCheckout() // 更新 overlay 并传播到父目录Overlay 更新与写入放大Checkout 的最后一步是让 EdenFS 的 overlay本地状态层与目标状态对齐在TreeInode::checkout处理完所有CheckoutAction之后调用 saveOverlayPostCheckout 把该目录的 overlay 更新到目标状态。由于该TreeInode的 overlay 被更新必须在父目录的 overlay 中记录这一点这会强制父目录被物化materialize并写盘且该过程可能递归向上传播因为父目录的 overlay 也变了还要再改祖父目录……。因此文档给出了一个重要的复杂度结论一次 checkout 中某个目录的 overlay 写入次数理论上可达O(子目录数量)——每次子目录状态变化都会连带触发一次父目录的 overlay 重写。这也是为什么尽力避免无谓的 overlay 写入如 DRY_RUN 直接跳过是性能关键之一。源码细节saveOverlayPostCheckout 开头的第一个判断就是if (ctx-isDryRun())直接 return——DRY_RUN 绝不更新父目录、绝不做任何不必要的 overlay 写入。而在非 DRY_RUN 路径上TreeInode.cpp 显示即使子项被去物化目录自身也会被标记物化以保证记录正确的源控制对象 ID去物化只发生在 checkout 流程中后续saveOverlayPostCheckout会再检查是否可以把自身去物化。更新完成后TreeInode.cpp 以DBG4日志记录本次目录更新的完成情况与错误数便于用eden的 debug 日志追踪。从源码延伸出的边界行为与常见问题原文档结尾的 QA 部分是留白的讨论环节这里结合源码补上几个可直接验证的边界行为供读者对照为什么新增文件要同时做条目级与目录级两种失效因为 OS 可能缓存了该名字不存在的负面路径缓存negative path cache只发FUSE_NOTIFY_INVAL_INODE不会清除对单个名字的负面缓存。代码中所有新增路径如 TreeInode.cpp 与 L2457-L2460都是先invalidateChannelEntryCache再invalidateChannelDirCache注释明确写道Make sure that the directory cache is invalidated so a subsequent readdir will see the added file.冲突时 FORCE 与 NORMAL 的分野在哪在CheckoutAction::doAction中冲突被记录到 CheckoutContext 后只有forceUpdate()为真时才会继续应用目标内容否则跳过该条目的更新并保留本地内容CheckoutAction.cpp。这也是文档所说FORCE 在冲突时总是优先目标提交文件内容的落点。失效失败怎么办ProjectedFS 上失效可能失败因此 TreeInode.cpp 在更新目录项时先做失效、再做内容更新失效异常会以EIO返回给调用方避免内容已改但内核缓存还是旧的这一不一致状态。FUSE 路径则全部异步由失效线程统一发送。checkout 的进度与取消CheckoutContext 携带checkoutProgress共享的原子计数供hg update进度条使用和CancellationTokenCheckoutAction::co_run与TreeInode::co_checkout都会通过ctx-throwIfCanceled()检查取消请求每个目录还会创建TreeInode::checkout/computeCheckoutActions/saveOverlayPostCheckout等 trace span见 TreeInode.cpp 与 L4863可用于性能剖析。性能基准仓库中还有专门的基准测试 CheckoutBenchmark.cpp覆盖 checkout 关键路径的吞吐评估可作为阅读实现时的配套参考。另外 InodeGarbageCollection.md 也提及了失效原语与 inode GC 之间的配合关系值得一并阅读。小结一次看似简单的hg update在 EdenFS 内部是一次对比 Tree → 并行加载对象 → 判定冲突 → 就地修改 → 分后端失效内核缓存 → 回写 overlay的精密协作。理解 DRY_RUN / NORMAL / FORCE 三种模式各自的分工理解invalidateChannelEntryCache与invalidateChannelDirCache在 FUSE、NFS、ProjectedFS 上的三种落地方式以及TreeInode::checkout到CheckoutAction再到checkoutUpdateEntry的调用链是深入 EdenFS 乃至 Sapling 整体架构的关键一步——本文所引用的源码均为当前仓库中的真实实现读者可以顺着链接逐行验证。赞分享开发工具CLI后端【免费下载链接】saplingA Scalable, User-Friendly Source Control System.项目地址https://gitcode.com/gh_mirrors/sa/sapling点击查看免费下载相关推荐komorebi 边框样式详解system / rounded / square 三种模式与底层绘制原理komorebi 边框样式详解system / rounded / square 三种模式与底层绘制原理 导读 border style 是 komore桌面应用EdenFS CASC 缓存流程解析从层级缓存到内容寻址本地缓存EdenFS CASC 缓存流程解析从层级缓存到内容寻址本地缓存 EdenFSSapling 项目的虚拟文件系统层的传统缓存流程遵循层级结构各层缓存按顺开发工具CLI后端Continue CLI 模式Modes完全指南normal / plan / auto 三种权限模式的原理与实战Continue CLI 模式Modes完全指南normal / plan / auto 三种权限模式的原理与实战 本篇技术指南聚焦 Continue 开人工智能AI Agent代码智能体开发工具工具调用RAG上一篇如何使用oapi-codegen生成AWS Lambda函数代码Serverless开发的终极指南下一篇终极指南掌握 Mustermann - Ruby 字符串模式匹配专家 创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考