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