use std::process::Command;
use console::style;
use crate::constants::{
format_config_key, version_meets_minimum, CONFIG_KEY_BASE_BRANCH, MIN_GIT_VERSION,
MIN_GIT_VERSION_MAJOR, MIN_GIT_VERSION_MINOR,
};
use crate::error::Result;
use crate::git;
use super::display::get_worktree_status;
use super::setup_claude;
struct WtInfo {
branch: String,
path: std::path::PathBuf,
status: String,
}
pub fn doctor() -> Result<()> {
let repo = git::get_repo_root(None)?;
println!(
"\n{}\n",
style("git-worktree-manager Health Check").cyan().bold()
);
let mut issues = 0u32;
let mut warnings = 0u32;
check_git_version(&mut issues);
let (worktrees, stale_count) = check_worktree_accessibility(&repo, &mut issues)?;
check_uncommitted_changes(&worktrees, &mut warnings);
let behind = check_behind_base(&worktrees, &repo, &mut warnings);
let conflicted = check_merge_conflicts(&worktrees, &mut issues);
check_claude_integration();
print_summary(issues, warnings);
print_recommendations(stale_count, &behind, &conflicted);
Ok(())
}
fn check_git_version(issues: &mut u32) {
println!("{}", style("1. Checking Git version...").bold());
match Command::new("git").arg("--version").output() {
Ok(output) if output.status.success() => {
let version_output = String::from_utf8_lossy(&output.stdout);
let version_str = version_output
.split_whitespace()
.nth(2)
.unwrap_or("unknown");
let is_ok =
version_meets_minimum(version_str, MIN_GIT_VERSION_MAJOR, MIN_GIT_VERSION_MINOR);
if is_ok {
println!(
" {} Git version {} (minimum: {})",
style("*").green(),
version_str,
MIN_GIT_VERSION,
);
} else {
println!(
" {} Git version {} is too old (minimum: {})",
style("x").red(),
version_str,
MIN_GIT_VERSION,
);
*issues += 1;
}
}
_ => {
println!(" {} Could not detect Git version", style("x").red());
*issues += 1;
}
}
println!();
}
fn check_worktree_accessibility(
repo: &std::path::Path,
issues: &mut u32,
) -> Result<(Vec<WtInfo>, u32)> {
println!("{}", style("2. Checking worktree accessibility...").bold());
let feature_worktrees = git::get_feature_worktrees(Some(repo))?;
let mut stale_count = 0u32;
let mut worktrees: Vec<WtInfo> = Vec::new();
for (branch_name, path) in &feature_worktrees {
let status = get_worktree_status(path, repo, Some(branch_name.as_str()));
if status == "stale" {
stale_count += 1;
println!(
" {} {}: Stale (directory missing)",
style("x").red(),
branch_name
);
*issues += 1;
}
worktrees.push(WtInfo {
branch: branch_name.clone(),
path: path.clone(),
status,
});
}
if stale_count == 0 {
println!(
" {} All {} worktrees are accessible",
style("*").green(),
worktrees.len()
);
}
println!();
Ok((worktrees, stale_count))
}
fn check_uncommitted_changes(worktrees: &[WtInfo], warnings: &mut u32) {
println!("{}", style("3. Checking for uncommitted changes...").bold());
let mut dirty: Vec<String> = Vec::new();
for wt in worktrees {
if wt.status == "modified" || wt.status == "active" {
if let Ok(r) = git::git_command(&["status", "--porcelain"], Some(&wt.path), false, true)
{
if r.returncode == 0 && !r.stdout.trim().is_empty() {
dirty.push(wt.branch.clone());
}
}
}
}
if dirty.is_empty() {
println!(" {} No uncommitted changes", style("*").green());
} else {
println!(
" {} {} worktree(s) with uncommitted changes:",
style("!").yellow(),
dirty.len()
);
for b in &dirty {
println!(" - {}", b);
}
*warnings += 1;
}
println!();
}
fn check_behind_base(
worktrees: &[WtInfo],
repo: &std::path::Path,
warnings: &mut u32,
) -> Vec<(String, String, String)> {
println!(
"{}",
style("4. Checking if worktrees are behind base branch...").bold()
);
let mut behind: Vec<(String, String, String)> = Vec::new();
for wt in worktrees {
if wt.status == "stale" {
continue;
}
let key = format_config_key(CONFIG_KEY_BASE_BRANCH, &wt.branch);
let base = match git::get_config(&key, Some(repo)) {
Some(b) => b,
None => continue,
};
let origin_base = format!("origin/{}", base);
if let Ok(r) = git::git_command(
&[
"rev-list",
"--count",
&format!("{}..{}", wt.branch, origin_base),
],
Some(&wt.path),
false,
true,
) {
if r.returncode == 0 {
let count = r.stdout.trim();
if count != "0" {
behind.push((wt.branch.clone(), base.clone(), count.to_string()));
}
}
}
}
if behind.is_empty() {
println!(
" {} All worktrees are up-to-date with base",
style("*").green()
);
} else {
println!(
" {} {} worktree(s) behind base branch:",
style("!").yellow(),
behind.len()
);
for (b, base, count) in &behind {
println!(" - {}: {} commit(s) behind {}", b, count, base);
}
println!(
" {}",
style("Tip: Use 'gw sync --all' to update all worktrees").dim()
);
*warnings += 1;
}
println!();
behind
}
fn check_merge_conflicts(worktrees: &[WtInfo], issues: &mut u32) -> Vec<(String, usize)> {
println!("{}", style("5. Checking for merge conflicts...").bold());
let mut conflicted: Vec<(String, usize)> = Vec::new();
for wt in worktrees {
if wt.status == "stale" {
continue;
}
if let Ok(r) = git::git_command(
&["diff", "--name-only", "--diff-filter=U"],
Some(&wt.path),
false,
true,
) {
if r.returncode == 0 && !r.stdout.trim().is_empty() {
let count = r.stdout.trim().lines().count();
conflicted.push((wt.branch.clone(), count));
}
}
}
if conflicted.is_empty() {
println!(" {} No merge conflicts detected", style("*").green());
} else {
println!(
" {} {} worktree(s) with merge conflicts:",
style("x").red(),
conflicted.len()
);
for (b, count) in &conflicted {
println!(" - {}: {} conflicted file(s)", b, count);
}
*issues += 1;
}
println!();
conflicted
}
fn check_claude_integration() {
println!("{}", style("6. Checking Claude Code integration...").bold());
let has_claude = Command::new("which")
.arg("claude")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if !has_claude {
println!(
" {} Claude Code not detected (optional)",
style("-").dim()
);
} else if setup_claude::is_skill_installed() {
println!(" {} Claude Code skill installed", style("*").green());
} else {
println!(
" {} Claude Code detected but delegation skill not installed",
style("!").yellow()
);
println!(
" {}",
style("Tip: Run 'gw setup-claude' to enable task delegation via Claude Code").dim()
);
}
println!();
}
fn print_summary(issues: u32, warnings: u32) {
println!("{}", style("Summary:").cyan().bold());
if issues == 0 && warnings == 0 {
println!("{}\n", style("* Everything looks healthy!").green().bold());
} else {
if issues > 0 {
println!(
"{}",
style(format!("x {} issue(s) found", issues)).red().bold()
);
}
if warnings > 0 {
println!(
"{}",
style(format!("! {} warning(s) found", warnings))
.yellow()
.bold()
);
}
println!();
}
}
fn print_recommendations(
stale_count: u32,
behind: &[(String, String, String)],
conflicted: &[(String, usize)],
) {
let has_recommendations = stale_count > 0 || !behind.is_empty() || !conflicted.is_empty();
if has_recommendations {
println!("{}", style("Recommendations:").bold());
if stale_count > 0 {
println!(
" - Run {} to clean up stale worktrees",
style("gw prune").cyan()
);
}
if !behind.is_empty() {
println!(
" - Run {} to update all worktrees",
style("gw sync --all").cyan()
);
}
if !conflicted.is_empty() {
println!(" - Resolve conflicts in conflicted worktrees");
}
println!();
}
}