Skip to main content

gn_cli/commands/
validate.rs

1use anyhow::Result;
2use clap::Args;
3use gn_core::{DataValidator, Namespace};
4use std::env;
5
6#[derive(Args, Debug)]
7pub struct ValidateArgs {
8    /// Namespace to validate (comments, review, todos, or custom)
9    #[arg(short, long)]
10    pub namespace: Option<String>,
11
12    /// Fail with exit code 1 if any orphan notes (anchoring to missing commits) are found
13    #[arg(long, default_value_t = false)]
14    pub strict: bool,
15
16    /// Output results as structured JSON
17    #[arg(long, default_value_t = false)]
18    pub json: bool,
19}
20
21pub fn run(args: &ValidateArgs) -> Result<()> {
22    let repo_path = env::current_dir()?;
23    let validator = DataValidator::new(&repo_path);
24
25    let namespaces = if let Some(ns_str) = &args.namespace {
26        vec![Namespace::from_str(ns_str)]
27    } else {
28        vec![
29            Namespace::Comments,
30            Namespace::Review,
31            Namespace::Todos,
32        ]
33    };
34
35    let report = validator.validate_all(&namespaces)?;
36
37    if args.json {
38        println!("{}", serde_json::to_string_pretty(&report)?);
39        if !report.healthy || (args.strict && report.total_orphans > 0) {
40            std::process::exit(1);
41        }
42        return Ok(());
43    }
44
45    println!("\x1b[1;36m═══════════════════════════════════════════════════════════════════════\x1b[0m");
46    println!("\x1b[1;36m           git-notes Data & Refspec Integrity Validator (fsck)          \x1b[0m");
47    println!("\x1b[1;36m═══════════════════════════════════════════════════════════════════════\x1b[0m");
48    println!("Repository: \x1b[1m{}\x1b[0m\n", report.repo_root);
49
50    for ref_rep in &report.ref_reports {
51        if !ref_rep.exists {
52            println!("  \x1b[90m○ {:<24} (ref not yet created / 0 notes)\x1b[0m", ref_rep.ref_path);
53            continue;
54        }
55
56        let status_badge = if ref_rep.corrupt_blobs == 0 && ref_rep.unparseable_notes == 0 {
57            if ref_rep.orphan_notes == 0 {
58                "\x1b[32m✔ OK\x1b[0m"
59            } else {
60                "\x1b[33m⚠ ORPHANS\x1b[0m"
61            }
62        } else {
63            "\x1b[31m✗ CORRUPT\x1b[0m"
64        };
65
66        println!(
67            "  [{}] \x1b[1m{:<24}\x1b[0m (commit: \x1b[90m{}\x1b[0m | {} note{})",
68            status_badge,
69            ref_rep.ref_path,
70            ref_rep.commit_sha.as_deref().unwrap_or("none").chars().take(8).collect::<String>(),
71            ref_rep.note_count,
72            if ref_rep.note_count == 1 { "" } else { "s" }
73        );
74
75        for issue in &ref_rep.issues {
76            if issue.contains("unreachable") || issue.contains("missing commit") {
77                println!("      \x1b[33m⚠ {}\x1b[0m", issue);
78            } else {
79                println!("      \x1b[31m✗ {}\x1b[0m", issue);
80            }
81        }
82    }
83
84    println!("\n\x1b[1;36m───────────────────────────────────────────────────────────────────────\x1b[0m");
85    println!(
86        "Summary: {} note(s) scanned across {} ref(s). Corrupt: {}, Orphans: {}",
87        report.total_notes,
88        report.total_refs,
89        report.total_corrupt,
90        report.total_orphans
91    );
92
93    if !report.healthy {
94        println!("\x1b[1;31m✗ Data corruption detected in git note references!\x1b[0m");
95        anyhow::bail!("Data validation failed: {} corrupt blob(s) detected.", report.total_corrupt);
96    } else if report.total_orphans > 0 {
97        println!("\x1b[33m💡 Tip: Run 'gn heal' to automatically re-anchor orphan notes after rebases.\x1b[0m");
98        if args.strict {
99            anyhow::bail!("Strict mode: {} orphan note(s) found.", report.total_orphans);
100        }
101    } else {
102        println!("\x1b[1;32m✔ All git-notes refs and JSON schemas are 100% valid and verified.\x1b[0m");
103    }
104
105    Ok(())
106}