- 版本控制
- 后端
【免费下载链接】lore
Lore is a next-generation, open source version control system
导读:本文以 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 原文)。这带来三个问题:
- 状态默认是"盲"的——用户看不到未暂存的本地修改,除非显式跑一次昂贵的扫描;
- 没有通知通道——IDE、文件系统监视器、构建系统、VFS 提供方明明通过操作系统事件知道哪些文件变了,却没有途径告诉 Lore,只能要么过度提交(全部 stage),要么漏报(等用户扫描);
- 跟踪逻辑重复——每个需要修改状态的应用程序都要各自实现一套跟踪,行为发散、边界情况各异、无法与 Lore 共享状态。
更深层的原因是--unstaged把"扫描文件系统(昂贵 I/O)"和"展示已知变更(廉价状态读取)"这两件事混在了同一个标志里。提案的核心洞察是:把变更的检测(detection)与变更的跟踪(tracking)分离。检测是集成方的责任(它们本来就通过 OS 事件知道变化),而跟踪是 Lore 的责任(持久化状态、树传播、与暂存/提交等操作交互),所有消费者从唯一权威来源读取。
2. 目标与非目标
Goals(提案目标)
- 把修改文件跟踪作为一等仓库概念:Lore 无需扫描即可知道磁盘上有哪些文件被修改,且该知识跨操作持久存在;
- 让外部集成能够上报修改:通过轻量 API(无需暂存)告知 Lore 文件变化;
- 默认展示修改:
lore status默认显示本地修改文件,无需标志或全量爬取; - 扫描与展示分离:按需扫描的结果被持久化,后续 status 调用能即时展示已知变更;
- 修改状态贯穿暂存/提交生命周期:暂存不清除修改跟踪,只有提交才清除;未纳入提交的文件保留修改状态。
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对相同文件系统状态与已提交修订确定。
已知风险与应对
- 共享动作位导致动作覆盖:stage 一个 modify 后再
file dirty标记为 delete,会把 staged 动作改成 delete。这是文档化的 "latest action wins" 语义——动作反映当前文件系统现实而非历史意图(用户 stage 后又删除了文件,理应看到删除动作); - 扫描中断的原子性:
scan_dirty会在读导向操作(lore status)中修改 staged 状态。缓解:staged anchor 在完整扫描完成后原子更新,扫描中途崩溃时旧 anchor 保持完整;file dirty的即时持久化产生的部分 dirty 状态,也不比一次被中断的lore stage更糟; - 存在性检查的竞态窗口:
file dirty通过文件系统存在性分类动作,存在"调用方检测与 Lore 存在性检查之间文件快速变化"的竞态。move/copy子命令通过完全信任调用方来规避这一点。
代价(Drawbacks)
- Dirty 与 Staged 共享动作位,无法同时表示两个生命周期状态的不同动作(如"staged 为 modify,dirty 为 delete");
file dirty的一次同步存在性检查引入了轻微系统调用开销与竞态窗口;- staged anchor 随 dirty 跟踪文件数增长(dirty 状态与 staged 状态共处同一 Merkle 树),但被仓库文件总数所界定,且沿用 staged 状态同一套基于块的序列化,成本随既有基础设施线性扩展。
10. 备选方案回顾:为什么放弃三种更简单的设计
提案明确评估并否决了三个备选方案:
- 独立 dirty anchor(
anchor-dirty):单独存储 dirty 状态。否决理由:每次 status 都要加载并 diff 两棵状态树,序列化/反序列化成本翻倍;单 anchor 模型避免了双树同步问题,并完整复用既有基础设施——dirty 标志作为同一节点上的附加位是更自然的选择; - 扁平路径集合(hash set / 排序列表):在 Merkle 树之外维护扁平 dirty 路径表。否决理由:丢失与树遍历和 diff 基础设施的集成;基于路径的查找无法利用 Merkle 树的层次结构做高效的子树操作("此目录下是否有任何文件是脏的?"),且每次 status 都要将扁平集合与树对账,徒增复杂度而无性能收益;
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. 遗留未决问题
提案在收尾时保留了三个未决问题,供后续实现继续打磨:
file dirty在未指定动作子命令时,应验证内容与已提交修订一致,还是仅靠文件系统存在性即可分类动作?--scan如何处理 dirty-moved 但目标文件缺失的节点?备选:在树中还原 move(把节点恢复到提交位置),或转换为 dirty-delete;--scan如何处理 dirty-added(零内容哈希)但文件已从磁盘删除的节点?备选:若未暂存则整节点丢弃,若已暂存则仅清除 dirty 标志。
13. 从提案到代码:当前仓库的落地状态
在本文撰写时的当前仓库中,该提案的主体已经落地,可以作为交叉验证的源码证据链:
- 标志位定义与测试:node.rs(V1
Dirty = 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
相关推荐
Linux 内核 Si4713 FM 无线电发射器驱动深度解析:V4L2 控制、RDS 与 RNL 测量实战指南
Linux 内核 Si4713 FM 无线电发射器驱动深度解析:V4L2 控制、RDS 与 RNL 测量实战指南 导读 本文以内核文档 Documentatio
版本控制后端Lore 日志体系全解:tracing 与 Lore 宏的双轨架构、级别门控与日志文件轮转
Lore 日志体系全解:tracing 与 Lore 宏的双轨架构、级别门控与日志文件轮转 Lore 代码库中存在两套并行的日志系统:面向服务端和工具链的 tr
版本控制后端gh_mirrors/co/completion核心功能解析:代码补全、定义跳转与文档查看全攻略
gh_mirrors/co/completion核心功能解析:代码补全、定义跳转与文档查看全攻略 GitHub 加速计划 / co / completion 是
版本控制后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考