use crate::cli::output::Output;
use crate::config::{get_repo_config_dir, is_repo_initialized, Settings};
use crate::errors::{CascadeError, Result};
use crate::git::{get_current_repository, is_git_repository};
use std::env;
pub async fn run() -> Result<()> {
Output::section("Cascade Doctor");
Output::info("Diagnosing repository health and configuration...");
println!();
let mut issues_found = 0;
let mut warnings_found = 0;
issues_found += check_git_repository().await?;
let (repo_issues, repo_warnings) = check_cascade_initialization().await?;
issues_found += repo_issues;
warnings_found += repo_warnings;
if issues_found == 0 {
let config_warnings = check_configuration().await?;
warnings_found += config_warnings;
}
warnings_found += check_git_configuration().await?;
print_summary(issues_found, warnings_found);
Ok(())
}
async fn check_git_repository() -> Result<u32> {
Output::check_start("Checking Git repository");
let current_dir = env::current_dir()
.map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
if !is_git_repository(¤t_dir) {
Output::error("Not in a Git repository");
Output::solution("Navigate to a Git repository or run 'git init'");
return Ok(1);
}
match get_current_repository() {
Ok(git_repo) => {
let repo_info = git_repo.get_info()?;
Output::success(format!(
"Git repository found at: {}",
repo_info.path.display()
));
if let Some(branch) = &repo_info.head_branch {
Output::success(format!("Current branch: {branch}"));
} else {
Output::warning("Detached HEAD state");
}
}
Err(e) => {
Output::error(format!("Git repository error: {e}"));
return Ok(1);
}
}
Ok(0)
}
async fn check_cascade_initialization() -> Result<(u32, u32)> {
Output::check_start("Checking Cascade initialization");
let git_repo = get_current_repository()?;
let repo_path = git_repo.path();
if !is_repo_initialized(repo_path) {
Output::error("Repository not initialized for Cascade");
Output::solution("Run 'ca init' to initialize");
return Ok((1, 0));
}
Output::success("Repository initialized for Cascade");
let config_dir = get_repo_config_dir(repo_path)?;
if !config_dir.exists() {
Output::error("Configuration directory missing");
Output::solution("Run 'ca init --force' to recreate");
return Ok((1, 0));
}
Output::success("Configuration directory exists");
let stacks_dir = config_dir.join("stacks");
let cache_dir = config_dir.join("cache");
let mut warnings = 0;
if !stacks_dir.exists() {
Output::warning("Stacks directory missing");
warnings += 1;
} else {
Output::success("Stacks directory exists");
}
if !cache_dir.exists() {
Output::warning("Cache directory missing");
warnings += 1;
} else {
Output::success("Cache directory exists");
}
Ok((0, warnings))
}
async fn check_configuration() -> Result<u32> {
Output::check_start("Checking configuration");
let git_repo = get_current_repository()?;
let config_dir = get_repo_config_dir(git_repo.path())?;
let config_file = config_dir.join("config.json");
let settings = Settings::load_from_file(&config_file)?;
let mut warnings = 0;
match settings.validate() {
Ok(()) => {
Output::success("Configuration is valid");
}
Err(e) => {
Output::warning(format!("Configuration validation failed: {e}"));
warnings += 1;
}
}
Output::check_start("Bitbucket configuration");
if settings.bitbucket.url.is_empty() {
Output::warning("Bitbucket server URL not configured");
Output::solution("ca config set bitbucket.url https://your-bitbucket-server.com");
warnings += 1;
} else {
Output::success("Bitbucket server URL configured");
}
if settings.bitbucket.project.is_empty() {
Output::warning("Bitbucket project key not configured");
Output::solution("ca config set bitbucket.project YOUR_PROJECT_KEY");
warnings += 1;
} else {
Output::success("Bitbucket project key configured");
}
if settings.bitbucket.repo.is_empty() {
Output::warning("Bitbucket repository slug not configured");
Output::solution("ca config set bitbucket.repo your-repo-name");
warnings += 1;
} else {
Output::success("Bitbucket repository slug configured");
}
if settings
.bitbucket
.token
.as_ref()
.is_none_or(|s| s.is_empty())
{
Output::warning("Bitbucket authentication token not configured");
Output::solution("ca config set bitbucket.token your-personal-access-token");
warnings += 1;
} else {
Output::success("Bitbucket authentication token configured");
}
Ok(warnings)
}
async fn check_git_configuration() -> Result<u32> {
Output::check_start("Checking Git configuration");
let git_repo = get_current_repository()?;
let repo_path = git_repo.path();
let git_repo_inner = git2::Repository::open(repo_path)?;
let mut warnings = 0;
match git_repo_inner.config() {
Ok(config) => {
match config.get_string("user.name") {
Ok(name) => {
Output::success(format!("Git user.name: {name}"));
}
Err(_) => {
Output::warning("Git user.name not configured");
Output::solution("git config user.name \"Your Name\"");
warnings += 1;
}
}
match config.get_string("user.email") {
Ok(email) => {
Output::success(format!("Git user.email: {email}"));
}
Err(_) => {
Output::warning("Git user.email not configured");
Output::solution("git config user.email \"your.email@example.com\"");
warnings += 1;
}
}
}
Err(_) => {
Output::warning("Could not read Git configuration");
warnings += 1;
}
}
match git_repo_inner.remotes() {
Ok(remotes) => {
if remotes.is_empty() {
Output::warning("No remote repositories configured");
Output::tip("Add a remote with 'git remote add origin <url>'");
warnings += 1;
} else {
Output::success(format!("Remote repositories configured: {}", remotes.len()));
}
}
Err(_) => {
Output::warning("Could not read remote repositories");
warnings += 1;
}
}
Ok(warnings)
}
fn print_summary(issues: u32, warnings: u32) {
Output::section("Summary");
if issues == 0 && warnings == 0 {
Output::success("All checks passed! Your repository is ready for Cascade.");
println!();
Output::tip("Next steps:");
Output::bullet("Create your first stack: ca create \"Add new feature\"");
Output::bullet("Submit for review: ca submit");
Output::bullet("View help: ca --help");
} else if issues == 0 {
Output::warning(format!(
"{} warning{} found, but no critical issues.",
warnings,
if warnings == 1 { "" } else { "s" }
));
Output::sub_item(
"Your repository should work, but consider addressing the warnings above.",
);
} else {
Output::error(format!(
"{} critical issue{} found that need to be resolved.",
issues,
if issues == 1 { "" } else { "s" }
));
if warnings > 0 {
Output::sub_item(format!(
"Additionally, {} warning{} found.",
warnings,
if warnings == 1 { "" } else { "s" }
));
}
Output::sub_item("Please address the issues above before using Cascade.");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::initialize_repo;
use git2::{Repository, Signature};
use std::env;
use tempfile::TempDir;
use tokio::sync::Mutex;
static CWD_MUTEX: Mutex<()> = Mutex::const_new(());
async fn create_test_repo() -> (TempDir, std::path::PathBuf) {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path().to_path_buf();
let repo = Repository::init(&repo_path).unwrap();
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test User").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
config.set_str("init.defaultBranch", "main").unwrap();
let signature = Signature::now("Test User", "test@example.com").unwrap();
let tree_id = {
let mut index = repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = repo.find_tree(tree_id).unwrap();
let commit_oid = repo
.commit(
None, &signature,
&signature,
"Initial commit",
&tree,
&[],
)
.unwrap();
let commit = repo.find_commit(commit_oid).unwrap();
repo.branch("main", &commit, false).unwrap();
repo.set_head("refs/heads/main").unwrap();
repo.checkout_head(None).unwrap();
(temp_dir, repo_path)
}
#[tokio::test]
async fn test_doctor_uninitialized() {
let _lock = CWD_MUTEX.lock().await;
let (_temp_dir, repo_path) = create_test_repo().await;
let original_dir = env::current_dir().unwrap();
env::set_current_dir(&repo_path).unwrap();
let result = run().await;
let _ = env::set_current_dir(&original_dir);
if let Err(e) = &result {
eprintln!("Doctor command failed: {e}");
}
assert!(result.is_ok());
}
#[tokio::test]
async fn test_doctor_initialized() {
let _lock = CWD_MUTEX.lock().await;
let (_temp_dir, repo_path) = create_test_repo().await;
initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string())).unwrap();
let original_dir = env::current_dir().expect("Failed to get current directory");
env::set_current_dir(&repo_path).expect("Failed to change to test repository directory");
let result = run().await;
let _ = env::set_current_dir(&original_dir);
assert!(
result.is_ok(),
"Doctor command should succeed in initialized repo: {:?}",
result.err()
);
}
}