news 2026/9/25 7:15:00

Lore 修改文件跟踪(Dirty Tracking)设计解析:从 Merkle 树标志位到 `lore status --scan` 的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Lore 修改文件跟踪(Dirty Tracking)设计解析:从 Merkle 树标志位到 `lore status --scan` 的完整实现
  • 版本控制
  • 后端

【免费下载链接】lore

Lore is a next-generation, open source version control system

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

导读:本文以 Lore 官方增强提案《Modified File Tracking》(docs/proposals/2026-05-03-modified-file-tracking.md)为主体,讲解 Lore 如何在不做全量文件系统扫描的前提下,把"磁盘上有哪些文件被修改"作为一等仓库概念进行跟踪。你将理解 Merkle 树节点上新增的Dirty标志位如何定义与传播、lore file dirty如何为 IDE/VFS/文件系统监视器提供轻量上报 API、lore status如何默认展示脏文件、以及--scan如何在按需扫描时持久化结果——全部附当前仓库源码证据。

1. 背景与动机:为什么lore status需要一次"改造"

Lore 是一个下一代开源版本控制系统(见 README.md)。在引入修改文件跟踪之前,Lore 只有两种方式获知文件系统变更:

  • lore status默认只显示已暂存(staged)的变更,对本地修改"视而不见";
  • lore status --unstaged会触发一次全递归扫描,逐文件对比内容哈希与已提交修订版本。

对于大型仓库,后者耗时数秒到数分钟,根本无法在日常交互中使用(提案 Motivation 原文)。这带来三个问题:

  1. 状态默认是"盲"的——用户看不到未暂存的本地修改,除非显式跑一次昂贵的扫描;
  2. 没有通知通道——IDE、文件系统监视器、构建系统、VFS 提供方明明通过操作系统事件知道哪些文件变了,却没有途径告诉 Lore,只能要么过度提交(全部 stage),要么漏报(等用户扫描);
  3. 跟踪逻辑重复——每个需要修改状态的应用程序都要各自实现一套跟踪,行为发散、边界情况各异、无法与 Lore 共享状态。

更深层的原因是--unstaged把"扫描文件系统(昂贵 I/O)"和"展示已知变更(廉价状态读取)"这两件事混在了同一个标志里。提案的核心洞察是:把变更的检测(detection)与变更的跟踪(tracking)分离。检测是集成方的责任(它们本来就通过 OS 事件知道变化),而跟踪是 Lore 的责任(持久化状态、树传播、与暂存/提交等操作交互),所有消费者从唯一权威来源读取。

2. 目标与非目标

Goals(提案目标)

  1. 把修改文件跟踪作为一等仓库概念:Lore 无需扫描即可知道磁盘上有哪些文件被修改,且该知识跨操作持久存在;
  2. 让外部集成能够上报修改:通过轻量 API(无需暂存)告知 Lore 文件变化;
  3. 默认展示修改:lore status默认显示本地修改文件,无需标志或全量爬取;
  4. 扫描与展示分离:按需扫描的结果被持久化,后续 status 调用能即时展示已知变更;
  5. 修改状态贯穿暂存/提交生命周期:暂存不清除修改跟踪,只有提交才清除;未纳入提交的文件保留修改状态。

Non-Goals(明确不做)

  • 不做内容级变更跟踪:只跟踪"哪些文件变了",不跟踪"文件内部变了什么";
  • 不做变更检测:本提案只提供跟踪设施,文件系统监视、VFS 集成、IDE 变更检测都是集成方的事,dirty 跟踪 API 只是它们的公共目标;
  • 不做按生命周期状态拆分动作:Dirty 与 Staged 在每个节点上共享同一动作类型,不支持"staged 为 modify 而 dirty 为 delete"这种独立动作组合。

3. 核心设计:Merkle 树节点上的Dirty标志位

提案把修改跟踪作为仓库状态中的一等公民。数据结构的落点是 Merkle 树节点上新增的一个正交标志位。

3.1 位布局:V1 的 bit 3 与 V2 的 bit 15

