11## Tag 命令改进详细计划
22
3- > 最后编写时间:2026-03-30
3+ > 最后编写时间:2026-04-01
44
55同时落地 [ Cross-Cutting Improvements A/B/F/G] ( README.md#全局层面改进贯穿所有命令 ) 。
66
7+ > 当前工作区实现已按本文范围落地一部分改动;以下内容改为记录已落地能力、剩余遗漏和后续收口项。
8+
79### 已完成前置条件与当前代码状态
810
911第一批全部 8 个命令的主改造已在当前代码库落地。` tag ` 是第二批(状态变更确认命令)中管理版本标记的命令。
1416- ` OutputConfig ` + ` emit_json_data() ` + ` info_println!() ` 输出框架已可用
1517- ` StableErrorCode ` 体系已有 18 个错误码
1618- ` CliError ` 支持 ` .with_hint() ` 、` .with_stable_code() ` 、` .with_detail() `
17- - ` execute() ` / ` execute_safe(args, _output) ` 双入口已存在(` tag.rs:42/51 ` )
19+ - ` execute() ` / ` execute_safe(args, output) ` 双入口已存在
20+ - ` run_tag() ` + ` TagOutput ` 已实现 list / create / delete 的 JSON / machine 输出
1821- ` -l ` / ` -d ` / ` -m ` / ` -f ` / ` -n ` 短标志已实现
19- - ` create_tag_safe() ` 已有 ` .with_hint ()` 提供重复创建时的 hint( ` tag.rs:87-96 ` )
22+ - 重复创建时的 hint 已在 ` map_create_tag_error ()` → ` CliError ` 映射中保留
2023- ` render_tags() ` 支持 ` -n ` 控制注释行数显示
2124- ` internal::tag ` 模块提供底层 tag API
22- - 内部 tag API 已返回 ` LBR-CONFLICT-002 ` 错误码(从 ` tag_test.rs:157 ` 验证)
25+ - create / delete / find major error path 已在命令层映射显式 ` StableErrorCode `
26+ - quiet delete、malformed ref delete 和 JSON schema 已有回归测试覆盖
27+
28+ ** 基于当前代码的 Review 结论(已改进部分 vs 仍需改进部分):**
29+
30+ 已改进(当前代码已具备):
2331
24- ** 基于当前代码的 Review 结论(tag 仍需改进的部分):**
32+ - ** 结构化输出已落地** :` run_tag() ` + ` TagOutput ` 已覆盖 list / create / delete 三类操作,` --json ` / ` --machine ` 可直接使用
33+ - ** 主要命令层错误已带显式 ` StableErrorCode ` ** :重复创建、HEAD unborn、tag not found、delete I/O 失败、repo read failure 等路径已有稳定错误码
34+ - ** ` TagError ` typed enum 已落地** :create / list / delete / load 路径已统一收口到命令层 typed error
35+ - ** 统一 ` run_tag() ` / ` render_tag_output() ` 分层已落地** :human / JSON / machine 已走同一执行层和渲染层
36+ - ** 重复创建 hint 已落地** :` map_create_tag_error() ` 已保留删除旧 tag 或更换 tag 名的提示
37+ - ** human 成功反馈已统一** :lightweight / annotated create 与 delete 都已输出单行确认消息
38+ - ** ` --help ` EXAMPLES 已落地**
39+ - ** quiet / malformed ref delete 回归已覆盖** :当前测试已覆盖 quiet delete、删除损坏 tag ref、JSON delete ` hash = null ` 等边界
2540
26- - ** 零 JSON / machine 输出** :` OutputConfig ` 参数标记为 ` _output ` 完全未使用(` tag.rs:51 ` )
27- - ** 零 ` StableErrorCode ` 在命令层** :虽然内部 tag API 返回 ` LBR-CONFLICT-002 ` ,但命令层的 ` CliError::fatal() ` 无显式错误码
28- - ** 无 ` TagError ` typed enum** :错误散落在 ` execute_safe() ` 、` create_tag_safe() ` 、` delete_tag_safe() ` 、` show_tag_safe() ` 中
29- - ** 退出码不对齐** :重复创建时退出码应为明确的非零值(当前通过 ` CliError::fatal() ` 返回 128,但无 stable code)
30- - ** 删除不存在 tag 时退出码不对齐** :应返回 exit ` 1 ` 或 ` 129 `
31- - ** 测试注释有全角括号** :` tag_test.rs ` 中有 ` (lightweight tag) ` 等全角括号应改为半角
41+ 仍需改进:
42+
43+ - ** legacy 说明尚未完全清理** :本文后文仍有少量历史设计叙述,需要继续收口为“现状 + follow-up”格式
44+ - ** human / JSON 双契约需持续维护** :lightweight tag 当前保持 ` message: null ` 的 machine 契约,同时 human ` -n ` 列表仍显示 commit message;后续需要继续用回归测试锁住这一行为
3245
3346### 目标与非目标
3447
35- ** 本批目标:**
36- - 引入 ` TagError ` typed error enum,覆盖 tag 层面的错误场景
37- - 所有 ` TagError → CliError ` 映射使用显式 ` StableErrorCode `
38- - 拆分执行层与渲染层:新增 ` run_tag(args) -> Result<TagOutput, TagError> ` 纯执行入口
39- - 实现 JSON 输出(tag 操作结果 + tag 列表结构化)
40- - 重复创建时保留 hint(已有)并补齐 ` StableErrorCode `
41- - 删除不存在 tag 时返回 exit ` 129 ` + hint
42- - 修复测试注释中的全角括号
43- - 补齐 ` --help ` EXAMPLES 段
48+ ** 已完成目标:**
49+ - ` TagError ` typed error enum、显式 ` StableErrorCode ` 、统一 ` run_tag() ` / ` render_tag_output() ` 分层、human 成功确认消息、create 失败来源结构化映射和 ` --help ` EXAMPLES 已落地
50+
51+ ** 后续收口目标:**
52+ - 继续维护 lightweight tag 的 human / machine 双契约和边界回归测试
53+ - 持续清理本文残留的历史设计说明,使计划文档完全反映当前实现
4454
4555** 本批非目标:**
4656- ** 不重写 ` internal::tag ` 底层业务语义** 。允许做类型收紧和错误建模调整(例如 ` create() ` 返回 ` CreateTagError ` ),但不改变 tag 创建/删除/查询的语义行为
5060
5161### 设计原则
5262
53- 1 . ** 执行路径与渲染职责拆分 ** :` execute_safe() ` 根据 ` OutputConfig ` 分流 human / JSON 路径,JSON 路径返回结构化 ` TagOutput `
63+ 1 . ** 执行层与渲染层拆分 ** :` execute_safe() ` 调用 ` run_tag() ` 收集结构化 ` TagOutput ` 结果,再根据 ` OutputConfig ` 通过 ` render_tag_output() ` 渲染 human / JSON / machine,消除当前 human 路径与 JSON 路径分治的架构
54642 . ** JSON 覆盖 list、create、delete 三种操作** :通过 ` action ` 字段区分
55653 . ** 错误码显式映射** :每个 ` TagError ` 变体都有确定的 ` StableErrorCode `
56664 . ** 保留现有 hint** :重复创建时的 hint 保持一致
67+ 5 . ** typed enum 自身携带错误分类信息** :不能依赖 ` TagError ` 变体外的来源注释再决定 ` StableErrorCode `
5768
5869### 特性 1:TagError typed error enum
5970
@@ -74,8 +85,20 @@ pub enum TagError {
7485 #[error(" tag name is required" )]
7586 MissingName ,
7687
77- #[error(" failed to create tag '{name}': {detail}" )]
78- CreateFailed { name : String , detail : String },
88+ #[error(" cannot create tag: HEAD does not point to a commit" )]
89+ HeadUnborn ,
90+
91+ #[error(" failed to read existing tags before creating '{name}': {detail}" )]
92+ CheckExistingFailed { name : String , detail : String },
93+
94+ #[error(" failed to serialize annotated tag object: {0}" )]
95+ SerializeAnnotatedTag (String ),
96+
97+ #[error(" failed to store annotated tag object: {0}" )]
98+ StoreObjectFailed (String ),
99+
100+ #[error(" failed to persist tag reference '{name}': {detail}" )]
101+ PersistReferenceFailed { name : String , detail : String },
79102
80103 #[error(" failed to delete tag '{name}': {detail}" )]
81104 DeleteFailed { name : String , detail : String },
@@ -88,6 +111,8 @@ pub enum TagError {
88111}
89112```
90113
114+ > ** 与 ` internal::tag::CreateTagError ` 的关系** :` CreateTagError ` 是底层业务模块定义的错误类型(含 ` AlreadyExists ` 、` HeadUnborn ` 、` CheckExisting ` 、` SerializeTag ` 、` StoreObject ` 、` PersistReference ` )。` TagError ` 是命令层 typed enum,当前代码通过 ` map_create_tag_error() ` 完成收口映射:` CheckExisting ` → ` CheckExistingFailed ` ,` SerializeTag ` → ` SerializeAnnotatedTag ` ,` StoreObject ` → ` StoreObjectFailed ` ,` PersistReference ` → ` PersistReferenceFailed ` 。
115+
91116** ` TagError → CliError ` 显式映射:**
92117
93118| TagError 变体 | StableErrorCode | 退出码 | hint |
@@ -96,64 +121,67 @@ pub enum TagError {
96121| ` AlreadyExists ` | ` ConflictOperationBlocked ` | 128 | ` delete it first with 'libra tag -d {name}' ` + ` or choose a different tag name ` |
97122| ` NotFound ` | ` CliInvalidTarget ` | 129 | ` use 'libra tag -l' to list available tags ` |
98123| ` MissingName ` | ` CliInvalidArguments ` | 129 | ` provide a tag name ` |
99- | ` CreateFailed ` | ` IoWriteFailed ` | 128 | 无 |
124+ | ` HeadUnborn ` | ` RepoStateInvalid ` | 128 | ` create a commit first before tagging HEAD ` |
125+ | ` CheckExistingFailed ` | ` RepoCorrupt ` | 128 | 无 |
126+ | ` SerializeAnnotatedTag ` | ` InternalInvariant ` | 128 | 附带 Issues URL |
127+ | ` StoreObjectFailed ` | ` IoWriteFailed ` | 128 | 无 |
128+ | ` PersistReferenceFailed ` | ` IoWriteFailed ` | 128 | 无 |
100129| ` DeleteFailed ` | ` IoWriteFailed ` | 128 | 无 |
101130| ` LoadFailed ` | ` RepoCorrupt ` | 128 | 无 |
102- | ` ListFailed ` | ` IoReadFailed ` | 128 | 无 |
131+ | ` ListFailed ` | ` RepoCorrupt ` | 128 | 无 |
132+
133+ ** 与当前代码中 inline 错误的对应关系:**
134+
135+ | 当前代码位置 | 当前 inline 错误 | 对应 TagError 变体 |
136+ | -------------| -----------------| ---------------------|
137+ | ` execute_safe:75 ` | ` validate_named_tag_action() ` | ` MissingName ` (delete/force 缺少 tag 名) |
138+ | ` execute_safe:89 ` | ` CliError::fatal(e.to_string()) ` render_tags 失败 | ` ListFailed ` |
139+ | ` create_tag_safe:148-151 ` | ` map_create_tag_error() ` → ` AlreadyExists ` | ` AlreadyExists ` |
140+ | ` map_create_tag_error:162-165 ` | ` CreateTagError::HeadUnborn ` | ` HeadUnborn ` |
141+ | ` map_create_tag_error:167-171 ` | ` CreateTagError::CheckExisting ` | ` CheckExistingFailed ` |
142+ | ` map_create_tag_error:172-175 ` | ` CreateTagError::SerializeTag ` | ` SerializeAnnotatedTag ` |
143+ | ` map_create_tag_error:176-178 ` | ` CreateTagError::StoreObject ` | ` StoreObjectFailed ` |
144+ | ` map_create_tag_error:180-184 ` | ` CreateTagError::PersistReference ` | ` PersistReferenceFailed ` |
145+ | ` delete_tag_safe:242-246 ` | ` tag::delete().map_err(...) ` | ` DeleteFailed ` |
146+ | ` lookup_tag:357-365 ` | ` Ok(None) ` tag not found | ` NotFound ` |
147+ | ` lookup_tag:357-365 ` | ` Err(e) ` repo corrupt | ` LoadFailed ` |
148+ | ` collect_tags:346-350 ` | ` tag::list().map_err(...) ` | ` ListFailed ` |
149+ | ` lookup_tag:332-334 ` | ` Ok(None) ` tag not found | ` NotFound ` |
150+ | ` lookup_tag:335-337 ` | ` Err(e) ` repo corrupt | ` LoadFailed ` |
103151
104152### 特性 2:执行层与渲染层拆分
105153
106- ** 方案 :**
154+ ** 已落地部分(保持不变) :** ` TagOutput ` enum(含 ` List ` / ` Create ` / ` Delete ` 三变体)、 ` TagListEntry ` 结构体均已存在于 ` tag.rs:41-63 ` ,JSON schema 已稳定。
107155
108- ``` rust
109- #[derive(Debug , Clone , Serialize )]
110- #[serde(tag = " action" )]
111- pub enum TagOutput {
112- #[serde(rename = " list" )]
113- List (TagListOutput ),
114- #[serde(rename = " create" )]
115- Create (TagCreateOutput ),
116- #[serde(rename = " delete" )]
117- Delete (TagDeleteOutput ),
118- }
156+ ** 本批变更:统一 ` run_tag() ` / ` render_tag_output() ` 分层**
119157
120- #[derive(Debug , Clone , Serialize )]
121- pub struct TagListOutput {
122- pub tags : Vec <TagListEntry >,
123- }
158+ 当前架构已经统一:` execute_safe() ` 调用 ` run_tag() ` 收集结构化结果,再由 ` render_tag_output() ` 统一渲染 human / JSON / machine。list / create / delete 三类路径已经合流。
124159
125- #[derive(Debug , Clone , Serialize )]
126- pub struct TagListEntry {
127- pub name : String ,
128- pub hash : String ,
129- /// "lightweight" or "annotated"
130- pub tag_type : String ,
131- /// Annotation message (first N lines, None for lightweight)
132- pub message : Option <String >,
133- }
160+ 目标架构:
134161
135- #[derive(Debug , Clone , Serialize )]
136- pub struct TagCreateOutput {
137- pub name : String ,
138- pub hash : String ,
139- pub tag_type : String ,
140- pub message : Option <String >,
141- }
162+ ``` rust
163+ /// 纯执行入口——收集结构化结果,不输出
164+ async fn run_tag (args : & TagArgs ) -> Result <TagOutput , TagError >
142165
143- #[derive(Debug , Clone , Serialize )]
144- pub struct TagDeleteOutput {
145- pub name : String ,
146- pub hash : Option <String >,
166+ /// 渲染层——根据 OutputConfig 决定 human/JSON/machine/quiet 输出
167+ fn render_tag_output (result : & TagOutput , output : & OutputConfig ) -> CliResult <()>
168+
169+ /// execute_safe 调用链
170+ pub async fn execute_safe (args : TagArgs , output : & OutputConfig ) -> CliResult <()> {
171+ let result = run_tag (& args ). await . map_err (CliError :: from )? ;
172+ render_tag_output (& result , output )
147173}
148174```
149175
176+ 历史上的 ` run_tag_json() ` 已合并入 ` run_tag() ` ;` render_tags() ` 当前主要保留给测试和辅助调用,实际 CLI human 列表渲染已在 ` render_tag_output() ` 中统一处理。
177+
150178** 渲染规则:**
151179
152180| 模式 | stdout | stderr |
153181| ------| --------| --------|
154182| human list | tag 名称列表(可选 ` -n ` 注释行数) | 无 |
155- | human create | 保留现有创建路径输出( lightweight create 后展示 tag/commit 信息; annotated create 无额外确认消息) | 无 |
156- | human delete | 确认消息(如 ` Deleted tag 'v1.0' (was abc1234) ` ) | 无 |
183+ | human create | 统一确认消息: ` Created lightweight tag 'v1.0' at abc1234 ` 或 ` Created annotated tag 'v1.0' at abc1234 ` | 无 |
184+ | human delete | 确认消息: ` Deleted tag 'v1.0' (was abc1234) ` ;target 丢失时退化为 ` Deleted tag 'v1.0' ` | 无 |
157185| human + ` --quiet ` | 无 | 无 |
158186| ` --json ` / ` --machine ` | JSON envelope(含 ` action ` 字段区分操作类型) | 无 |
159187
@@ -227,10 +255,10 @@ When deleting malformed refs that have no stored target, `hash` is `null`.
227255
228256| ID | 改进 | tag 中的具体落地 |
229257| ----| ------| -----------------|
230- | ** A** | 退出码 ` 0/128/129 ` | 参数错误(缺少 tag 名、不存在的 tag 名)→ exit ` 129 ` ;运行时错误(重复创建、I/O 失败)→ exit ` 128 ` ;成功 → exit ` 0 ` |
258+ | ** A** | 退出码 ` 0/128/129 ` | 参数错误(缺少 tag 名、不存在的 tag 名)→ exit ` 129 ` ;运行时错误(重复创建、HEAD unborn、 I/O 失败)→ exit ` 128 ` ;成功 → exit ` 0 ` |
231259| ** B** | ` --help ` EXAMPLES | 见下方 EXAMPLES 段 |
232260| ** F** | 拼写纠错 | ** 不适用** ——tag 名是用户自定义值,无 enum 可做 fuzzy match |
233- | ** G** | Issues URL | 仅在 ` LoadFailed ` / ` ListFailed ` 错误时输出 Issues URL |
261+ | ** G** | Issues URL | 与 switch 保持一致——仅在映射为 ` InternalInvariant ` 的内部不变式错误时输出。当前仅 ` SerializeAnnotatedTag ` 属于此类; ` RepoCorrupt ` / ` IoWriteFailed ` 是数据或 I/O 问题,不附带 Issues URL |
234262
235263### ` --help ` EXAMPLES 段
236264
@@ -252,9 +280,11 @@ EXAMPLES:
252280
253281- ** (已有)** 重复 tag 错误码、basic creation、annotated tag、force tag、list、delete、annotation lines
254282- ** (新增)` TagError ` 变体覆盖** :
255- - ` NotFound ` :删除不存在 tag 返回 exit ` 129 `
256- - ` MissingName ` :无 tag 名返回 exit ` 129 `
283+ - ` NotFound ` :删除不存在 tag 返回 exit ` 129 ` + ` LBR-CLI-003 `
284+ - ` MissingName ` :无 tag 名返回 exit ` 129 ` + ` LBR-CLI-002 `
285+ - ` HeadUnborn ` :空仓库创建 tag 返回 exit ` 128 ` + ` LBR-REPO-003 `
257286- ** (新增)quiet / delete 输出约束** :` --quiet tag -d ` 不应污染 stdout;human delete 保持确认消息
287+ - ** (新增)human create 输出统一** :lightweight / annotated create 均输出单行确认消息,不再依赖 ` show_tag_safe() ` 打印详情
258288- ** (新增)force 失败路径回归** :` -f ` 遇到对象存储失败时必须保留原有 ref,不得丢 tag
259289- ** (修复)全角括号** :将 ` (lightweight tag) ` 等改为 ` (lightweight tag) `
260290
@@ -280,5 +310,5 @@ EXAMPLES:
280310
281311| 文件 | 改动类型 | 说明 |
282312| ------| ---------| ------|
283- | ` src/command/tag.rs ` | ** 重构 ** | 新增 ` TagError ` typed enum;新增 ` TagOutput ` / ` TagListOutput ` / ` TagCreateOutput ` / ` TagDeleteOutput ` 结构体;命令层 ` TagError → CliError ` 显式 ` StableErrorCode ` 映射;JSON 输出;quiet/delete 输出约束;补齐 ` --help ` EXAMPLES |
284- | ` tests/command/tag_test.rs ` | ** 扩展 ** | 新增 ` TagError ` 变体覆盖、 JSON schema 回归、force 失败路径保护、修复全角括号 |
313+ | ` src/command/tag.rs ` | ** 维护 ** | 保持已落地的 ` TagOutput ` / ` TagError ` / ` run_tag() ` / ` render_tag_output() ` / create hint / human 确认消息 / ` --help ` EXAMPLES 不回退;后续仅维护双契约与边界回归 |
314+ | ` tests/command/tag_test.rs ` | ** 维护 ** | 在现有 JSON / quiet / malformed ref delete / lightweight-human-vs-machine 契约回归基础上,继续维护 ` TagError ` 变体覆盖与成功反馈一致性校验 |
0 commit comments