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