NodeFlags(V1,u16)此前 bit 3 名为Unused1,NodeFlagsV2(V2,u32)此前 bit 15 未定义。提案将它们分别赋予Dirty语义。这在当前源码中已经完整落地,见 node.rs:

// V1 (u16) const File = 0b1; const Link = 0b10; const ExternalNameDeprecated= 0b100; /// Node has been modified on the filesystem (dirty tracking) const Dirty = 0b1000; // bit 3,原 Unused1 /// Node is staged for commit const Staged = 0b10000; // bit 4 // ... /// Action bits shared between Dirty and Staged (Modify, Add, Delete, Move, Copy) const ActionBits = 0b1111100000; // bits 5-9 // ... const DirtyModify = 0b101000; // Dirty + Modify const DirtyAdd = 0b1001000; // Dirty + Add const DirtyDelete = 0b10001000; // Dirty + Delete const DirtyMove = 0b100001000; // Dirty + Move const DirtyCopy = 0b1000001000;// Dirty + Copy /// All dirty flags (Dirty base + ActionBits) const DirtyBits = 0b1111101000;

V2 侧对应定义(node.rs):

// V2 (u32) /// Node has been modified on the filesystem (dirty tracking) const Dirty = 0b1000000000000000; // bit 15,原未定义

关键语义:

  • Dirty 与 Staged 正交:Dirty(bit 3)位于StagedBits(V1 为0b111111111110000,即 bits 4-14)之外,二者互不干扰;
  • 共享动作位:Dirty 与 Staged 共用 bits 5-9 的 Modify/Add/Delete/Move/Copy 动作位。一个节点在任何时刻只有一个动作类型,无论它处于哪个生命周期状态。于是变更节点有且仅有四种合法组合:dirty only、staged only、dirty+staged、clean;
  • 源码中有专门测试断言这些约束(node.rs):
    assert_eq!(NodeFlags::Dirty.bits(), 0b1000); assert_eq!(NodeFlags::Dirty.bits() & NodeFlags::Staged.bits(), 0); assert_eq!(NodeFlagsV2::Dirty.bits(), 1 << 15);

V2 转 V1 时,Dirty按直接位映射处理,与File、Link、Discarded的既有转换模式一致(见 node.rs 附近的 V2→V1 转换逻辑)。

3.2 传播:node_mark_dirty()与 early-out 优化

Dirty 标志沿 Merkle 树向上传播到父目录,采用与既有 staged 传播(node_mark)相同的早退优化:父节点已经 dirty 就停止向上。核心原语是State::node_mark_dirty(state.rs):

  • 目标节点被标记为给定的 dirty 标志(含动作位);
  • 父目录只获得基础 Dirty 位(bit 3,无动作位)——因为父目录的"脏"只是表示"其下存在脏子节点",不需要动作语义;
  • 当mark_dirty为 false 且节点已具备目标标志时提前返回(early-out);
  • 设置新 dirty 位前先清除既有DirtyBits,即"最新动作胜出(latest wins)",同时保留 Staged 与 Merge 位;
  • 一路向上直到根块,根块也被标记为脏。

配套的查询原语State::node_has_dirty_children(state.rs)遍历某父节点的子节点,判断是否存在带 Dirty 标志的后代——这是父目录脏标志清理、以及"跳过无脏子树的子树遍历"的关键。

4. 轻量上报 API:lore file dirty命令族

外部进程(IDE、文件系统监视器、VFS 提供方、构建系统)通过新的lore file dirty命令把"哪些文件变了"推给 Lore,而无需暂存、无需读取内容。命令族如下(提案原文 + file.rs 中的 CLI 定义):

命令语义
lore file dirty <paths>按文件系统存在性(不校验内容)分类动作:文件存在且在当前修订中 → modify;文件存在但不在修订中 → add;文件缺失但在修订中 → delete;文件缺失且不在修订中 → 忽略。目录则递归其子节点
lore file dirty move <from> <to>标记文件为"移动",不做文件系统验证(调用方可信)
lore file dirty copy <from> <to>标记文件为"复制",不做文件系统验证
lore file dirty --targets <file>从 targets 文件批量标记

