Skip to main content

gn_cli/commands/
doctor.rs

1use anyhow::Result;
2use std::path::Path;
3use std::process::Command;
4
5pub fn run() -> Result<()> {
6    println!("\n\x1b[1;36m🩺 git-notes doctor\x1b[0m");
7    println!("Checking repository configuration and health...\n");
8
9    let mut errors = 0;
10    let mut warnings = 0;
11
12    // 1. Git Repository Check
13    let git_root = match Command::new("git").args(["rev-parse", "--show-toplevel"]).output() {
14        Ok(out) if out.status.success() => {
15            let root = String::from_utf8_lossy(&out.stdout).trim().to_string();
16            println!("  \x1b[32m✓\x1b[0m Git repository: {}", root);
17            Some(root)
18        }
19        _ => {
20            println!("  \x1b[31m✗\x1b[0m Git repository: not inside a valid git repo");
21            errors += 1;
22            None
23        }
24    };
25
26    // 2. Binary Version Check
27    let current_version = env!("CARGO_PKG_VERSION");
28    println!("  \x1b[32m✓\x1b[0m Binary version: v{}", current_version);
29
30    // 3. Notes Refs Namespaces Check
31    let namespaces = ["comments", "review", "todos"];
32    for ns in &namespaces {
33        let ref_path = format!("refs/notes/{}", ns);
34        let check_ref = Command::new("git")
35            .args(["for-each-ref", &ref_path, "--format=%(objectname)"])
36            .output();
37
38        match check_ref {
39            Ok(out) if out.status.success() && !out.stdout.is_empty() => {
40                let note_count = Command::new("git")
41                    .args(["notes", "--ref", &ref_path, "list"])
42                    .output()
43                    .map(|o| String::from_utf8_lossy(&o.stdout).lines().count())
44                    .unwrap_or(0);
45                println!("  \x1b[32m✓\x1b[0m {}: exists ({} note(s))", ref_path, note_count);
46            }
47            _ => {
48                println!(
49                    "  \x1b[33m⚠\x1b[0m {}: not initialized yet (run: git-notes sync pull)",
50                    ref_path
51                );
52                warnings += 1;
53            }
54        }
55    }
56
57    // 4. Remote Fetch Refspec Check
58    let remote_config = Command::new("git")
59        .args(["config", "--get-all", "remote.origin.fetch"])
60        .output();
61
62    let has_notes_fetch = match remote_config {
63        Ok(out) if out.status.success() => {
64            String::from_utf8_lossy(&out.stdout).contains("refs/notes/*")
65        }
66        _ => false,
67    };
68
69    if has_notes_fetch {
70        println!("  \x1b[32m✓\x1b[0m Remote fetch refspec: configured for refs/notes/*");
71    } else {
72        println!("  \x1b[33m⚠\x1b[0m Remote fetch refspec: missing '+refs/notes/*:refs/notes/*'");
73        println!("    \x1b[90mFix with: git config --add remote.origin.fetch '+refs/notes/*:refs/notes/*'\x1b[0m");
74        warnings += 1;
75    }
76
77    // 5. Hooks Status Check
78    if let Some(ref root) = git_root {
79        let hooks_dir = Path::new(root).join(".git").join("hooks");
80        let post_merge = hooks_dir.join("post-merge");
81        let pre_push = hooks_dir.join("pre-push");
82
83        if post_merge.exists() {
84            println!("  \x1b[32m✓\x1b[0m post-merge hook: installed");
85        } else {
86            println!("  \x1b[33m⚠\x1b[0m post-merge hook: not installed (run: git-notes-hooks install)");
87            warnings += 1;
88        }
89
90        if pre_push.exists() {
91            println!("  \x1b[32m✓\x1b[0m pre-push hook: installed");
92        } else {
93            println!("  \x1b[33m⚠\x1b[0m pre-push hook: not installed");
94            warnings += 1;
95        }
96
97        // 6. Stale Notes Check
98        let mut stale_count = 0;
99        let notes_out = Command::new("git")
100            .args(["notes", "--ref", "refs/notes/comments", "list"])
101            .output();
102
103        if let Ok(out) = notes_out {
104            for line in String::from_utf8_lossy(&out.stdout).lines() {
105                if let Some(blob) = line.split_whitespace().next() {
106                    if let Ok(cat) = Command::new("git").args(["cat-file", "blob", blob]).output() {
107                        if let Ok(note) = serde_json::from_slice::<serde_json::Value>(&cat.stdout) {
108                            if let Some(rel_file) = note.get("file").and_then(|f| f.as_str()) {
109                                if !rel_file.is_empty() && !Path::new(root).join(rel_file).exists() {
110                                    stale_count += 1;
111                                    println!(
112                                        "  \x1b[33m⚠\x1b[0m Stale note {}: referenced file '{}' no longer exists",
113                                        &blob[..8.min(blob.len())],
114                                        rel_file
115                                    );
116                                }
117                            }
118                        }
119                    }
120                }
121            }
122        }
123
124        if stale_count == 0 {
125            println!("  \x1b[32m✓\x1b[0m Stale notes: all referenced files exist in working tree");
126        } else {
127            warnings += stale_count;
128        }
129    }
130
131    // Summary
132    println!();
133    if errors == 0 && warnings == 0 {
134        println!("\x1b[32m✨ All checks passed! git-notes is in peak health.\x1b[0m\n");
135    } else {
136        println!(
137            "\x1b[1mSummary: \x1b[31m{} error(s)\x1b[0m, \x1b[33m{} warning(s)\x1b[0m\n",
138            errors, warnings
139        );
140    }
141
142    println!("┌─────────────────────────────────────────────────────────────┐");
143    println!("│ \x1b[1;36m💬 Enjoying git-notes? Help us grow!\x1b[0m                        │");
144    println!("│ \x1b[33m⭐ Star on GitHub:\x1b[0m   https://github.com/isaim0011/git-notes   │");
145    println!("│ \x1b[35m★ Review on Open VSX:\x1b[0m https://open-vsx.org/extension/isaim0011/vscode-git-notes │");
146    println!("└─────────────────────────────────────────────────────────────┘\n");
147
148    Ok(())
149}