Skip to main content

git_worktree_manager/operations/
diagnostics.rs

1use std::process::Command;
2
3use console::style;
4
5use crate::constants::{format_config_key, CONFIG_KEY_BASE_BRANCH};
6use crate::error::Result;
7use crate::git;
8
9use super::display::get_worktree_status;
10
11/// Perform health check on all worktrees.
12pub fn doctor() -> Result<()> {
13    let repo = git::get_repo_root(None)?;
14    println!(
15        "\n{}\n",
16        style("git-worktree-manager Health Check").cyan().bold()
17    );
18
19    let mut issues = 0u32;
20    let mut warnings = 0u32;
21
22    // 1. Check Git version
23    println!("{}", style("1. Checking Git version...").bold());
24    match Command::new("git").arg("--version").output() {
25        Ok(output) if output.status.success() => {
26            let version_output = String::from_utf8_lossy(&output.stdout);
27            let version_str = version_output
28                .split_whitespace()
29                .nth(2)
30                .unwrap_or("unknown");
31
32            let parts: Vec<u32> = version_str
33                .split('.')
34                .filter_map(|p| p.parse().ok())
35                .collect();
36            let is_ok = parts.len() >= 2 && (parts[0] > 2 || (parts[0] == 2 && parts[1] >= 31));
37
38            if is_ok {
39                println!(
40                    "   {} Git version {} (minimum: 2.31.0)",
41                    style("*").green(),
42                    version_str
43                );
44            } else {
45                println!(
46                    "   {} Git version {} is too old (minimum: 2.31.0)",
47                    style("x").red(),
48                    version_str
49                );
50                issues += 1;
51            }
52        }
53        _ => {
54            println!("   {} Could not detect Git version", style("x").red());
55            issues += 1;
56        }
57    }
58    println!();
59
60    // 2. Check worktree accessibility
61    println!("{}", style("2. Checking worktree accessibility...").bold());
62    let feature_worktrees = git::get_feature_worktrees(Some(&repo))?;
63    let mut stale_count = 0u32;
64
65    struct WtInfo {
66        branch: String,
67        path: std::path::PathBuf,
68        status: String,
69    }
70
71    let mut worktrees: Vec<WtInfo> = Vec::new();
72
73    for (branch_name, path) in &feature_worktrees {
74        let status = get_worktree_status(path, &repo);
75        if status == "stale" {
76            stale_count += 1;
77            println!(
78                "   {} {}: Stale (directory missing)",
79                style("x").red(),
80                branch_name
81            );
82            issues += 1;
83        }
84        worktrees.push(WtInfo {
85            branch: branch_name.clone(),
86            path: path.clone(),
87            status,
88        });
89    }
90
91    if stale_count == 0 {
92        println!(
93            "   {} All {} worktrees are accessible",
94            style("*").green(),
95            worktrees.len()
96        );
97    }
98    println!();
99
100    // 3. Check for uncommitted changes
101    println!("{}", style("3. Checking for uncommitted changes...").bold());
102    let mut dirty: Vec<String> = Vec::new();
103    for wt in &worktrees {
104        if wt.status == "modified" || wt.status == "active" {
105            if let Ok(r) = git::git_command(&["status", "--porcelain"], Some(&wt.path), false, true)
106            {
107                if r.returncode == 0 && !r.stdout.trim().is_empty() {
108                    dirty.push(wt.branch.clone());
109                }
110            }
111        }
112    }
113
114    if dirty.is_empty() {
115        println!("   {} No uncommitted changes", style("*").green());
116    } else {
117        println!(
118            "   {} {} worktree(s) with uncommitted changes:",
119            style("!").yellow(),
120            dirty.len()
121        );
122        for b in &dirty {
123            println!("      - {}", b);
124        }
125        warnings += 1;
126    }
127    println!();
128
129    // 4. Check if worktrees are behind base branch
130    println!(
131        "{}",
132        style("4. Checking if worktrees are behind base branch...").bold()
133    );
134    let mut behind: Vec<(String, String, String)> = Vec::new();
135
136    for wt in &worktrees {
137        if wt.status == "stale" {
138            continue;
139        }
140        let key = format_config_key(CONFIG_KEY_BASE_BRANCH, &wt.branch);
141        let base = match git::get_config(&key, Some(&repo)) {
142            Some(b) => b,
143            None => continue,
144        };
145
146        let origin_base = format!("origin/{}", base);
147        if let Ok(r) = git::git_command(
148            &[
149                "rev-list",
150                "--count",
151                &format!("{}..{}", wt.branch, origin_base),
152            ],
153            Some(&wt.path),
154            false,
155            true,
156        ) {
157            if r.returncode == 0 {
158                let count = r.stdout.trim();
159                if count != "0" {
160                    behind.push((wt.branch.clone(), base.clone(), count.to_string()));
161                }
162            }
163        }
164    }
165
166    if behind.is_empty() {
167        println!(
168            "   {} All worktrees are up-to-date with base",
169            style("*").green()
170        );
171    } else {
172        println!(
173            "   {} {} worktree(s) behind base branch:",
174            style("!").yellow(),
175            behind.len()
176        );
177        for (b, base, count) in &behind {
178            println!("      - {}: {} commit(s) behind {}", b, count, base);
179        }
180        println!(
181            "   {}",
182            style("Tip: Use 'gw sync --all' to update all worktrees").dim()
183        );
184        warnings += 1;
185    }
186    println!();
187
188    // 5. Check for merge conflicts
189    println!("{}", style("5. Checking for merge conflicts...").bold());
190    let mut conflicted: Vec<(String, usize)> = Vec::new();
191
192    for wt in &worktrees {
193        if wt.status == "stale" {
194            continue;
195        }
196        if let Ok(r) = git::git_command(
197            &["diff", "--name-only", "--diff-filter=U"],
198            Some(&wt.path),
199            false,
200            true,
201        ) {
202            if r.returncode == 0 && !r.stdout.trim().is_empty() {
203                let count = r.stdout.trim().lines().count();
204                conflicted.push((wt.branch.clone(), count));
205            }
206        }
207    }
208
209    if conflicted.is_empty() {
210        println!("   {} No merge conflicts detected", style("*").green());
211    } else {
212        println!(
213            "   {} {} worktree(s) with merge conflicts:",
214            style("x").red(),
215            conflicted.len()
216        );
217        for (b, count) in &conflicted {
218            println!("      - {}: {} conflicted file(s)", b, count);
219        }
220        issues += 1;
221    }
222    println!();
223
224    // Summary
225    println!("{}", style("Summary:").cyan().bold());
226    if issues == 0 && warnings == 0 {
227        println!("{}\n", style("* Everything looks healthy!").green().bold());
228    } else {
229        if issues > 0 {
230            println!(
231                "{}",
232                style(format!("x {} issue(s) found", issues)).red().bold()
233            );
234        }
235        if warnings > 0 {
236            println!(
237                "{}",
238                style(format!("! {} warning(s) found", warnings))
239                    .yellow()
240                    .bold()
241            );
242        }
243        println!();
244    }
245
246    // Recommendations
247    let has_recommendations = stale_count > 0 || !behind.is_empty() || !conflicted.is_empty();
248    if has_recommendations {
249        println!("{}", style("Recommendations:").bold());
250        if stale_count > 0 {
251            println!(
252                "  - Run {} to clean up stale worktrees",
253                style("gw prune").cyan()
254            );
255        }
256        if !behind.is_empty() {
257            println!(
258                "  - Run {} to update all worktrees",
259                style("gw sync --all").cyan()
260            );
261        }
262        if !conflicted.is_empty() {
263            println!("  - Resolve conflicts in conflicted worktrees");
264        }
265        println!();
266    }
267
268    Ok(())
269}