CLI 参数结构在 file.rs 中可见:FileDirtyArgs扁平化两个参数组——路径组(paths可变数量 +--targets批量文件)与子命令组(Move/Copy),move/copy各自携带from/to两个位置参数。注意路径组在顶层并不强制(因为 move/copy 有各自的路径参数),其必填性在 handler 层当未使用子命令时校验。

dirty 状态持久化在既有的 staged anchor中——不引入新的 anchor 类型,因此也不引入新的序列化格式。API 尊重与 stage API 相同的 ignore 与 view 过滤器(实现中可见对FilterMode/FilterStates、is_path_under_layer_mask/route_layer_paths的复用,见 dirty.rs)。

同时提供顶层快捷键lore dirty,镜像既有lore stage快捷键。dirty.rs 的实现注释也印证了其设计意图:"Detected changes are marked dirty and staged in a single pass"、"lore dirty <paths>,或运行lore status --scan以在整棵树范围内对账 dirty 标志"(见 file.rs)。

典型集成场景(VFS 提供方):VFS 提供方从内核拦截 write/rename/delete 回调,把每个回调翻译成一次对应的file dirty调用,就能在零文件系统扫描的情况下维护精确的 dirty 状态。同样的 API 服务于 IDE 集成、构建系统以及任何能观察到文件变化的过程——这正是提案 Goal 2 的落点。

5. 状态展示:lore status默认显示脏文件

lore status的输出结构变化:

  • 新增"Dirty files (not staged for commit)"区块,位于 staged 区块与(现已降级的)unstaged 区块之间;
  • dirty+staged的文件仍出现在 staged 区块;
  • 程序化输出(C struct 与 JSON 事件)新增flag_dirty字段。

FFI 侧,新字段flag_dirty追加到lore_repository_status_file_event_data_tC 结构体的flag_conflict_theirs之后,保持既有字段的布局兼容。源码中对应字段为pub flag_dirty: u8,由change.flags.is_dirty().into()填充(见 status.rs)。迁移提示:此前通过flag_staged == false识别 unstaged 文件的消费者,应改为检查flag_dirty == true。

为了让 dirty 文件在驱动状态展示的 diff 输出中浮现,diff 引擎diff_subtree_node()被扩展:即使节点的内容哈希与比较状态一致,只要带 Dirty 标志也要在 diff 中报出(提案 Status display 一节)。从源码看,状态输出引擎同时支持"基于状态树的 diff"与"文件系统扫描 diff"两种来源,并引入show_scan标志来区分哪些变更应由状态 diff 报出、哪些留给扫描去重检测(见 status.rs 中reported_by_state_diff的注释:"A change that is neither staged nor dirty is not a working-tree change. One a scan will re-detect from the filesystem, settling its flags inline, is left to the scan")。

6. 按需扫描:lore status --scan与 scan_dirty 模式

--unstaged的另一个问题是被"降级"了:

  • 新增lore status --scan:触发一次文件系统爬取,复用既有diff_filesystem()基础设施;
  • 扫描比较文件系统与staged 状态(因为 stage 与 dirty 标记都不会更新内容,staged 状态的内容哈希就等于已提交修订的哈希);
  • --unstaged变为--scan 的隐藏别名;内部StatusOptions.unstaged字段更名为scan(源码中可见pub scan: bool及配套的--check-dirty选项,见 status.rs)。

diff_filesystem遍历器被扩展出scan_dirty 模式(核心实现位于 os_diff.rs,入口为diff_filesystem_subtree_impl/diff_filesystem_directory等一组函数):

  • 遍历过程中内联地对修改过的文件设置 Dirty、对保留(匹配)的文件清除过期 Dirty,零额外树遍历开销;
  • 父目录清理(清除"已无任何脏子节点"目录上的 Dirty)在 retain 点内联完成;
  • 状态 diff 与文件系统扫描可以并行执行,并通过任务计数器折叠出按动作分组的汇总事件(status.rs 注释:"Accumulates per-action dirty counts across the parallel staged/scan walks; emitted as a single summary event for--scan/--check-dirty")。

