1use clap::Args;
2use owo_colors::OwoColorize;
3
4use chub_core::annotations::{
5 clear_annotation, list_annotations, read_annotation, write_annotation, AnnotationKind,
6};
7
8use crate::output;
9
10#[derive(Args)]
11pub struct AnnotateArgs {
12 id: Option<String>,
14
15 note: Option<String>,
17
18 #[arg(long)]
20 clear: bool,
21
22 #[arg(long)]
24 list: bool,
25
26 #[arg(long)]
28 team: bool,
29
30 #[arg(long)]
32 personal: bool,
33
34 #[arg(long)]
36 org: bool,
37
38 #[arg(long)]
40 author: Option<String>,
41
42 #[arg(long, value_name = "KIND")]
44 kind: Option<String>,
45
46 #[arg(long, value_name = "LEVEL")]
48 severity: Option<String>,
49}
50
51fn parse_kind(s: Option<&str>) -> AnnotationKind {
52 s.and_then(AnnotationKind::parse)
53 .unwrap_or(AnnotationKind::Note)
54}
55
56fn get_author(explicit: Option<&str>) -> String {
57 explicit.map(|s| s.to_string()).unwrap_or_else(|| {
58 std::env::var("USER")
59 .or_else(|_| std::env::var("USERNAME"))
60 .unwrap_or_else(|_| "unknown".to_string())
61 })
62}
63
64fn print_team_annotation_list(annotations: &[chub_core::team::team_annotations::TeamAnnotation]) {
65 for a in annotations {
66 eprintln!("{}", a.id.bold());
67 if !a.issues.is_empty() {
68 eprintln!(" {}", "Issues:".yellow());
69 for note in &a.issues {
70 let sev = note
71 .severity
72 .as_deref()
73 .map(|s| format!(" [{}]", s))
74 .unwrap_or_default();
75 eprintln!(
76 " {} {}{} {}",
77 note.author.cyan(),
78 format!("({})", note.date).dimmed(),
79 sev.yellow(),
80 note.note
81 );
82 }
83 }
84 if !a.fixes.is_empty() {
85 eprintln!(" {}", "Fixes:".green());
86 for note in &a.fixes {
87 eprintln!(
88 " {} {} {}",
89 note.author.cyan(),
90 format!("({})", note.date).dimmed(),
91 note.note
92 );
93 }
94 }
95 if !a.practices.is_empty() {
96 eprintln!(" {}", "Practices:".blue());
97 for note in &a.practices {
98 eprintln!(
99 " {} {} {}",
100 note.author.cyan(),
101 format!("({})", note.date).dimmed(),
102 note.note
103 );
104 }
105 }
106 if !a.notes.is_empty() {
107 eprintln!(" {}", "Notes:".dimmed());
108 for note in &a.notes {
109 eprintln!(
110 " {} {} {}",
111 note.author.cyan(),
112 format!("({})", note.date).dimmed(),
113 note.note
114 );
115 }
116 }
117 eprintln!();
118 }
119}
120
121fn print_team_annotation_single(id: &str, ann: &chub_core::team::team_annotations::TeamAnnotation) {
122 eprintln!("{}", id.bold());
123 if !ann.issues.is_empty() {
124 eprintln!(" {}", "Issues:".yellow());
125 for note in &ann.issues {
126 let sev = note
127 .severity
128 .as_deref()
129 .map(|s| format!(" [{}]", s))
130 .unwrap_or_default();
131 eprintln!(
132 " {} {}{} {}",
133 note.author.cyan(),
134 format!("({})", note.date).dimmed(),
135 sev.yellow(),
136 note.note
137 );
138 }
139 }
140 if !ann.fixes.is_empty() {
141 eprintln!(" {}", "Fixes:".green());
142 for note in &ann.fixes {
143 eprintln!(
144 " {} {} {}",
145 note.author.cyan(),
146 format!("({})", note.date).dimmed(),
147 note.note
148 );
149 }
150 }
151 if !ann.practices.is_empty() {
152 eprintln!(" {}", "Practices:".blue());
153 for note in &ann.practices {
154 eprintln!(
155 " {} {} {}",
156 note.author.cyan(),
157 format!("({})", note.date).dimmed(),
158 note.note
159 );
160 }
161 }
162 for note in &ann.notes {
163 eprintln!(
164 " {} {} {}",
165 note.author.cyan(),
166 format!("({})", note.date).dimmed(),
167 note.note
168 );
169 }
170}
171
172pub async fn run(args: AnnotateArgs, json: bool) {
173 if args.list {
174 if args.org {
175 let annotations = chub_core::team::org_annotations::list_org_annotations().await;
176 if json {
177 println!(
178 "{}",
179 serde_json::to_string_pretty(&annotations).unwrap_or_default()
180 );
181 } else {
182 if annotations.is_empty() {
183 eprintln!("No org annotations.");
184 return;
185 }
186 print_team_annotation_list(&annotations);
187 }
188 } else if args.team {
189 let annotations = chub_core::team::team_annotations::list_team_annotations();
191 if json {
192 println!(
193 "{}",
194 serde_json::to_string_pretty(&annotations).unwrap_or_default()
195 );
196 } else {
197 if annotations.is_empty() {
198 eprintln!("No team annotations.");
199 return;
200 }
201 print_team_annotation_list(&annotations);
202 }
203 } else {
204 let annotations = list_annotations();
205 if json {
206 println!(
207 "{}",
208 serde_json::to_string_pretty(&annotations).unwrap_or_default()
209 );
210 } else {
211 if annotations.is_empty() {
212 eprintln!("No annotations.");
213 return;
214 }
215 for a in &annotations {
216 eprintln!(
217 "{} {} [{}]",
218 a.id.bold(),
219 format!("({})", a.updated_at).dimmed(),
220 a.kind.as_str().cyan()
221 );
222 eprintln!(" {}", a.note);
223 eprintln!();
224 }
225 }
226 }
227 return;
228 }
229
230 let id = match args.id {
231 Some(id) => id,
232 None => {
233 output::error(
234 "Missing required argument: <id>. Run: chub annotate <id> <note> | chub annotate <id> --clear | chub annotate --list",
235 json,
236 );
237 std::process::exit(1);
238 }
239 };
240
241 if args.clear {
242 let (scope, removed) = if args.org {
243 match chub_core::team::org_annotations::clear_org_annotation(&id).await {
244 Ok(r) => ("org", r),
245 Err(e) => {
246 output::error(&format!("Failed to clear org annotation: {}", e), json);
247 std::process::exit(1);
248 }
249 }
250 } else if args.team {
251 (
252 "team",
253 chub_core::team::team_annotations::clear_team_annotation(&id),
254 )
255 } else {
256 ("personal", clear_annotation(&id))
257 };
258
259 if json {
260 println!(
261 "{}",
262 serde_json::json!({
263 "id": id,
264 "cleared": removed,
265 "scope": scope,
266 })
267 );
268 } else if removed {
269 eprintln!("{} annotation cleared for {}.", scope, id.bold());
270 } else {
271 eprintln!("No {} annotation found for {}.", scope, id.bold());
272 }
273 return;
274 }
275
276 if let Some(note) = args.note {
277 let kind = parse_kind(args.kind.as_deref());
278
279 if args.org {
280 let author = get_author(args.author.as_deref());
281 match chub_core::team::org_annotations::write_org_annotation(
282 &id,
283 ¬e,
284 &author,
285 kind.clone(),
286 args.severity.clone(),
287 )
288 .await
289 {
290 Ok(_) => {
291 if json {
292 println!(
293 "{}",
294 serde_json::json!({
295 "status": "saved",
296 "id": id,
297 "scope": "org",
298 "kind": kind.as_str(),
299 "author": author,
300 })
301 );
302 } else {
303 output::success(&format!(
304 "Org {} saved for {} (by {})",
305 kind.as_str(),
306 id.bold(),
307 author
308 ));
309 }
310 }
311 Err(e) => {
312 output::error(&format!("Failed to write org annotation: {}", e), json);
313 std::process::exit(1);
314 }
315 }
316 } else if args.team {
317 let author = get_author(args.author.as_deref());
319 match chub_core::team::team_annotations::write_team_annotation(
320 &id,
321 ¬e,
322 &author,
323 kind.clone(),
324 args.severity.clone(),
325 ) {
326 Some(_ann) => {
327 if json {
328 println!(
329 "{}",
330 serde_json::json!({
331 "status": "saved",
332 "id": id,
333 "scope": "team",
334 "kind": kind.as_str(),
335 "author": author,
336 })
337 );
338 } else {
339 output::success(&format!(
340 "Team {} saved for {} (by {})",
341 kind.as_str(),
342 id.bold(),
343 author
344 ));
345 }
346 let auto_push =
348 chub_core::team::org_annotations::get_annotation_server_config()
349 .map(|c| c.auto_push)
350 .unwrap_or(false);
351 if auto_push {
352 let _ = chub_core::team::org_annotations::write_org_annotation(
353 &id,
354 ¬e,
355 &author,
356 kind.clone(),
357 args.severity.clone(),
358 )
359 .await;
360 }
361 }
362 None => {
363 output::error(
364 "Failed to save team annotation. Is .chub/ initialized?",
365 json,
366 );
367 std::process::exit(1);
368 }
369 }
370 } else {
371 let data = write_annotation(&id, ¬e, kind.clone(), args.severity.clone());
373 if json {
374 println!(
375 "{}",
376 serde_json::to_string_pretty(&data).unwrap_or_default()
377 );
378 } else {
379 eprintln!("{} saved for {}.", kind.as_str().cyan(), id.bold());
380 }
381 }
382 return;
383 }
384
385 if args.org {
387 if let Some(ann) = chub_core::team::org_annotations::read_org_annotation(&id).await {
388 if json {
389 println!("{}", serde_json::to_string_pretty(&ann).unwrap_or_default());
390 } else {
391 print_team_annotation_single(&id, &ann);
392 }
393 } else if json {
394 println!("{}", serde_json::json!({ "id": id, "notes": [] }));
395 } else {
396 eprintln!("No org annotation for {}.", id.bold());
397 }
398 } else if args.team {
399 if let Some(ann) = chub_core::team::team_annotations::read_team_annotation(&id) {
400 if json {
401 println!("{}", serde_json::to_string_pretty(&ann).unwrap_or_default());
402 } else {
403 print_team_annotation_single(&id, &ann);
404 }
405 } else if json {
406 println!("{}", serde_json::json!({ "id": id, "notes": [] }));
407 } else {
408 eprintln!("No team annotation for {}.", id.bold());
409 }
410 } else if args.personal {
411 let existing = read_annotation(&id);
413 if let Some(ann) = existing {
414 if json {
415 println!("{}", serde_json::to_string_pretty(&ann).unwrap_or_default());
416 } else {
417 eprintln!(
418 "{} {} [{}]",
419 ann.id.bold(),
420 format!("({})", ann.updated_at).dimmed(),
421 ann.kind.as_str().cyan()
422 );
423 eprintln!("{}", ann.note);
424 }
425 } else if json {
426 println!("{}", serde_json::json!({ "id": id, "note": null }));
427 } else {
428 eprintln!("No personal annotation for {}.", id.bold());
429 }
430 } else {
431 let merged = chub_core::team::team_annotations::get_merged_annotation_async(&id).await;
433 if let Some(text) = merged {
434 if json {
435 println!("{}", serde_json::json!({ "id": id, "annotation": text }));
436 } else {
437 eprintln!("{}", id.bold());
438 eprintln!("{}", text);
439 }
440 } else if json {
441 println!("{}", serde_json::json!({ "id": id, "annotation": null }));
442 } else {
443 eprintln!("No annotations for {}.", id.bold());
444 }
445 }
446}