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
11pub 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 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 output.push(check_repo_status(&green, &yellow, &red)?);
51
52 output.push(check_untracked_files(&green, &yellow, &red)?);
54
55 output.push(check_stale_branches(&green, &yellow, &red)?);
57
58 output.push(check_repo_size(&green, &yellow, &red)?);
60
61 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 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}