从并发实现看,diff 遍历还受信号量约束(diff_filesystem_task_semaphore,os_diff.rs),避免大型仓库扫描时任务数量失控。

7. 操作交互:Dirty 状态如何在生命周期中存活

提案明确了 dirty 标志与各项既有操作的关系(Operation interactions 一节),这也是"贯穿暂存/提交生命周期"目标(Goal 5)的落地规则:

操作对 Dirty 的影响
Stage添加 Staged 标志,不清除 Dirty;清标志操作在 Dirty 已设置时保留 Dirty 与动作位
Unstage清除 Staged 后重新检查文件系统:文件与已提交修订一致则清除 Dirty,仍不一致则保留 Dirty;父目录向上清理(无脏子节点则清脏);若 unstage 后仍有 dirty-only 节点,则保留 staged anchor
Reset对 dirty-only 文件:恢复文件内容并清除 Dirty(含父目录清理);对 staged 或 dirty+staged 文件:继续拒绝(行为不变)
Commit提交的文件同时清除 Dirty 与 Staged;dirty-only 文件跨提交保留。提交后若仍有 dirty-only 节点,staged 状态被重新挂到新修订并持久化为新 staged anchor;若无脏节点,anchor 被删除
Sync不删除 staged anchor,Dirty 标志跨 sync 存活到新修订
Branch switch普通切换保留 staged anchor(含 dirty 标志);强制切换(--reset)删除 staged anchor,清空全部 dirty 状态
Merge / cherry-pick / revert通过加法式标志操作保留既有标志,Dirty 在这些操作后存活

源码中State::node_mark_dirty的"清除 DirtyBits 但保留 Staged/Merge 位"行为、以及State上配套的清 dirty 原语(node_has_dirty_children的反向操作,见 state.rs 注释)正是这些交互的底层支撑。相关调用点遍布 stage.rs、file/reset.rs、file/unstage.rs 与 commit.rs。

8. 兼容性、安全与迁移

兼容性

  • Wire 格式:不涉及。Dirty 标志只存在于本地 staged anchor,从不传输给远端对端;
  • 客户端/服务端协议:不涉及。file dirty与--scan都是本地操作,无新增 RPC;
  • 磁盘格式:Dirty 占 V1 bit 3(原Unused1)与 V2 bit 15(原未定义),两处都是保留未用位。旧客户端读取新客户端写入的 staged 状态时,会把 dirty 位当作 flags 字段中的未知位;由于旧客户端不解释 bit 3、且该位在StagedBits掩码(bits 4-14)之外,不会影响 staged 标志的解析;又因为节点 flags 字段作为不透明整数读写,dirty 位会在旧客户端的读写周期中被静默保留;
  • CLI 与公开 API:新增lore file dirty、lore dirty(快捷键)与lore status --scan;--unstaged保留为隐藏别名;flag_dirty追加在 C 结构体末尾,保持既有字段布局。

安全与隐私

  • 安全:Dirty 标志是仅本地元数据,存于 staged anchor,从不传输、不进提交修订;file dirty不读文件内容,只检查存在性以分类动作;信任模型不变(本地用户对自己的仓库状态拥有完整访问权);
  • 隐私:Dirty 记录的是文件路径(本就属于仓库树)与动作类型(modify/add/delete/move/copy),无新增用户标识或元数据;dirty 状态仅本地,永不离开客户端机器。

迁移

无破坏性变更,无需迁移。--unstaged作为隐藏别名保留;flag_dirty追加到结构体末尾不破坏既有字段。

9. 非功能属性:并发、内存、确定性与风险

并发模型

file dirty操作作用于 staged anchor——这是一个单写者资源(与stage/unstage相同的并发模型)。多进程并发 dirty 通知在没有外部协调的情况下不安全,这与既有暂存操作的模型一致(Non-Functional Considerations 原文)。

