memstead_cli/commands/
review_mark.rs1use clap::{Args as ClapArgs, Parser, Subcommand};
15
16use crate::CliError;
17use crate::output::{print_json, print_markdown};
18use crate::setup::CliContext;
19
20#[derive(Parser, Debug)]
21pub struct Args {
22 #[command(subcommand)]
23 pub action: ReviewMarkAction,
24}
25
26#[derive(Subcommand, Debug)]
27pub enum ReviewMarkAction {
28 List,
31 Set(SetArgs),
39 Clear(ClearArgs),
41 Diff(DiffArgs),
46}
47
48#[derive(ClapArgs, Debug)]
49pub struct SetArgs {
50 pub mem: String,
52 pub state: String,
54 #[arg(long)]
58 pub note: Option<String>,
59}
60
61#[derive(ClapArgs, Debug)]
62pub struct ClearArgs {
63 pub mem: String,
65 #[arg(long)]
67 pub note: Option<String>,
68}
69
70#[derive(ClapArgs, Debug)]
71pub struct DiffArgs {
72 pub mem: String,
74 #[arg(long)]
79 pub rename_similarity: Option<f32>,
80}
81
82pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
83 let mut engine = ctx.cli_engine()?.into_base();
84 match args.action {
85 ReviewMarkAction::List => {
86 let marks = engine.review_marks();
87 if ctx.json {
88 print_json(&serde_json::json!({ "marks": marks }))?;
89 return Ok(());
90 }
91 let mut lines = vec!["# Review marks".to_string(), String::new()];
92 if marks.is_empty() {
93 lines.push("_no mems loaded_".to_string());
94 }
95 for s in &marks {
96 let head = s.head.as_deref().unwrap_or("(no head)");
97 let line = match s.mark.as_deref() {
98 None => format!("- `{}` — no mark (head `{head}`)", s.mem),
99 Some(mark) if Some(mark) == s.head.as_deref() => {
100 format!("- `{}` — mark `{mark}` at head (nothing unreviewed)", s.mem)
101 }
102 Some(mark) => format!(
103 "- `{}` — mark `{mark}`, head `{head}` (unreviewed changes — `memstead review-mark diff {}`)",
104 s.mem, s.mem
105 ),
106 };
107 lines.push(line);
108 }
109 print_markdown(&lines.join("\n"));
110 Ok(())
111 }
112 ReviewMarkAction::Set(a) => {
113 let outcome = engine
114 .set_review_mark(&a.mem, Some(&a.state), a.note.as_deref())
115 .map_err(CliError::from_engine_op)?;
116 report_outcome(ctx, &outcome, "set")
117 }
118 ReviewMarkAction::Clear(a) => {
119 let outcome = engine
120 .set_review_mark(&a.mem, None, a.note.as_deref())
121 .map_err(CliError::from_engine_op)?;
122 report_outcome(ctx, &outcome, "cleared")
123 }
124 ReviewMarkAction::Diff(a) => {
125 let report = engine
126 .review_mark_diff(&a.mem, a.rename_similarity)
127 .map_err(CliError::from_engine_op)?;
128 if ctx.json {
129 print_json(&report)?;
130 return Ok(());
131 }
132 let mut lines = vec![
133 format!(
134 "# Unreviewed changes in `{}` since mark `{}`",
135 report.mem, report.since
136 ),
137 String::new(),
138 format!("- HEAD: `{}`", report.head),
139 format!("- Changes: {}", report.changes.len()),
140 String::new(),
141 ];
142 if report.changes.is_empty() {
143 lines.push("_head is at the mark — nothing unreviewed_".to_string());
144 } else {
145 for change in &report.changes {
146 use memstead_base::ChangeEnvelope::*;
147 let type_suffix = |t: &Option<String>| {
148 t.as_ref().map(|s| format!(" [{s}]")).unwrap_or_default()
149 };
150 let title_suffix = |t: &Option<String>| {
151 t.as_ref().map(|s| format!(" — {s}")).unwrap_or_default()
152 };
153 lines.push(match change {
154 Added {
155 id,
156 title,
157 entity_type,
158 } => format!(
159 "- **added** `{id}`{}{}",
160 type_suffix(entity_type),
161 title_suffix(title)
162 ),
163 Updated {
164 id,
165 title,
166 entity_type,
167 } => format!(
168 "- **updated** `{id}`{}{}",
169 type_suffix(entity_type),
170 title_suffix(title)
171 ),
172 Removed {
173 id,
174 title,
175 entity_type,
176 } => format!(
177 "- **removed** `{id}`{}{}",
178 type_suffix(entity_type),
179 title_suffix(title)
180 ),
181 Renamed {
182 from_id,
183 to_id,
184 title,
185 entity_type,
186 } => format!(
187 "- **renamed** `{from_id}` → `{to_id}`{}{}",
188 type_suffix(entity_type),
189 title_suffix(title)
190 ),
191 });
192 }
193 }
194 print_markdown(&lines.join("\n"));
195 Ok(())
196 }
197 }
198}
199
200fn report_outcome(
201 ctx: &CliContext,
202 outcome: &memstead_base::SetReviewMarkOutcome,
203 verb: &str,
204) -> anyhow::Result<()> {
205 if ctx.json {
206 print_json(outcome)?;
207 return Ok(());
208 }
209 let mut lines = vec![match outcome.mark.as_deref() {
210 Some(mark) => format!("Review mark {verb} on `{}`: `{mark}`", outcome.mem),
211 None => format!("Review mark {verb} on `{}`", outcome.mem),
212 }];
213 if let Some(prev) = outcome.previous.as_deref() {
214 lines.push(format!("- previous: `{prev}`"));
215 }
216 for w in &outcome.warnings {
217 lines.push(format!("- warning: {w}"));
218 }
219 print_markdown(&lines.join("\n"));
220 Ok(())
221}