1use anyhow::{anyhow, Context, Result};
2use clap::Args;
3use gn_core::{Note, NotesEngine};
4use std::process::Command;
5
6#[derive(Args)]
7pub struct ReplyArgs {
8 pub id: Option<String>,
10
11 #[arg(short, long)]
13 pub message: String,
14
15 #[arg(short, long)]
17 pub sign: bool,
18}
19
20pub fn run(args: &ReplyArgs) -> Result<()> {
21 let engine = NotesEngine::new(".");
22 let namespaces = vec!["comments", "review", "todos"];
23
24 let mut all_notes = Vec::new();
25
26 for ns in &namespaces {
27 let namespace_enum = gn_core::Namespace::Custom(ns.to_string());
28 if let Ok(notes) = engine.read_notes(&namespace_enum) {
29 all_notes.extend(notes);
30 }
31 }
32
33 if all_notes.is_empty() {
34 return Err(anyhow!("No notes exist in the repository to reply to."));
35 }
36
37 let found_note = match &args.id {
39 None => match super::picker::pick_note("Select note to reply to:", &all_notes)? {
40 Some(n) => Some(n.clone()),
41 None => {
42 println!("Cancelled.");
43 return Ok(());
44 }
45 },
46 Some(raw_id) => {
47 let target = raw_id.trim().trim_start_matches('#');
48 if target.eq_ignore_ascii_case("latest") || target == "^" {
49 all_notes.last().cloned()
50 } else if let Ok(idx) = target.parse::<usize>() {
51 if idx >= 1 && idx <= all_notes.len() {
52 Some(all_notes[idx - 1].clone())
53 } else {
54 None
55 }
56 } else {
57 all_notes
58 .iter()
59 .find(|n| n.id.to_string().starts_with(target))
60 .cloned()
61 }
62 }
63 };
64
65 let parent_note =
66 found_note.ok_or_else(|| anyhow!("Target note not found"))?;
67
68 let name_output = Command::new("git")
69 .args(["config", "user.name"])
70 .output()
71 .context("Failed to read user.name")?;
72 let email_output = Command::new("git")
73 .args(["config", "user.email"])
74 .output()
75 .context("Failed to read user.email")?;
76
77 let author = format!(
78 "{} <{}>",
79 String::from_utf8_lossy(&name_output.stdout).trim(),
80 String::from_utf8_lossy(&email_output.stdout).trim()
81 );
82
83 let mut reply = Note::reply(&parent_note, args.message.clone(), author);
84
85 if super::signing::is_signing_requested(args.sign) {
86 let payload = reply.signing_payload();
87 let sig = super::signing::sign_payload(&payload)
88 .context("Failed to cryptographically sign reply note")?;
89 reply.signature = Some(sig);
90 }
91
92 let id = engine.write_note(&reply)?;
93 if reply.signature.is_some() {
94 println!(
95 "\x1b[32m✔\x1b[0m Reply (signed) added to thread \x1b[36m{}\x1b[0m (Note ID: \x1b[36m{}\x1b[0m)",
96 &parent_note.id.to_string()[..8],
97 &id[..8.min(id.len())]
98 );
99 } else {
100 println!(
101 "\x1b[32m✔\x1b[0m Reply added to thread \x1b[36m{}\x1b[0m (Note ID: \x1b[36m{}\x1b[0m)",
102 &parent_note.id.to_string()[..8],
103 &id[..8.min(id.len())]
104 );
105 }
106
107 Ok(())
108}