git_x/
health.rs

1use console::Style;
2use std::process::Command;
3
4pub fn run() {
5    let bold = Style::new().bold();
6    let green = Style::new().green().bold();
7    let yellow = Style::new().yellow().bold();
8    let red = Style::new().red().bold();
9
10    println!("{}", bold.apply_to("Repository Health Check"));
11    println!("{}", bold.apply_to("========================="));
12    println!();
13
14    // Check if we're in a git repository
15    if !is_git_repo(&std::env::current_dir().unwrap_or_else(|_| ".".into())) {
16        println!("{} Not in a Git repository", red.apply_to("✗"));
17        return;
18    }
19
20    // 1. Check repository status
21    check_repo_status(&green, &yellow, &red);
22
23    // 2. Check for untracked files
24    check_untracked_files(&green, &yellow, &red);
25
26    // 3. Check for stale branches
27    check_stale_branches(&green, &yellow, &red);
28
29    // 4. Check repository size
30    check_repo_size(&green, &yellow, &red);
31
32    // 5. Check for uncommitted changes
33    check_uncommitted_changes(&green, &yellow, &red);
34
35    println!();
36    println!("{}", bold.apply_to("Health check complete!"));
37}
38
39pub fn is_git_repo(path: &std::path::Path) -> bool {
40    Command::new("git")
41        .args(["rev-parse", "--git-dir"])
42        .current_dir(path)
43        .output()
44        .map(|output| output.status.success())
45        .unwrap_or(false)
46}
47
48fn check_repo_status(green: &Style, _yellow: &Style, red: &Style) {
49    let output = Command::new("git")
50        .args(["status", "--porcelain"])
51        .output()
52        .expect("Failed to run git status");
53
54    let status_output = String::from_utf8_lossy(&output.stdout);
55
56    if status_output.trim().is_empty() {
57        println!("{} Working directory is clean", green.apply_to("✓"));
58    } else {
59        println!("{} Working directory has changes", red.apply_to("✗"));
60    }
61}
62
63fn check_untracked_files(green: &Style, yellow: &Style, _red: &Style) {
64    let output = Command::new("git")
65        .args(["ls-files", "--others", "--exclude-standard"])
66        .output()
67        .expect("Failed to list untracked files");
68
69    let untracked = String::from_utf8_lossy(&output.stdout);
70    let untracked_files: Vec<&str> = untracked.lines().collect();
71
72    if untracked_files.is_empty() {
73        println!("{} No untracked files", green.apply_to("✓"));
74    } else {
75        println!(
76            "{} {} untracked files found",
77            yellow.apply_to("!"),
78            untracked_files.len()
79        );
80    }
81}
82
83fn check_stale_branches(green: &Style, yellow: &Style, _red: &Style) {
84    let output = Command::new("git")
85        .args([
86            "for-each-ref",
87            "--format=%(refname:short) %(committerdate:relative)",
88            "refs/heads/",
89        ])
90        .output()
91        .expect("Failed to list branches");
92
93    let branches = String::from_utf8_lossy(&output.stdout);
94    let mut stale_count = 0;
95
96    for line in branches.lines() {
97        if line.contains("months ago") || line.contains("year") {
98            stale_count += 1;
99        }
100    }
101
102    if stale_count == 0 {
103        println!(
104            "{} No stale branches (older than 1 month)",
105            green.apply_to("✓")
106        );
107    } else {
108        println!(
109            "{} {} potentially stale branches found",
110            yellow.apply_to("!"),
111            stale_count
112        );
113    }
114}
115
116fn check_repo_size(green: &Style, yellow: &Style, red: &Style) {
117    let output = Command::new("du")
118        .args(["-sh", ".git"])
119        .output()
120        .expect("Failed to check repository size");
121
122    let size_output = String::from_utf8_lossy(&output.stdout);
123    let size = size_output.split_whitespace().next().unwrap_or("unknown");
124
125    // Simple heuristic for repository size warnings
126    if size.ends_with('K')
127        || (size.ends_with('M') && size.chars().next().unwrap_or('0').to_digit(10).unwrap_or(0) < 5)
128    {
129        println!(
130            "{} Repository size: {} (healthy)",
131            green.apply_to("✓"),
132            size
133        );
134    } else if size.ends_with('M')
135        || (size.ends_with('G') && size.chars().next().unwrap_or('0').to_digit(10).unwrap_or(0) < 1)
136    {
137        println!(
138            "{} Repository size: {} (moderate)",
139            yellow.apply_to("!"),
140            size
141        );
142    } else {
143        println!(
144            "{} Repository size: {} (large - consider cleanup)",
145            red.apply_to("✗"),
146            size
147        );
148    }
149}
150
151fn check_uncommitted_changes(green: &Style, yellow: &Style, _red: &Style) {
152    let output = Command::new("git")
153        .args(["diff", "--cached", "--name-only"])
154        .output()
155        .expect("Failed to check staged changes");
156
157    let staged = String::from_utf8_lossy(&output.stdout);
158    let staged_files: Vec<&str> = staged.lines().filter(|line| !line.is_empty()).collect();
159
160    if staged_files.is_empty() {
161        println!("{} No staged changes", green.apply_to("✓"));
162    } else {
163        println!(
164            "{} {} files staged for commit",
165            yellow.apply_to("!"),
166            staged_files.len()
167        );
168    }
169}