Skip to main content

gn_cli/commands/
add.rs

1use anyhow::{Context, Result};
2use clap::Args;
3use gn_core::{Note, NotesEngine};
4use std::process::Command;
5
6#[derive(Args)]
7pub struct AddArgs {
8    /// File to attach the note to
9    #[arg(short, long)]
10    pub file: String,
11
12    /// Line range (e.g., "10" or "10-15")
13    #[arg(short, long)]
14    pub line: String,
15
16    /// Note message
17    #[arg(short, long)]
18    pub message: String,
19
20    /// Namespace to add the note to
21    #[arg(short, long, default_value = "comments")]
22    pub namespace: String,
23
24    /// Thread parent note ID (for replies)
25    #[arg(short, long)]
26    pub thread: Option<String>,
27
28    /// Cryptographically sign the note with GPG or SSH key
29    #[arg(short, long)]
30    pub sign: bool,
31}
32
33pub fn run(args: &AddArgs) -> Result<()> {
34    let name_output = Command::new("git")
35        .args(["config", "user.name"])
36        .output()
37        .context("Failed to read user.name")?;
38    let email_output = Command::new("git")
39        .args(["config", "user.email"])
40        .output()
41        .context("Failed to read user.email")?;
42
43    let author = format!(
44        "{} <{}>",
45        String::from_utf8_lossy(&name_output.stdout).trim(),
46        String::from_utf8_lossy(&email_output.stdout).trim()
47    );
48
49    let head_output = Command::new("git")
50        .args(["rev-parse", "HEAD"])
51        .output()
52        .context("Failed to get HEAD commit")?;
53    let commit = String::from_utf8_lossy(&head_output.stdout)
54        .trim()
55        .to_string();
56
57    let mut line_start = None;
58    let mut line_end = None;
59    if !args.line.is_empty() {
60        if args.line.contains('-') {
61            let parts: Vec<&str> = args.line.split('-').collect();
62            if parts.len() == 2 {
63                line_start = parts[0].parse().ok();
64                line_end = parts[1].parse().ok();
65            }
66        } else {
67            line_start = args.line.parse().ok();
68            line_end = line_start;
69        }
70    }
71
72    let namespace = gn_core::Namespace::Custom(args.namespace.clone());
73
74    let mut note = Note::new(
75        commit,
76        Some(args.file.clone()),
77        line_start,
78        line_end,
79        args.message.clone(),
80        author,
81        namespace,
82    );
83
84    if let Some(thread_str) = &args.thread {
85        if let Ok(tid) = uuid::Uuid::parse_str(thread_str) {
86            note.thread_id = Some(tid);
87        }
88    }
89
90    if super::signing::is_signing_requested(args.sign) {
91        let payload = note.signing_payload();
92        let sig = super::signing::sign_payload(&payload)
93            .context("Failed to cryptographically sign note")?;
94        note.signature = Some(sig);
95    }
96
97    let engine = NotesEngine::new(".");
98    engine.write_note(&note)?;
99
100    if note.signature.is_some() {
101        println!("✓ Note {} (signed) added to refs/notes/{}", note.id, args.namespace);
102    } else {
103        println!("✓ Note {} added to refs/notes/{}", note.id, args.namespace);
104    }
105
106    Ok(())
107}