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