git_worktree_manager/operations/
diagnostics.rs1use 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
11pub 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 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 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 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 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 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 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 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}