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::setup_claude;
14
15/// Worktree info collected during health check.
16struct WtInfo {
17    branch: String,
18    path: std::path::PathBuf,
19    status: String,
20}
21
22/// Perform health check on all worktrees.
23pub 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    // 1. Check Git version
34    check_git_version(&mut issues);
35
36    // 2. Check worktree accessibility
37    let (worktrees, stale_count) = check_worktree_accessibility(&repo, &mut issues)?;
38
39    // 3. Check for uncommitted changes
40    check_uncommitted_changes(&worktrees, &mut warnings);
41
42    // 4. Check if worktrees are behind base branch
43    let behind = check_behind_base(&worktrees, &repo, &mut warnings);
44
45    // 5. Check for merge conflicts
46    let conflicted = check_merge_conflicts(&worktrees, &mut issues);
47
48    // 6. Check Claude Code integration
49    check_claude_integration();
50
51    // Summary
52    print_summary(issues, warnings);
53
54    // Recommendations
55    print_recommendations(stale_count, &behind, &conflicted);
56
57    Ok(())
58}
59
60/// Check Git version meets minimum requirement.
61fn 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
99/// Check that all worktrees are accessible (not stale).
100fn 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
139/// Check for uncommitted changes in worktrees.
140fn 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
170/// Check if worktrees are behind their base branch.
171fn 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
237/// Check for merge conflicts in worktrees.
238fn 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
277/// Check Claude Code installation and skill integration.
278fn 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
307/// Print health check summary.
308fn 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
331/// Print remediation recommendations.
332fn 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}