内存与性能

  • Dirty 是既有 Merkle 树节点上的单个位,不分配任何新数据结构;
  • scan_dirty模式在遍历中内联设置/清除标志,不额外缓冲;
  • collect_dirty_file_nodes()辅助函数只遍历脏子树(不带 Dirty 标志的目录直接跳过),保证查询效率;
  • 状态无进程内存驻留:dirty 状态持久化在 staged anchor(磁盘文件),跨操作只经过标准的反序列化/序列化周期;
  • 确定性:相同的文件系统状态产生相同的 dirty 标志;file dirty对相同输入(路径 + 存在性)确定;--scan对相同文件系统状态与已提交修订确定。

已知风险与应对

  1. 共享动作位导致动作覆盖:stage 一个 modify 后再file dirty标记为 delete,会把 staged 动作改成 delete。这是文档化的 "latest action wins" 语义——动作反映当前文件系统现实而非历史意图(用户 stage 后又删除了文件,理应看到删除动作);
  2. 扫描中断的原子性:scan_dirty会在读导向操作(lore status)中修改 staged 状态。缓解:staged anchor 在完整扫描完成后原子更新,扫描中途崩溃时旧 anchor 保持完整;file dirty的即时持久化产生的部分 dirty 状态,也不比一次被中断的lore stage更糟;
  3. 存在性检查的竞态窗口:file dirty通过文件系统存在性分类动作,存在"调用方检测与 Lore 存在性检查之间文件快速变化"的竞态。move/copy子命令通过完全信任调用方来规避这一点。

代价(Drawbacks)

  • Dirty 与 Staged 共享动作位,无法同时表示两个生命周期状态的不同动作(如"staged 为 modify,dirty 为 delete");
  • file dirty的一次同步存在性检查引入了轻微系统调用开销与竞态窗口;
  • staged anchor 随 dirty 跟踪文件数增长(dirty 状态与 staged 状态共处同一 Merkle 树),但被仓库文件总数所界定,且沿用 staged 状态同一套基于块的序列化,成本随既有基础设施线性扩展。

10. 备选方案回顾:为什么放弃三种更简单的设计

提案明确评估并否决了三个备选方案:

  1. 独立 dirty anchor(anchor-dirty):单独存储 dirty 状态。否决理由:每次 status 都要加载并 diff 两棵状态树,序列化/反序列化成本翻倍;单 anchor 模型避免了双树同步问题,并完整复用既有基础设施——dirty 标志作为同一节点上的附加位是更自然的选择;
  2. 扁平路径集合(hash set / 排序列表):在 Merkle 树之外维护扁平 dirty 路径表。否决理由:丢失与树遍历和 diff 基础设施的集成;基于路径的查找无法利用 Merkle 树的层次结构做高效的子树操作("此目录下是否有任何文件是脏的?"),且每次 status 都要将扁平集合与树对账,徒增复杂度而无性能收益;
  3. file dirty时做内容哈希验证:读取并哈希文件内容来确认文件真的变了。否决理由:内容哈希违背轻量通知通道的设计初衷——调用方(IDE/监视器)已经知道文件变了,再读文件又重新引入了 dirty 跟踪 API 本要避免的 I/O 成本。需要内容级验证时,用户可以用--scan。

11. 与既有 VCS 的对照(Prior Art)

提案把 Lore 的方案放在 VCS 谱系中定位:

  • Git:用 index(暂存区)跟踪工作树状态,git status每次对比工作树与 index、index 与 HEAD,做全量文件系统扫描;用 fsmonitor 扩展钩住 OS 级变更通知(macOS 的 FSEvents、Windows 的 ReadDirectoryChangesW)来跳过未修改子树;VFS for Git(前 GVFS)维护一份由投影文件系统提供方填充的 modified-paths 列表。Lore 的file dirty扮演相似角色——外部进程与 VFS 提供方上报变更——但把结果集成进既有 Merkle 树而非使用单独的 watch 位图或旁路文件;
  • Perforce:通过显式 checkout 跟踪文件状态,文件默认只读,直到p4 edit才标记为打开编辑并变为可写——完全不隐式检测变更的反向极端。Lore 介于两者之间:既支持显式通知(file dirty)也支持按需扫描(--scan);
  • Mercurial:用 dirstate 记录每个被跟踪文件的预期 size/mtime/mode,hg status先用文件系统元数据比对定位变更、仅当元数据不一致时才回退到内容比较。Lore 的快速路径与之同源——见file_modification函数(state.rs):先比 size(ModifiedBySize),再比记录的 mtime(UnmodifiedByMtime),最后才哈希内容。Mercurial 的 Rust 重写(rhg)也加入了与 Git fsmonitor 精神类似的文件系统监视器集成。

