Skip to main content

gn_cli/commands/
blame.rs

1use anyhow::Result;
2use clap::Args;
3use gn_core::namespace::Namespace;
4use gn_core::NotesEngine;
5use std::collections::HashMap;
6use std::process::Command;
7
8#[derive(Args, Debug)]
9pub struct BlameArgs {
10    /// File to blame
11    #[arg(short, long)]
12    pub file: String,
13
14    /// Namespace to check notes from (default: all)
15    #[arg(short, long, default_value = "comments")]
16    pub namespace: String,
17}
18
19pub fn run(args: &BlameArgs) -> Result<()> {
20    let engine = NotesEngine::new(".");
21    let ns = Namespace::from_str(&args.namespace);
22
23    // Read notes for this namespace
24    let notes = engine.read_notes(&ns).unwrap_or_default();
25
26    // Index notes by line start
27    let mut notes_by_line: HashMap<u32, Vec<&gn_core::note::Note>> = HashMap::new();
28    for note in &notes {
29        if let Some(ref note_file) = note.file {
30            if note_file == &args.file {
31                let line_num = note.line_start.unwrap_or(1);
32                notes_by_line.entry(line_num).or_default().push(note);
33            }
34        }
35    }
36
37    // Run git blame
38    let blame_output = Command::new("git")
39        .args(["blame", &args.file])
40        .output()?;
41
42    if !blame_output.status.success() {
43        eprintln!("{}", String::from_utf8_lossy(&blame_output.stderr));
44        return Ok(());
45    }
46
47    let blame_text = String::from_utf8_lossy(&blame_output.stdout);
48    for (idx, line) in blame_text.lines().enumerate() {
49        let current_line_num = (idx + 1) as u32;
50        println!("{}", line);
51
52        if let Some(attached_notes) = notes_by_line.get(&current_line_num) {
53            for n in attached_notes {
54                let status_str = match n.status {
55                    gn_core::note::NoteStatus::Open => "\x1b[33m[Open]\x1b[0m",
56                    gn_core::note::NoteStatus::Approved => "\x1b[32m[Approved]\x1b[0m",
57                    gn_core::note::NoteStatus::Rejected => "\x1b[31m[Rejected]\x1b[0m",
58                    gn_core::note::NoteStatus::Resolved => "\x1b[32m[Resolved]\x1b[0m",
59                };
60                let id_short = &n.id.to_string()[..8];
61                println!(
62                    "          \x1b[36m╰─ 💬 [{} — {}] {}\x1b[0m {}",
63                    n.author, id_short, n.body.trim(), status_str
64                );
65            }
66        }
67    }
68
69    Ok(())
70}