git_worktree_manager/operations/
diagnostics.rs1use 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
16struct WtInfo {
18 branch: String,
19 path: std::path::PathBuf,
20 status: String,
21}
22
23pub fn doctor(session_start: bool, quiet: bool) -> Result<()> {
25 if session_start {
26 return doctor_session_start(quiet);
27 }
28 let repo = git::get_repo_root(None)?;
29 println!(
30 "\n{}\n",
31 style("git-worktree-manager Health Check").cyan().bold()
32 );
33
34 let mut issues = 0u32;
35 let mut warnings = 0u32;
36
37 check_git_version(&mut issues);
38 let (worktrees, stale_count) = check_worktree_accessibility(&repo, &mut issues)?;
39 check_uncommitted_changes(&worktrees, &mut warnings);
40 check_busy_worktrees(&worktrees);
41 check_claude_integration();
42
43 print_summary(issues, warnings);
44 print_recommendations(stale_count);
45
46 Ok(())
47}
48
49fn doctor_session_start(quiet: bool) -> Result<()> {
52 let cwd = std::env::current_dir().ok();
53 let cwd_ok = cwd.as_ref().map(|p| p.exists()).unwrap_or(false);
54 let cwd_str = cwd
55 .as_ref()
56 .map(|p| p.display().to_string())
57 .unwrap_or_else(|| "?".into());
58
59 let repo_root = git::get_repo_root(None).ok();
62 let branch = repo_root
63 .as_deref()
64 .and_then(|root| git::get_current_branch(Some(root)).ok())
65 .unwrap_or_else(|| "?".into());
66 let base = if branch != "?" {
67 repo_root
68 .as_deref()
69 .and_then(|root| {
70 let key = format_config_key(CONFIG_KEY_BASE_BRANCH, &branch);
71 git::get_config(&key, Some(root))
72 })
73 .unwrap_or_else(|| "?".into())
74 } else {
75 "?".into()
76 };
77 let prefix = if quiet { "gw:" } else { "gw doctor:" };
78 println!(
79 "{} cwd={} ok={} branch={} base={}",
80 prefix, cwd_str, cwd_ok, branch, base,
81 );
82 Ok(())
83}
84
85fn check_git_version(issues: &mut u32) {
87 println!("{}", style("1. Checking Git version...").bold());
88 match Command::new("git").arg("--version").output() {
89 Ok(output) if output.status.success() => {
90 let version_output = String::from_utf8_lossy(&output.stdout);
91 let version_str = version_output
92 .split_whitespace()
93 .nth(2)
94 .unwrap_or("unknown");
95
96 let is_ok =
97 version_meets_minimum(version_str, MIN_GIT_VERSION_MAJOR, MIN_GIT_VERSION_MINOR);
98
99 if is_ok {
100 println!(
101 " {} Git version {} (minimum: {})",
102 style("*").green(),
103 version_str,
104 MIN_GIT_VERSION,
105 );
106 } else {
107 println!(
108 " {} Git version {} is too old (minimum: {})",
109 style("x").red(),
110 version_str,
111 MIN_GIT_VERSION,
112 );
113 *issues += 1;
114 }
115 }
116 _ => {
117 println!(" {} Could not detect Git version", style("x").red());
118 *issues += 1;
119 }
120 }
121 println!();
122}
123
124fn check_worktree_accessibility(
126 repo: &std::path::Path,
127 issues: &mut u32,
128) -> Result<(Vec<WtInfo>, u32)> {
129 println!("{}", style("2. Checking worktree accessibility...").bold());
130 let feature_worktrees = git::get_feature_worktrees(Some(repo))?;
131 let mut stale_count = 0u32;
132 let mut worktrees: Vec<WtInfo> = Vec::new();
133
134 let pr_cache = PrCache::default();
137
138 for (branch_name, path) in &feature_worktrees {
139 let status = get_worktree_status(path, repo, Some(branch_name.as_str()), &pr_cache);
140 if status == "stale" {
141 stale_count += 1;
142 println!(
143 " {} {}: Stale (directory missing)",
144 style("x").red(),
145 branch_name
146 );
147 *issues += 1;
148 }
149 worktrees.push(WtInfo {
150 branch: branch_name.clone(),
151 path: path.clone(),
152 status,
153 });
154 }
155
156 if stale_count == 0 {
157 println!(
158 " {} All {} worktrees are accessible",
159 style("*").green(),
160 worktrees.len()
161 );
162 }
163 println!();
164
165 Ok((worktrees, stale_count))
166}
167
168fn check_uncommitted_changes(worktrees: &[WtInfo], warnings: &mut u32) {
170 println!("{}", style("3. Checking for uncommitted changes...").bold());
171 let mut dirty: Vec<String> = Vec::new();
172 for wt in worktrees {
173 if wt.status == "modified" || wt.status == "active" {
174 if let Ok(r) = git::git_command(&["status", "--porcelain"], Some(&wt.path), false, true)
175 {
176 if r.returncode == 0 && !r.stdout.trim().is_empty() {
177 dirty.push(wt.branch.clone());
178 }
179 }
180 }
181 }
182
183 if dirty.is_empty() {
184 println!(" {} No uncommitted changes", style("*").green());
185 } else {
186 println!(
187 " {} {} worktree(s) with uncommitted changes:",
188 style("!").yellow(),
189 dirty.len()
190 );
191 for b in &dirty {
192 println!(" - {}", b);
193 }
194 *warnings += 1;
195 }
196 println!();
197}
198
199fn check_busy_worktrees(worktrees: &[WtInfo]) {
203 println!("{}", style("4. Checking busy worktrees...").bold());
204 let busy: Vec<&WtInfo> = worktrees
205 .iter()
206 .filter(|w| !crate::operations::busy::detect_busy_lockfile_only(&w.path).is_empty())
207 .collect();
208 if busy.is_empty() {
209 println!(" {} No busy worktrees", style("*").green());
210 } else {
211 println!(
212 " {} {} busy worktree(s) (active in another session):",
213 style("i").cyan(),
214 busy.len()
215 );
216 for wt in &busy {
217 println!(" - {}", wt.branch);
218 }
219 }
220 println!();
221}
222
223fn check_claude_integration() {
225 println!("{}", style("5. Checking Claude Code integration...").bold());
226
227 let has_claude = git::has_command("claude");
230
231 let msgs = setup_claude_doctor_messages();
232
233 if !has_claude {
234 println!(
235 " {} Claude Code not detected (optional)",
236 style("-").dim()
237 );
238 } else if setup_claude::is_installed() {
239 println!(" {} {}", style("*").green(), msgs.installed);
240 } else {
241 println!(" {} {}", style("!").yellow(), msgs.missing_alert);
242 println!(" {}", style(format!("Tip: {}", msgs.missing_tip)).dim());
243 }
244 println!();
245}
246
247fn print_summary(issues: u32, warnings: u32) {
249 println!("{}", style("Summary:").cyan().bold());
250 if issues == 0 && warnings == 0 {
251 println!("{}\n", style("* Everything looks healthy!").green().bold());
252 } else {
253 if issues > 0 {
254 println!(
255 "{}",
256 style(format!("x {} issue(s) found", issues)).red().bold()
257 );
258 }
259 if warnings > 0 {
260 println!(
261 "{}",
262 style(format!("! {} warning(s) found", warnings))
263 .yellow()
264 .bold()
265 );
266 }
267 println!();
268 }
269}
270
271fn print_recommendations(stale_count: u32) {
273 if stale_count == 0 {
274 return;
275 }
276 println!("{}", style("Recommendations:").bold());
277 println!(
278 " - Run {} to clean up stale worktrees",
279 style("gw rm").cyan()
280 );
281 println!();
282}
283
284pub struct SetupClaudeDoctorMessages {
286 pub installed: &'static str,
287 pub missing_alert: &'static str,
288 pub missing_tip: &'static str,
289}
290
291pub fn setup_claude_doctor_messages() -> SetupClaudeDoctorMessages {
293 SetupClaudeDoctorMessages {
294 installed: "gw skills installed",
295 missing_alert: "Claude Code detected but gw skills not installed",
296 missing_tip: "Run 'gw setup-claude' to install gw skills and hooks into this repo",
297 }
298}