Skip to main content

git_worktree_manager/operations/
diagnostics.rs

1use 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
16/// Worktree info collected during health check.
17struct WtInfo {
18    branch: String,
19    path: std::path::PathBuf,
20    status: String,
21}
22
23/// Perform health check on all worktrees.
24pub 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    // 1. Check Git version
35    check_git_version(&mut issues);
36
37    // 2. Check worktree accessibility
38    let (worktrees, stale_count) = check_worktree_accessibility(&repo, &mut issues)?;
39
40    // 3. Check for uncommitted changes
41    check_uncommitted_changes(&worktrees, &mut warnings);
42
43    // 4. Check if worktrees are behind base branch
44    let behind = check_behind_base(&worktrees, &repo, &mut warnings);
45
46    // 5. Check for merge conflicts
47    let conflicted = check_merge_conflicts(&worktrees, &mut issues);
48
49    // 6. Check Claude Code integration
50    check_claude_integration();
51
52    // Summary
53    print_summary(issues, warnings);
54
55    // Recommendations
56    print_recommendations(stale_count, &behind, &conflicted);
57
58    Ok(())
59}
60
61/// Check Git version meets minimum requirement.
62fn 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
100/// Check that all worktrees are accessible (not stale).
101fn 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    // doctor needs fresh state; bypass the 60s TTL.
111    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
143/// Check for uncommitted changes in worktrees.
144fn 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
174/// Check if worktrees are behind their base branch.
175fn 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
241/// Check for merge conflicts in worktrees.
242fn 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
281/// Check Claude Code installation and skill integration.
282fn 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
311/// Print health check summary.
312fn 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
335/// Print remediation recommendations.
336fn 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}