cascade_cli/cli/commands/
doctor.rs1use 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
6pub 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 issues_found += check_git_repository().await?;
17
18 let (repo_issues, repo_warnings) = check_cascade_initialization().await?;
20 issues_found += repo_issues;
21 warnings_found += repo_warnings;
22
23 if issues_found == 0 {
25 let config_warnings = check_configuration().await?;
26 warnings_found += config_warnings;
27 }
28
29 warnings_found += check_git_configuration().await?;
31
32 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(¤t_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 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 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 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 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 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 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 let repo = Repository::init(&repo_path).unwrap();
290
291 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 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, &signature,
309 &signature,
310 "Initial commit",
311 &tree,
312 &[],
313 )
314 .unwrap();
315
316 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 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_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 drop(temp_dir);
361
362 if let Err(e) = &result {
363 eprintln!("Doctor command failed: {e}");
364 }
365 assert!(result.is_ok());
366 }
367}