Libra Notes 命令实现实验报告
一、实验目的
- 理解 Git 对象存储模型(blob、commit、tree)及 notes 机制的工作原理
- 掌握 Rust 异步编程模型(
async fn、tokio、sea-orm 数据库操作)
- 实践 CLI 工具开发流程:需求分析 → 数据库设计 → 核心逻辑 → CLI 集成 → 测试覆盖
- 学习工业级代码规范:clippy 零告警、稳定错误码映射、结构化 JSON 输出、兼容性矩阵
- 体验开源协作流程: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/003、LBR-REPO-003、LBR-CONFLICT-002 等
- 自定义 namespace:
--ref 支持短名称自动展开(review → refs/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...]
--ref 在 NotesArgs 上定义,支持短名称自动展开(review → refs/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() |
短名称自动展开(review → refs/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 |
关键实现细节:
- 无竞态 add:
force=true 用 INSERT ... ON CONFLICT DO UPDATE;force=false 用 INSERT OR IGNORE + rows_affected() 检测冲突。消除 check-then-insert 竞态窗口
- 事务化 remove:先解析验证所有目标对象,再在单一事务内执行 DELETE。
AND blob = ? 条件防止并发 add -f 覆盖后误删——若 rows_affected() == 0,事务回滚并返回 NotFound
- list nullable:
list <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 为空,返回错误
- 错误映射:
NotesCliError → CliError 通过 From 实现,每种附带 StableErrorCode 和 hint
- CLI 注册:
Commands::Notes → execute_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,最终 2026053102→2026053101 |
| 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=true 用 ON CONFLICT DO UPDATE 原子 upsert;force=false 用 INSERT OR IGNORE + rows_affected() 检测冲突。
6.2 remove 非原子与并发覆写
问题:多目标 remove 逐个 DELETE,中途失败产生部分删除状态;无并发保护时可能误删 add -f 刚写入的笔记。方案:两阶段(先验证全部目标,再事务内 DELETE),带 AND blob = ? 条件,rows_affected() == 0 时回滚事务。
6.3 迁移版本排序
问题:初始迁移版本 2026053001 低于上游已有的 2026053101(ai_final_decision),导致已升级仓库跳过此迁移。后续上游删除 ai_final_decision 后需重新调整版本号。方案:最终以 2026053101 注册,插入上游新增的 2026060201 和 2026060401 之前,共 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 CONFLICT、INSERT 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 等元数据列
Libra Notes 命令实现实验报告
一、实验目的
async fn、tokio、sea-orm数据库操作)二、需求分析
2.1 功能需求
libra notes 是兼容 Git notes 的笔记管理命令,支持在不修改 commit 对象的前提下附加后置元数据。核心子命令:
add-m/-F指定内容,-f强制覆盖)listshowremoveappend/edit/copy/merge/prune/get-ref2.2 非功能需求
--json/--machine输出,用于 AI agent 消费LBR-CLI-002/003、LBR-REPO-003、LBR-CONFLICT-002等--ref支持短名称自动展开(review→refs/notes/review)三、设计思路
3.1 架构分层
3.2 数据模型
选用 SQLite 而非 Git 原生的 loose refs 存储 notes 映射关系,原因:
SELECT,无需目录扫描采用双路径 schema 保证:新仓库通过 bootstrap SQL 创建表,已有仓库通过幂等迁移
2026053101_notes升级。3.3 CLI 设计
--ref在NotesArgs上定义,支持短名称自动展开(review→refs/notes/review)list-m可重复使用,-m/-F按命令行原始顺序拼接段落list <object>仅输出 note hash(兼容 Git 行为)-m ''或空文件返回错误)execute_safe接收argv: &[String]参数以保留-m/-F出现顺序3.4 错误映射
四、实现过程
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()refs/notes/开头normalize_notes_ref()review→refs/notes/review)resolve_object()/resolve_head()/resolve_ref()add()INSERT OR IGNORE+rows_affected()无竞态插入;ON CONFLICT DO UPDATE原子 upsertlist()note_hash: Noneshow()remove()AND blob = ?并发保护)find_note_blob()关键实现细节:
force=true用INSERT ... ON CONFLICT DO UPDATE;force=false用INSERT OR IGNORE+rows_affected()检测冲突。消除 check-then-insert 竞态窗口AND blob = ?条件防止并发add -f覆盖后误删——若rows_affected() == 0,事务回滚并返回NotFoundlist <object>无结果返回{"note_hash": null}成功响应,而非错误normalize_notes_ref()将review展开为refs/notes/review4.3 CLI 集成 —
src/command/notes.rs-m可重复、-F可重复、-fbool flag-m/-F顺序保持:通过ordered_content_parts(argv)扫描原始参数,按命令行顺序拼接段落,而非 clap 默认的先全部-m后全部-Femit_json_data输出结构化数据。对象作用域 list 仅输出 note hashNotesCliError→CliError通过From实现,每种附带StableErrorCode和hintCommands::Notes→execute_safe(args, output, argv),命令放入 "Commit And Branching" 组4.4 文档与兼容性
docs/commands/notes.md:Synopsis、Options、JSON Examples、Design Rationale、Parameter ComparisonCOMPATIBILITY.md:标记partial,注明 local-only 限制CHANGELOG.md:[Unreleased]中新增条目Cargo.toml:OpenSSL 切换 vendored,消除系统依赖4.5 调试与修复历程
blob.get_type()编译错误ObjectTraitresolve_objectunborn HEAD 错误码错误resolve_ref,HEAD 走Head::current_commit_result()--ref位置、--format不支持)--ref前置、用rev-parse HEADINSERT OR IGNORE+rows_affected()/ON CONFLICT DO UPDATEAND blob = ?SELECT changes()跨连接不安全rows_affected()list <object>无结果返回错误note_hash: null成功响应2026053101_notes幂等迁移并注册2026053001升至2026053101,最终2026053102→2026053101--ref不支持短名称normalize_notes_ref()-m/-F顺序不保留argv参数传入ordered_content_parts()object_scoped标志,仅输出 note hashstd::env::args()嵌入调用不安全argv: &[String]参数化传递features = ["vendored"]五、测试方案与结果
5.1 测试用例设计
63 个集成测试覆盖三层:
--ref+--quiet5.2 测试结果
5.3 代码质量指标
5.4 Conventional Commits 规范遵循
使用
@commitlint/cli+@commitlint/config-conventional对 notes 相关的 12 次 commit 进行自动化检测:feat(notes): ...style(notes): ...fix(notes): ...检测结果:10/12 通过,遵循率 83.3%。未通过的两条分别为超长消息缺少 type(合并冲突描述)和句尾多余句号。
5.5 提交粒度分析
评估: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=true用ON CONFLICT DO UPDATE原子 upsert;force=false用INSERT OR IGNORE+rows_affected()检测冲突。6.2 remove 非原子与并发覆写
问题:多目标 remove 逐个 DELETE,中途失败产生部分删除状态;无并发保护时可能误删
add -f刚写入的笔记。方案:两阶段(先验证全部目标,再事务内 DELETE),带AND blob = ?条件,rows_affected() == 0时回滚事务。6.3 迁移版本排序
问题:初始迁移版本
2026053001低于上游已有的2026053101(ai_final_decision),导致已升级仓库跳过此迁移。后续上游删除ai_final_decision后需重新调整版本号。方案:最终以2026053101注册,插入上游新增的2026060201和2026060401之前,共 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命令行顺序object_scoped标志,仅输出 note hashargv: &[String]参数化代替std::env::args()features = ["vendored"]消除系统依赖七、总结与心得
本次实验完整实现了 Git 兼容的 notes 命令,经历从数据库设计到 CLI 集成、从初始提交到多轮 Code Review 迭代的全链路开发流程。
技术收获:
ON CONFLICT、INSERT OR IGNORE、事务化操作、rows_affected()冲突检测工程实践:
COMPATIBILITY.md+docs/commands/notes.md完整记录兼容性边界不足与改进:
append/edit/copy/merge等高级子命令未实现notes表缺乏author/timestamp等元数据列