12. 遗留未决问题

提案在收尾时保留了三个未决问题,供后续实现继续打磨:

  1. file dirty在未指定动作子命令时,应验证内容与已提交修订一致,还是仅靠文件系统存在性即可分类动作?
  2. --scan如何处理 dirty-moved 但目标文件缺失的节点?备选:在树中还原 move(把节点恢复到提交位置),或转换为 dirty-delete;
  3. --scan如何处理 dirty-added(零内容哈希)但文件已从磁盘删除的节点?备选:若未暂存则整节点丢弃,若已暂存则仅清除 dirty 标志。

13. 从提案到代码:当前仓库的落地状态

在本文撰写时的当前仓库中,该提案的主体已经落地,可以作为交叉验证的源码证据链:

  • 标志位定义与测试:node.rs(V1Dirty = 0b1000、DirtyBits、DirtyModify/Add/Delete/Move/Copy)、node.rs(V2Dirty = 1 << 15),位不变式断言在 node.rs;
  • 传播原语:state.rs(node_mark_dirty,父节点仅基础 Dirty 位 + early-out)、state.rs(node_has_dirty_children);
  • 命令实现:file/dirty.rs(DirtyError错误集、modify/add/delete/move/copy 各分类路径上的node_mark_dirty调用)、file.rs(FileDirtyArgs/--targets/move/copyCLI 参数);
  • 状态展示与扫描:repository/status.rs(flag_dirty字段、scan选项、show_scan区分状态 diff 与扫描 diff、并行扫描汇总)、state/os_diff.rs(diff_filesystem基础设施与扫描并发控制);
  • 快速路径:state.rs(file_modification:size → mtime → hash 三级判定)。

总体而言,Modified File Tracking 提案为 Lore 补齐了"修改状态"这一 VCS 交互中的关键拼图:用 Merkle 树上的一个正交标志位,把变更检测(交给集成方)与变更跟踪(交给仓库)彻底分离,让lore status从"默认盲目"变为"默认知情",同时保留--scan作为按需的、结果可持久化的全量对账手段。

  • 版本控制
  • 后端

【免费下载链接】lore

Lore is a next-generation, open source version control system

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

相关推荐

上一篇:终极开源项目影响力评估指南:star-history工具深度评测
下一篇:gh_mirrors/sh1/sh的发布准备文档:准备清单与检查项

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 7:09:38

C语言面试题深度解析:static、const、sizeof与指针内存考点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 7:09:23

认识 React 360:用 React 构建跨平台 360° 与 VR 网页应用

前端3D渲染 【免费下载链接】react-360 Create amazing 360 and VR content using React 项目地址&#xff1a; https://gitcode.com/gh_mirrors/re/react-360 点击查看 免费下载 React 360 是一个基于 React 构建 3D 与 VR 用户界面的开源框架&#xff0c;让你用熟悉的组件、…

作者头像 李华
网站建设 2026/9/25 7:06:58

昇腾Atlas 300V AI推理加速卡上部署YOLO模型全流程指南

1. 先说结论&#xff1a;Atlas 300V 到底是不是“运算加速卡”很多人第一次看到“atlas 300v 24g 是运算加速卡吗”这个热搜词&#xff0c;我就知道大概率是刚接触昇腾生态的开发者。这个问题问得其实很微妙——因为“运算加速卡”这个词本身就有歧义。如果你拿GPU的思维去理解…

作者头像 李华