cascade_cli/cli/commands/
doctor.rs

1use crate::config::{get_repo_config_dir, is_repo_initialized, Settings};
2use crate::errors::{CascadeError, Result};
3use crate::git::{get_current_repository, is_git_repository};
4use std::env;
5
6/// Check repository health and configuration
7pub async fn run() -> Result<()> {
8    println!("🩺 Cascade Doctor");
9    println!("━━━━━━━━━━━━━━━━━");
10    println!("Diagnosing repository health and configuration...\n");
11
12    let mut issues_found = 0;
13    let mut warnings_found = 0;
14
15    // Check 1: Git repository
16    issues_found += check_git_repository().await?;
17
18    // Check 2: Cascade initialization
19    let (repo_issues, repo_warnings) = check_cascade_initialization().await?;
20    issues_found += repo_issues;
21    warnings_found += repo_warnings;
22
23    // Check 3: Configuration
24    if issues_found == 0 {
25        let config_warnings = check_configuration().await?;
26        warnings_found += config_warnings;
27    }
28
29    // Check 4: Git configuration
30    warnings_found += check_git_configuration().await?;
31
32    // Summary
33    print_summary(issues_found, warnings_found);
34
35    Ok(())
36}
37
38async fn check_git_repository() -> Result<u32> {
39    println!("šŸ” Checking Git repository...");
40
41    let current_dir = env::current_dir()
42        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
43
44    if !is_git_repository(&current_dir) {
45        println!("  āŒ Not in a Git repository");
46        println!("     Solution: Navigate to a Git repository or run 'git init'");
47        return Ok(1);
48    }
49
50    match get_current_repository() {
51        Ok(git_repo) => {
52            let repo_info = git_repo.get_info()?;
53            println!("  āœ… Git repository found at: {}", repo_info.path.display());
54
55            if let Some(branch) = &repo_info.head_branch {
56                println!("  āœ… Current branch: {branch}");
57            } else {
58                println!("  āš ļø  Detached HEAD state");
59            }
60        }
61        Err(e) => {
62            println!("  āŒ Git repository error: {e}");
63            return Ok(1);
64        }
65    }
66
67    Ok(0)
68}
69
70async fn check_cascade_initialization() -> Result<(u32, u32)> {
71    println!("\n🌊 Checking Cascade initialization...");
72
73    let git_repo = get_current_repository()?;
74    let repo_path = git_repo.path();
75
76    if !is_repo_initialized(repo_path) {
77        println!("  āŒ Repository not initialized for Cascade");
78        println!("     Solution: Run 'ca init' to initialize");
79        return Ok((1, 0));
80    }
81
82    println!("  āœ… Repository initialized for Cascade");
83
84    // Check for configuration directory structure
85    let config_dir = get_repo_config_dir(repo_path)?;
86
87    if !config_dir.exists() {
88        println!("  āŒ Configuration directory missing");
89        println!("     Solution: Run 'ca init --force' to recreate");
90        return Ok((1, 0));
91    }
92
93    println!("  āœ… Configuration directory exists");
94
95    // Check for required subdirectories
96    let stacks_dir = config_dir.join("stacks");
97    let cache_dir = config_dir.join("cache");
98
99    let mut warnings = 0;
100
101    if !stacks_dir.exists() {
102        println!("  āš ļø  Stacks directory missing");
103        warnings += 1;
104    } else {
105        println!("  āœ… Stacks directory exists");
106    }
107
108    if !cache_dir.exists() {
109        println!("  āš ļø  Cache directory missing");
110        warnings += 1;
111    } else {
112        println!("  āœ… Cache directory exists");
113    }
114
115    Ok((0, warnings))
116}
117
118async fn check_configuration() -> Result<u32> {
119    println!("\nāš™ļø  Checking configuration...");
120
121    let git_repo = get_current_repository()?;
122    let config_dir = get_repo_config_dir(git_repo.path())?;
123    let config_file = config_dir.join("config.json");
124
125    let settings = Settings::load_from_file(&config_file)?;
126    let mut warnings = 0;
127
128    // Validate configuration
129    match settings.validate() {
130        Ok(()) => {
131            println!("  āœ… Configuration is valid");
132        }
133        Err(e) => {
134            println!("  āš ļø  Configuration validation failed: {e}");
135            warnings += 1;
136        }
137    }
138
139    // Check Bitbucket configuration completeness
140    println!("\nšŸ“” Bitbucket configuration:");
141
142    if settings.bitbucket.url.is_empty() {
143        println!("  āš ļø  Bitbucket server URL not configured");
144        println!("     Solution: ca config set bitbucket.url https://your-bitbucket-server.com");
145        warnings += 1;
146    } else {
147        println!("  āœ… Bitbucket server URL configured");
148    }
149
150    if settings.bitbucket.project.is_empty() {
151        println!("  āš ļø  Bitbucket project key not configured");
152        println!("     Solution: ca config set bitbucket.project YOUR_PROJECT_KEY");
153        warnings += 1;
154    } else {
155        println!("  āœ… Bitbucket project key configured");
156    }
157
158    if settings.bitbucket.repo.is_empty() {
159        println!("  āš ļø  Bitbucket repository slug not configured");
160        println!("     Solution: ca config set bitbucket.repo your-repo-name");
161        warnings += 1;
162    } else {
163        println!("  āœ… Bitbucket repository slug configured");
164    }
165
166    if settings
167        .bitbucket
168        .token
169        .as_ref()
170        .is_none_or(|s| s.is_empty())
171    {
172        println!("  āš ļø  Bitbucket authentication token not configured");
173        println!("     Solution: ca config set bitbucket.token your-personal-access-token");
174        warnings += 1;
175    } else {
176        println!("  āœ… Bitbucket authentication token configured");
177    }
178
179    Ok(warnings)
180}
181
182async fn check_git_configuration() -> Result<u32> {
183    println!("\nšŸ“¦ Checking Git configuration...");
184
185    let git_repo = get_current_repository()?;
186    let repo_path = git_repo.path();
187    let git_repo_inner = git2::Repository::open(repo_path)?;
188
189    let mut warnings = 0;
190
191    // Check Git user configuration
192    match git_repo_inner.config() {
193        Ok(config) => {
194            match config.get_string("user.name") {
195                Ok(name) => {
196                    println!("  āœ… Git user.name: {name}");
197                }
198                Err(_) => {
199                    println!("  āš ļø  Git user.name not configured");
200                    println!("     Solution: git config user.name \"Your Name\"");
201                    warnings += 1;
202                }
203            }
204
205            match config.get_string("user.email") {
206                Ok(email) => {
207                    println!("  āœ… Git user.email: {email}");
208                }
209                Err(_) => {
210                    println!("  āš ļø  Git user.email not configured");
211                    println!("     Solution: git config user.email \"your.email@example.com\"");
212                    warnings += 1;
213                }
214            }
215        }
216        Err(_) => {
217            println!("  āš ļø  Could not read Git configuration");
218            warnings += 1;
219        }
220    }
221
222    // Check for remote repositories
223    match git_repo_inner.remotes() {
224        Ok(remotes) => {
225            if remotes.is_empty() {
226                println!("  āš ļø  No remote repositories configured");
227                println!("     Tip: Add a remote with 'git remote add origin <url>'");
228                warnings += 1;
229            } else {
230                println!("  āœ… Remote repositories configured: {}", remotes.len());
231            }
232        }
233        Err(_) => {
234            println!("  āš ļø  Could not read remote repositories");
235            warnings += 1;
236        }
237    }
238
239    Ok(warnings)
240}
241
242fn print_summary(issues: u32, warnings: u32) {
243    println!("\nšŸ“Š Summary:");
244    println!("━━━━━━━━━━");
245
246    if issues == 0 && warnings == 0 {
247        println!("šŸŽ‰ All checks passed! Your repository is ready for Cascade.");
248        println!("\nšŸ’” Next steps:");
249        println!("  1. Create your first stack: ca create \"Add new feature\"");
250        println!("  2. Submit for review: ca submit");
251        println!("  3. View help: ca --help");
252    } else if issues == 0 {
253        println!(
254            "āš ļø  {} warning{} found, but no critical issues.",
255            warnings,
256            if warnings == 1 { "" } else { "s" }
257        );
258        println!("   Your repository should work, but consider addressing the warnings above.");
259    } else {
260        println!(
261            "āŒ {} critical issue{} found that need to be resolved.",
262            issues,
263            if issues == 1 { "" } else { "s" }
264        );
265        if warnings > 0 {
266            println!(
267                "   Additionally, {} warning{} found.",
268                warnings,
269                if warnings == 1 { "" } else { "s" }
270            );
271        }
272        println!("   Please address the issues above before using Cascade.");
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use crate::config::initialize_repo;
280    use git2::{Repository, Signature};
281    use std::env;
282    use tempfile::TempDir;
283
284    async fn create_test_repo() -> (TempDir, std::path::PathBuf) {
285        let temp_dir = TempDir::new().unwrap();
286        let repo_path = temp_dir.path().to_path_buf();
287
288        // Initialize git repository
289        let repo = Repository::init(&repo_path).unwrap();
290
291        // Configure git user and default branch
292        let mut config = repo.config().unwrap();
293        config.set_str("user.name", "Test User").unwrap();
294        config.set_str("user.email", "test@example.com").unwrap();
295        config.set_str("init.defaultBranch", "main").unwrap();
296
297        // Create initial commit
298        let signature = Signature::now("Test User", "test@example.com").unwrap();
299        let tree_id = {
300            let mut index = repo.index().unwrap();
301            index.write_tree().unwrap()
302        };
303        let tree = repo.find_tree(tree_id).unwrap();
304
305        let commit_oid = repo
306            .commit(
307                None, // Create initial commit without updating HEAD
308                &signature,
309                &signature,
310                "Initial commit",
311                &tree,
312                &[],
313            )
314            .unwrap();
315
316        // Create main branch and set HEAD
317        let commit = repo.find_commit(commit_oid).unwrap();
318        repo.branch("main", &commit, false).unwrap();
319        repo.set_head("refs/heads/main").unwrap();
320        repo.checkout_head(None).unwrap();
321
322        (temp_dir, repo_path)
323    }
324
325    #[tokio::test]
326    async fn test_doctor_uninitialized() {
327        let (temp_dir, repo_path) = create_test_repo().await;
328
329        let original_dir = env::current_dir().unwrap();
330        env::set_current_dir(&repo_path).unwrap();
331
332        let result = run().await;
333
334        let _ = env::set_current_dir(original_dir);
335
336        // Keep temp_dir alive until after the operation completes
337        drop(temp_dir);
338
339        if let Err(e) = &result {
340            eprintln!("Doctor command failed: {e}");
341        }
342        assert!(result.is_ok());
343    }
344
345    #[tokio::test]
346    async fn test_doctor_initialized() {
347        let (temp_dir, repo_path) = create_test_repo().await;
348
349        // Initialize Cascade
350        initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string())).unwrap();
351
352        let original_dir = env::current_dir().unwrap();
353        env::set_current_dir(&repo_path).unwrap();
354
355        let result = run().await;
356
357        let _ = env::set_current_dir(original_dir);
358
359        // Keep temp_dir alive until after the operation completes
360        drop(temp_dir);
361
362        if let Err(e) = &result {
363            eprintln!("Doctor command failed: {e}");
364        }
365        assert!(result.is_ok());
366    }
367}