git_worktree_manager/operations/
diagnostics.rs1use std::process::Command;
2
3use console::style;
4
5use crate::constants::{
6 format_config_key, version_meets_minimum, CONFIG_KEY_BASE_BRANCH, MIN_GIT_VERSION,
7 MIN_GIT_VERSION_MAJOR, MIN_GIT_VERSION_MINOR,
8};
9use crate::error::Result;
10use crate::git;
11
12use super::display::get_worktree_status;
13use super::pr_cache::PrCache;
14use super::setup_claude;
15
16struct WtInfo {
18 branch: String,
19 path: std::path::PathBuf,
20 status: String,
21}
22
23pub fn doctor() -> Result<()> {
25 let repo = git::get_repo_root(None)?;
26 println!(
27 "\n{}\n",
28 style("git-worktree-manager Health Check").cyan().bold()
29 );
30
31 let mut issues = 0u32;
32 let mut warnings = 0u32;
33
34 check_git_version(&mut issues);
36
37 let (worktrees, stale_count) = check_worktree_accessibility(&repo, &mut issues)?;
39
40 check_uncommitted_changes(&worktrees, &mut warnings);
42
43 let behind = check_behind_base(&worktrees, &repo, &mut warnings);
45
46 let conflicted = check_merge_conflicts(&worktrees, &mut issues);
48
49 check_claude_integration();
51
52 print_summary(issues, warnings);
54
55 print_recommendations(stale_count, &behind, &conflicted);
57
58 Ok(())
59}
60
61fn check_git_version(issues: &mut u32) {
63 println!("{}", style("1. Checking Git version...").bold());
64 match Command::new("git").arg("--version").output() {
65 Ok(output) if output.status.success() => {
66 let version_output = String::from_utf8_lossy(&output.stdout);
67 let version_str = version_output
68 .split_whitespace()
69 .nth(2)
70 .unwrap_or("unknown");
71
72 let is_ok =
73 version_meets_minimum(version_str, MIN_GIT_VERSION_MAJOR, MIN_GIT_VERSION_MINOR);
74
75 if is_ok {
76 println!(
77 " {} Git version {} (minimum: {})",
78 style("*").green(),
79 version_str,
80 MIN_GIT_VERSION,
81 );
82 } else {
83 println!(
84 " {} Git version {} is too old (minimum: {})",
85 style("x").red(),
86 version_str,
87 MIN_GIT_VERSION,
88 );
89 *issues += 1;
90 }
91 }
92 _ => {
93 println!(" {} Could not detect Git version", style("x").red());
94 *issues += 1;
95 }
96 }
97 println!();
98}
99
100fn check_worktree_accessibility(
102 repo: &std::path::Path,
103 issues: &mut u32,
104) -> Result<(Vec<WtInfo>, u32)> {
105 println!("{}", style("2. Checking worktree accessibility...").bold());
106 let feature_worktrees = git::get_feature_worktrees(Some(repo))?;
107 let mut stale_count = 0u32;
108 let mut worktrees: Vec<WtInfo> = Vec::new();
109
110 let pr_cache = PrCache::load_or_fetch(repo, true);
112
113 for (branch_name, path) in &feature_worktrees {
114 let status = get_worktree_status(path, repo, Some(branch_name.as_str()), &pr_cache);
115 if status == "stale" {
116 stale_count += 1;
117 println!(
118 " {} {}: Stale (directory missing)",
119 style("x").red(),
120 branch_name
121 );
122 *issues += 1;
123 }
124 worktrees.push(WtInfo {
125 branch: branch_name.clone(),
126 path: path.clone(),
127 status,
128 });
129 }
130
131 if stale_count == 0 {
132 println!(
133 " {} All {} worktrees are accessible",
134 style("*").green(),
135 worktrees.len()
136 );
137 }
138 println!();
139
140 Ok((worktrees, stale_count))
141}
142
143fn check_uncommitted_changes(worktrees: &[WtInfo], warnings: &mut u32) {
145 println!("{}", style("3. Checking for uncommitted changes...").bold());
146 let mut dirty: Vec<String> = Vec::new();
147 for wt in worktrees {
148 if wt.status == "modified" || wt.status == "active" {
149 if let Ok(r) = git::git_command(&["status", "--porcelain"], Some(&wt.path), false, true)
150 {
151 if r.returncode == 0 && !r.stdout.trim().is_empty() {
152 dirty.push(wt.branch.clone());
153 }
154 }
155 }
156 }
157
158 if dirty.is_empty() {
159 println!(" {} No uncommitted changes", style("*").green());
160 } else {
161 println!(
162 " {} {} worktree(s) with uncommitted changes:",
163 style("!").yellow(),
164 dirty.len()
165 );
166 for b in &dirty {
167 println!(" - {}", b);
168 }
169 *warnings += 1;
170 }
171 println!();
172}
173
174fn check_behind_base(
176 worktrees: &[WtInfo],
177 repo: &std::path::Path,
178 warnings: &mut u32,
179) -> Vec<(String, String, String)> {
180 println!(
181 "{}",
182 style("4. Checking if worktrees are behind base branch...").bold()
183 );
184 let mut behind: Vec<(String, String, String)> = Vec::new();
185
186 for wt in worktrees {
187 if wt.status == "stale" {
188 continue;
189 }
190 let key = format_config_key(CONFIG_KEY_BASE_BRANCH, &wt.branch);
191 let base = match git::get_config(&key, Some(repo)) {
192 Some(b) => b,
193 None => continue,
194 };
195
196 let origin_base = format!("origin/{}", base);
197 if let Ok(r) = git::git_command(
198 &[
199 "rev-list",
200 "--count",
201 &format!("{}..{}", wt.branch, origin_base),
202 ],
203 Some(&wt.path),
204 false,
205 true,
206 ) {
207 if r.returncode == 0 {
208 let count = r.stdout.trim();
209 if count != "0" {
210 behind.push((wt.branch.clone(), base.clone(), count.to_string()));
211 }
212 }
213 }
214 }
215
216 if behind.is_empty() {
217 println!(
218 " {} All worktrees are up-to-date with base",
219 style("*").green()
220 );
221 } else {
222 println!(
223 " {} {} worktree(s) behind base branch:",
224 style("!").yellow(),
225 behind.len()
226 );
227 for (b, base, count) in &behind {
228 println!(" - {}: {} commit(s) behind {}", b, count, base);
229 }
230 println!(
231 " {}",
232 style("Tip: Use 'gw sync --all' to update all worktrees").dim()
233 );
234 *warnings += 1;
235 }
236 println!();
237
238 behind
239}
240
241fn check_merge_conflicts(worktrees: &[WtInfo], issues: &mut u32) -> Vec<(String, usize)> {
243 println!("{}", style("5. Checking for merge conflicts...").bold());
244 let mut conflicted: Vec<(String, usize)> = Vec::new();
245
246 for wt in worktrees {
247 if wt.status == "stale" {
248 continue;
249 }
250 if let Ok(r) = git::git_command(
251 &["diff", "--name-only", "--diff-filter=U"],
252 Some(&wt.path),
253 false,
254 true,
255 ) {
256 if r.returncode == 0 && !r.stdout.trim().is_empty() {
257 let count = r.stdout.trim().lines().count();
258 conflicted.push((wt.branch.clone(), count));
259 }
260 }
261 }
262
263 if conflicted.is_empty() {
264 println!(" {} No merge conflicts detected", style("*").green());
265 } else {
266 println!(
267 " {} {} worktree(s) with merge conflicts:",
268 style("x").red(),
269 conflicted.len()
270 );
271 for (b, count) in &conflicted {
272 println!(" - {}: {} conflicted file(s)", b, count);
273 }
274 *issues += 1;
275 }
276 println!();
277
278 conflicted
279}
280
281fn check_claude_integration() {
283 println!("{}", style("6. Checking Claude Code integration...").bold());
284
285 let has_claude = Command::new("which")
286 .arg("claude")
287 .output()
288 .map(|o| o.status.success())
289 .unwrap_or(false);
290
291 if !has_claude {
292 println!(
293 " {} Claude Code not detected (optional)",
294 style("-").dim()
295 );
296 } else if setup_claude::is_skill_installed() {
297 println!(" {} Claude Code skill installed", style("*").green());
298 } else {
299 println!(
300 " {} Claude Code detected but delegation skill not installed",
301 style("!").yellow()
302 );
303 println!(
304 " {}",
305 style("Tip: Run 'gw setup-claude' to enable task delegation via Claude Code").dim()
306 );
307 }
308 println!();
309}
310
311fn print_summary(issues: u32, warnings: u32) {
313 println!("{}", style("Summary:").cyan().bold());
314 if issues == 0 && warnings == 0 {
315 println!("{}\n", style("* Everything looks healthy!").green().bold());
316 } else {
317 if issues > 0 {
318 println!(
319 "{}",
320 style(format!("x {} issue(s) found", issues)).red().bold()
321 );
322 }
323 if warnings > 0 {
324 println!(
325 "{}",
326 style(format!("! {} warning(s) found", warnings))
327 .yellow()
328 .bold()
329 );
330 }
331 println!();
332 }
333}
334
335fn print_recommendations(
337 stale_count: u32,
338 behind: &[(String, String, String)],
339 conflicted: &[(String, usize)],
340) {
341 let has_recommendations = stale_count > 0 || !behind.is_empty() || !conflicted.is_empty();
342 if has_recommendations {
343 println!("{}", style("Recommendations:").bold());
344 if stale_count > 0 {
345 println!(
346 " - Run {} to clean up stale worktrees",
347 style("gw prune").cyan()
348 );
349 }
350 if !behind.is_empty() {
351 println!(
352 " - Run {} to update all worktrees",
353 style("gw sync --all").cyan()
354 );
355 }
356 if !conflicted.is_empty() {
357 println!(" - Resolve conflicts in conflicted worktrees");
358 }
359 println!();
360 }
361}