Skip to content

Report for "notes" #400

Description

@ShallowDream121

Libra Notes 命令实现实验报告

一、实验目的

  1. 理解 Git 对象存储模型(blob、commit、tree)及 notes 机制的工作原理
  2. 掌握 Rust 异步编程模型(async fntokiosea-orm 数据库操作)
  3. 实践 CLI 工具开发流程:需求分析 → 数据库设计 → 核心逻辑 → CLI 集成 → 测试覆盖
  4. 学习工业级代码规范:clippy 零告警、稳定错误码映射、结构化 JSON 输出、兼容性矩阵
  5. 体验开源协作流程:PR 提交 → Code Review → 迭代修改 → 合并上游

二、需求分析

2.1 功能需求

libra notes 是兼容 Git notes 的笔记管理命令,支持在不修改 commit 对象的前提下附加后置元数据。核心子命令:

子命令 功能 Git 兼容性
add 为 commit 添加笔记(-m/-F 指定内容,-f 强制覆盖) 兼容
list 列出笔记对象及其关联的 commit(支持按对象过滤) 兼容
show 显示笔记内容 兼容
remove 删除指定对象的笔记 兼容
append/edit/copy/merge/prune/get-ref 不实现 记录在 COMPATIBILITY.md

2.2 非功能需求

  • 结构化输出:支持 --json / --machine 输出,用于 AI agent 消费
  • 稳定错误码:7 种错误类型映射到 LBR-CLI-002/003LBR-REPO-003LBR-CONFLICT-002
  • 自定义 namespace--ref 支持短名称自动展开(reviewrefs/notes/review
  • 代码质量:clippy 零告警通过

三、设计思路

3.1 架构分层

tests/command/notes_test.rs       (63 集成测试)
─────────────────────────────────────────
src/command/notes.rs               (CLI 层:参数解析、输出渲染、错误映射)
─────────────────────────────────────────
src/internal/notes.rs              (核心层:业务逻辑、对象解析、数据库操作)
─────────────────────────────────────────
sql/migrations/2026053101_notes.sql (迁移层:已有仓库升级)
sql/sqlite_20260309_init.sql       (数据层:新仓库 bootstrap DDL)
─────────────────────────────────────────
src/internal/db/migration.rs       (注册层:builtin_migrations)

3.2 数据模型

选用 SQLite 而非 Git 原生的 loose refs 存储 notes 映射关系,原因:

  • 原子事务:INSERT/UPDATE/DELETE 在 WAL 模式下天然原子
  • 高效查询:列出所有笔记是一条 SELECT,无需目录扫描
  • 并发安全:SQLite WAL 模式支持读写并发
CREATE TABLE IF NOT EXISTS `notes` (
    `id`         INTEGER PRIMARY KEY AUTOINCREMENT,
    `notes_ref`  TEXT NOT NULL,
    `object`     TEXT NOT NULL,
    `blob`       TEXT NOT NULL,
    UNIQUE(`notes_ref`, `object`)
);
CREATE INDEX IF NOT EXISTS idx_notes_ref ON `notes`(`notes_ref`);

采用双路径 schema 保证:新仓库通过 bootstrap SQL 创建表,已有仓库通过幂等迁移 2026053101_notes 升级。

3.3 CLI 设计

libra notes [--ref <ref>] [<subcommand>] [args...]
  • --refNotesArgs 上定义,支持短名称自动展开(reviewrefs/notes/review
  • 省略子命令时默认执行 list
  • -m 可重复使用,-m/-F 按命令行原始顺序拼接段落
  • 对象作用域 list <object> 仅输出 note hash(兼容 Git 行为)
  • 拒绝空内容(-m '' 或空文件返回错误)
  • execute_safe 接收 argv: &[String] 参数以保留 -m/-F 出现顺序

3.4 错误映射

NotesError::InvalidNotesRef  → StableErrorCode::CliInvalidArguments → LBR-CLI-002
NotesError::InvalidObject    → StableErrorCode::CliInvalidTarget    → LBR-CLI-003
NotesError::NotFound         → StableErrorCode::CliInvalidTarget    → LBR-CLI-003
NotesError::AlreadyExists    → StableErrorCode::ConflictOperationBlocked → LBR-CONFLICT-002
NotesError::HeadUnborn       → StableErrorCode::RepoStateInvalid    → LBR-REPO-003
NotesError::QueryFailed      → StableErrorCode::IoReadFailed        → LBR-IO-001
NotesError::ResolveFailed    → StableErrorCode::RepoCorrupt         → LBR-REPO-002
NotesError::StoreBlobFailed  → StableErrorCode::IoWriteFailed       → LBR-IO-002

四、实现过程

4.1 数据库与迁移

sql/sqlite_20260309_init.sql 新增 notes 表 DDL 作为新仓库的 bootstrap 路径。

同时创建幂等迁移 sql/migrations/2026053101_notes.sql,在 builtin_migrations() 注册(当前共 10 个迁移,最大版本 2026060401)。已有仓库在下次启动时自动应用该迁移,IF NOT EXISTS 保证对已有表的仓库是空操作。

4.2 核心逻辑 — src/internal/notes.rs

函数清单

函数 职责
validate_notes_ref() 校验 ref 必须以 refs/notes/ 开头
normalize_notes_ref() 短名称自动展开(reviewrefs/notes/review
resolve_object() / resolve_head() / resolve_ref() 对象解析,HEAD 特判 unborn 检测
add() INSERT OR IGNORE + rows_affected() 无竞态插入;ON CONFLICT DO UPDATE 原子 upsert
list() 按 ref + 可选 object 查询,无结果返回 note_hash: None
show() 查询 blob hash → 从对象库读取 blob 内容
remove() 两阶段:先验证全部目标,再事务内 DELETE(带 AND blob = ? 并发保护)
find_note_blob() 查询特定 (ref, object) 的 blob hash

关键实现细节:

  • 无竞态 addforce=trueINSERT ... ON CONFLICT DO UPDATEforce=falseINSERT OR IGNORE + rows_affected() 检测冲突。消除 check-then-insert 竞态窗口
  • 事务化 remove:先解析验证所有目标对象,再在单一事务内执行 DELETE。AND blob = ? 条件防止并发 add -f 覆盖后误删——若 rows_affected() == 0,事务回滚并返回 NotFound
  • list nullablelist <object> 无结果返回 {"note_hash": null} 成功响应,而非错误
  • 短 ref 展开normalize_notes_ref()review 展开为 refs/notes/review

4.3 CLI 集成 — src/command/notes.rs

  • 参数定义:四种子命令,-m 可重复、-F 可重复、-f bool flag
  • -m/-F 顺序保持:通过 ordered_content_parts(argv) 扫描原始参数,按命令行顺序拼接段落,而非 clap 默认的先全部 -m 后全部 -F
  • 输出格式:人类可读模式打印格式化结果;JSON 模式通过 emit_json_data 输出结构化数据。对象作用域 list 仅输出 note hash
  • 空内容拒绝:组装后的内容若 trim 为空,返回错误
  • 错误映射NotesCliErrorCliError 通过 From 实现,每种附带 StableErrorCodehint
  • CLI 注册Commands::Notesexecute_safe(args, output, argv),命令放入 "Commit And Branching" 组

4.4 文档与兼容性

  • docs/commands/notes.md:Synopsis、Options、JSON Examples、Design Rationale、Parameter Comparison
  • COMPATIBILITY.md:标记 partial,注明 local-only 限制
  • CHANGELOG.md[Unreleased] 中新增条目
  • Cargo.toml:OpenSSL 切换 vendored,消除系统依赖

4.5 调试与修复历程

阶段 问题 解决方案
编译期 blob.get_type() 编译错误 导入 ObjectTrait
编译期 resolve_object unborn HEAD 错误码错误 拆分 resolve_ref,HEAD 走 Head::current_commit_result()
测试期 27 个测试失败(exit code、--ref 位置、--format 不支持) 修正 exit code(128→129)、--ref 前置、用 rev-parse HEAD
Code Review add 存在 check-then-insert 竞态 INSERT OR IGNORE + rows_affected() / ON CONFLICT DO UPDATE
Code Review remove 非原子、DELETE 无并发保护 两阶段 + 事务 + AND blob = ?
Code Review SELECT changes() 跨连接不安全 改用 rows_affected()
Code Review list <object> 无结果返回错误 改为返回 note_hash: null 成功响应
Code Review 缺少运行时迁移 创建 2026053101_notes 幂等迁移并注册
Code Review 迁移版本低于已有 head 版本从 2026053001 升至 2026053101,最终 20260531022026053101
Code Review --ref 不支持短名称 新增 normalize_notes_ref()
Code Review -m/-F 顺序不保留 通过 argv 参数传入 ordered_content_parts()
Code Review 空内容可创建空笔记 组装后 trim 检查,拒绝空内容
Code Review 对象作用域 list 输出多余字段 object_scoped 标志,仅输出 note hash
Code Review std::env::args() 嵌入调用不安全 argv: &[String] 参数化传递
Code Review OpenSSL 系统依赖 Cargo.toml 加 features = ["vendored"]

五、测试方案与结果

5.1 测试用例设计

63 个集成测试覆盖三层:

分类 用例数 覆盖内容
L1-基础 add/list/show/remove + JSON + --ref + --quiet 25 正常流程、JSON 输出、短 ref 展开、空内容拒绝
L1-边界 空内容/Unicode/超长内容/多对象/多 ref 隔离/原子性回归 12 极端输入、namespace 隔离、remove 原子性验证
L1-错误 缺参数/无效对象/冲突/unborn HEAD/invalid ref/not found 26 所有错误路径、exit code、hint 文案

5.2 测试结果

test result: ok. 63 passed; 0 failed; 0 ignored; 0 measured

5.3 代码质量指标

指标 数值
核心源码行数(internal + command) 600 NLOC(排除空行和注释)
测试代码行数 1013 NLOC(tests/command/notes_test.rs)
平均圈复杂度(lizard) 5.1
行覆盖率(cargo-llvm-cov) 94.00% (185/196 internal, 145/155 command)
clippy 告警(notes 相关) 0 warnings, 0 errors

5.4 Conventional Commits 规范遵循

使用 @commitlint/cli + @commitlint/config-conventional 对 notes 相关的 12 次 commit 进行自动化检测:

类型 次数 通过 说明
feat(notes): ... 4 4/4 初始功能提交,格式完全合规
style(notes): ... 1 1/1 格式修正
fix(notes): ... 6 5/6 Review 修复迭代(1 次句尾多了句号)
其他 1 0/1 长消息缺少 type/scope

检测结果:10/12 通过,遵循率 83.3%。未通过的两条分别为超长消息缺少 type(合并冲突描述)和句尾多余句号。

5.5 提交粒度分析

指标 计算 结果
Notes 功能 Commits (C) 12
源代码有效行数 (LOC,tokei) internal 252 + command 348 600
总有效代码行数 (含测试,tokei) 600 + 1013 1,613
平均 LOC/Commit (源代码) 600 ÷ 12 50.0
平均 LOC/Commit (含测试) 1,613 ÷ 12 134.4
C/LOC 比值 (源代码) 12 ÷ 600 0.020

评估:50.0 LOC/commit 处在推荐范围下限(50–200),反映了多轮 Code Review 小步迭代的特点。初始 4 次核心提交粒度偏大(129.5 LOC/commit),后续 8 次修复提交粒度精细(平均 ~10 LOC/commit)。

六、问题与解决方案

6.1 add 竞态条件

问题:并发 notes add 同时通过 find_note_blob 检查,后到的 INSERT 触发 UNIQUE 约束报通用错误。方案force=trueON CONFLICT DO UPDATE 原子 upsert;force=falseINSERT OR IGNORE + rows_affected() 检测冲突。

6.2 remove 非原子与并发覆写

问题:多目标 remove 逐个 DELETE,中途失败产生部分删除状态;无并发保护时可能误删 add -f 刚写入的笔记。方案:两阶段(先验证全部目标,再事务内 DELETE),带 AND blob = ? 条件,rows_affected() == 0 时回滚事务。

6.3 迁移版本排序

问题:初始迁移版本 2026053001 低于上游已有的 2026053101ai_final_decision),导致已升级仓库跳过此迁移。后续上游删除 ai_final_decision 后需重新调整版本号。方案:最终以 2026053101 注册,插入上游新增的 20260602012026060401 之前,共 10 个迁移。

6.4 连接池不安全的 SELECT changes()

问题SELECT changes() 是连接局部的,连接池可能将 INSERT 和 SELECT 分发到不同连接。方案:用 INSERT OR IGNORE + rows_affected() 替代,全程在同一语句返回中完成冲突检测。

6.5 list 行为与文档不一致

问题:文档描述 list <object> 无结果返回 note_hash: null,但实现返回 NotFound 错误。方案:改为返回 Ok(vec![NoteEntry { note_hash: None, ... }])

6.6 其他 Code Review 修复

  • normalize_notes_ref() 支持短 ref 名展开
  • ordered_content_parts(argv) 保留 -m/-F 命令行顺序
  • 对象作用域 list 加 object_scoped 标志,仅输出 note hash
  • 拒绝空内容(trim 后为空报错)
  • argv: &[String] 参数化代替 std::env::args()
  • OpenSSL 加 features = ["vendored"] 消除系统依赖

七、总结与心得

本次实验完整实现了 Git 兼容的 notes 命令,经历从数据库设计到 CLI 集成、从初始提交到多轮 Code Review 迭代的全链路开发流程。

技术收获:

  • 深入理解 Git 对象存储模型及 notes 作为 commit 外挂元数据的机制
  • 实践了数据库并发安全设计:ON CONFLICTINSERT OR IGNORE、事务化操作、rows_affected() 冲突检测
  • 掌握了 Rust 异步编程与 sea-orm 原始 SQL 操作
  • 学习了开源协作流程:通过多轮 Code Review 逐步修复竞态条件、迁移版本冲突、跨连接安全性等问题

工程实践:

  • 63 个集成测试覆盖所有正常/异常路径,94% 行覆盖率
  • clippy 零告警,最大圈复杂度 10
  • 10 轮以上 Code Review 迭代,修复 15+ 个问题
  • COMPATIBILITY.md + docs/commands/notes.md 完整记录兼容性边界

不足与改进:

  • append/edit/copy/merge 等高级子命令未实现
  • notes refs 仅本地有效,push/fetch/clone 暂不传输
  • notes 表缺乏 author/timestamp 等元数据列

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions