git_x/
health.rs

1use crate::command::Command;
2use crate::{GitXError, Result};
3use console::Style;
4use std::process::Command as StdCommand;
5
6pub fn run() -> Result<String> {
7    let cmd = HealthCommand;
8    cmd.execute(())
9}
10
11/// Command implementation for git health
12pub struct HealthCommand;
13
14impl Command for HealthCommand {
15    type Input = ();
16    type Output = String;
17
18    fn execute(&self, _input: ()) -> Result<String> {
19        run_health()
20    }
21
22    fn name(&self) -> &'static str {
23        "health"
24    }
25
26    fn description(&self) -> &'static str {
27        "Check repository health and show diagnostic information"
28    }
29}
30
31fn run_health() -> Result<String> {
32    let bold = Style::new().bold();
33    let green = Style::new().green().bold();
34    let yellow = Style::new().yellow().bold();
35    let red = Style::new().red().bold();
36
37    let mut output = Vec::new();
38
39    output.push(format!("{}", bold.apply_to("Repository Health Check")));
40    output.push(format!("{}", bold.apply_to("=========================")));
41    output.push(String::new());
42
43    // Check if we're in a git repository
44    if !is_git_repo(&std::env::current_dir().unwrap_or_else(|_| ".".into())) {
45        output.push(format!("{} Not in a Git repository", red.apply_to("✗")));
46        return Ok(output.join("\n"));
47    }
48
49    // 1. Check repository status
50    output.push(check_repo_status(&green, &yellow, &red)?);
51
52    // 2. Check for untracked files
53    output.push(check_untracked_files(&green, &yellow, &red)?);
54
55    // 3. Check for stale branches
56    output.push(check_stale_branches(&green, &yellow, &red)?);
57
58    // 4. Check repository size
59    output.push(check_repo_size(&green, &yellow, &red)?);
60
61    // 5. Check for uncommitted changes
62    output.push(check_uncommitted_changes(&green, &yellow, &red)?);
63
64    output.push(String::new());
65    output.push(format!("{}", bold.apply_to("Health check complete!")));
66
67    Ok(output.join("\n"))
68}
69
70pub fn is_git_repo(path: &std::path::Path) -> bool {
71    StdCommand::new("git")
72        .args(["rev-parse", "--git-dir"])
73        .current_dir(path)
74        .output()
75        .map(|output| output.status.success())
76        .unwrap_or(false)
77}
78
79fn check_repo_status(green: &Style, _yellow: &Style, red: &Style) -> Result<String> {
80    let output = StdCommand::new("git")
81        .args(["status", "--porcelain"])
82        .output()
83        .map_err(|_| GitXError::GitCommand("Failed to run git status".to_string()))?;
84
85    if !output.status.success() {
86        return Err(GitXError::GitCommand(
87            "Failed to get repository status".to_string(),
88        ));
89    }
90
91    let status_output = String::from_utf8_lossy(&output.stdout);
92
93    if status_output.trim().is_empty() {
94        Ok(format!(
95            "{} Working directory is clean",
96            green.apply_to("✓")
97        ))
98    } else {
99        Ok(format!(
100            "{} Working directory has changes",
101            red.apply_to("✗")
102        ))
103    }
104}
105
106fn check_untracked_files(green: &Style, yellow: &Style, _red: &Style) -> Result<String> {
107    let output = StdCommand::new("git")
108        .args(["ls-files", "--others", "--exclude-standard"])
109        .output()
110        .map_err(|_| GitXError::GitCommand("Failed to list untracked files".to_string()))?;
111
112    if !output.status.success() {
113        return Err(GitXError::GitCommand(
114            "Failed to get untracked files".to_string(),
115        ));
116    }
117
118    let untracked = String::from_utf8_lossy(&output.stdout);
119    let untracked_files: Vec<&str> = untracked.lines().collect();
120
121    if untracked_files.is_empty() {
122        Ok(format!("{} No untracked files", green.apply_to("✓")))
123    } else {
124        Ok(format!(
125            "{} {} untracked files found",
126            yellow.apply_to("!"),
127            untracked_files.len()
128        ))
129    }
130}
131
132fn check_stale_branches(green: &Style, yellow: &Style, _red: &Style) -> Result<String> {
133    let output = StdCommand::new("git")
134        .args([
135            "for-each-ref",
136            "--format=%(refname:short) %(committerdate:relative)",
137            "refs/heads/",
138        ])
139        .output()
140        .map_err(|_| GitXError::GitCommand("Failed to list branches".to_string()))?;
141
142    if !output.status.success() {
143        return Err(GitXError::GitCommand(
144            "Failed to get branch information".to_string(),
145        ));
146    }
147
148    let branches = String::from_utf8_lossy(&output.stdout);
149    let mut stale_count = 0;
150
151    for line in branches.lines() {
152        if line.contains("months ago") || line.contains("year") {
153            stale_count += 1;
154        }
155    }
156
157    if stale_count == 0 {
158        Ok(format!(
159            "{} No stale branches (older than 1 month)",
160            green.apply_to("✓")
161        ))
162    } else {
163        Ok(format!(
164            "{} {} potentially stale branches found",
165            yellow.apply_to("!"),
166            stale_count
167        ))
168    }
169}
170
171fn check_repo_size(green: &Style, yellow: &Style, red: &Style) -> Result<String> {
172    let output = StdCommand::new("du")
173        .args(["-sh", ".git"])
174        .output()
175        .map_err(|_| GitXError::GitCommand("Failed to check repository size".to_string()))?;
176
177    if !output.status.success() {
178        return Err(GitXError::GitCommand(
179            "Failed to get repository size".to_string(),
180        ));
181    }
182
183    let size_output = String::from_utf8_lossy(&output.stdout);
184    let size = size_output.split_whitespace().next().unwrap_or("unknown");
185
186    // Simple heuristic for repository size warnings
187    if size.ends_with('K')
188        || (size.ends_with('M') && size.chars().next().unwrap_or('0').to_digit(10).unwrap_or(0) < 5)
189    {
190        Ok(format!(
191            "{} Repository size: {} (healthy)",
192            green.apply_to("✓"),
193            size
194        ))
195    } else if size.ends_with('M')
196        || (size.ends_with('G') && size.chars().next().unwrap_or('0').to_digit(10).unwrap_or(0) < 1)
197    {
198        Ok(format!(
199            "{} Repository size: {} (moderate)",
200            yellow.apply_to("!"),
201            size
202        ))
203    } else {
204        Ok(format!(
205            "{} Repository size: {} (large - consider cleanup)",
206            red.apply_to("✗"),
207            size
208        ))
209    }
210}
211
212fn check_uncommitted_changes(green: &Style, yellow: &Style, _red: &Style) -> Result<String> {
213    let output = StdCommand::new("git")
214        .args(["diff", "--cached", "--name-only"])
215        .output()
216        .map_err(|_| GitXError::GitCommand("Failed to check staged changes".to_string()))?;
217
218    if !output.status.success() {
219        return Err(GitXError::GitCommand(
220            "Failed to get staged changes".to_string(),
221        ));
222    }
223
224    let staged = String::from_utf8_lossy(&output.stdout);
225    let staged_files: Vec<&str> = staged.lines().filter(|line| !line.is_empty()).collect();
226
227    if staged_files.is_empty() {
228        Ok(format!("{} No staged changes", green.apply_to("✓")))
229    } else {
230        Ok(format!(
231            "{} {} files staged for commit",
232            yellow.apply_to("!"),
233            staged_files.len()
234        ))
235    }
236}