cascade_cli/cli/commands/
status.rs

1use crate::config::{get_repo_config_dir, is_repo_initialized, Settings};
2use crate::errors::{CascadeError, Result};
3use crate::git::{get_current_repository, GitRepository};
4use std::env;
5
6/// Show repository overview and all stacks status
7pub async fn run() -> Result<()> {
8    println!("šŸ“Š Repository Overview");
9    println!("━━━━━━━━━━━━━━━━━━━━━━");
10
11    // Get current directory and repository
12    let _current_dir = env::current_dir()
13        .map_err(|e| CascadeError::config(format!("Could not get current directory: {e}")))?;
14
15    let git_repo = match get_current_repository() {
16        Ok(repo) => repo,
17        Err(_) => {
18            println!("āŒ Not in a Git repository");
19            return Ok(());
20        }
21    };
22
23    // Show Git repository information
24    show_git_status(&git_repo)?;
25
26    // Show Cascade initialization status
27    show_cascade_status(&git_repo)?;
28
29    Ok(())
30}
31
32fn show_git_status(git_repo: &GitRepository) -> Result<()> {
33    println!("\nšŸ”§ Git Repository:");
34
35    let repo_info = git_repo.get_info()?;
36
37    // Repository path
38    println!("  Path: {}", repo_info.path.display());
39
40    // Current branch
41    if let Some(branch) = &repo_info.head_branch {
42        println!("  Current branch: {branch}");
43    } else {
44        println!("  Current branch: (detached HEAD)");
45    }
46
47    // Current commit
48    if let Some(commit) = &repo_info.head_commit {
49        println!("  HEAD commit: {}", &commit[..12]);
50    }
51
52    // Working directory status
53    if repo_info.is_dirty {
54        println!("  Working directory: āš ļø  Has uncommitted changes");
55    } else {
56        println!("  Working directory: āœ… Clean");
57    }
58
59    // Untracked files
60    if !repo_info.untracked_files.is_empty() {
61        println!(
62            "  Untracked files: {} files",
63            repo_info.untracked_files.len()
64        );
65        if repo_info.untracked_files.len() <= 5 {
66            for file in &repo_info.untracked_files {
67                println!("    - {file}");
68            }
69        } else {
70            for file in repo_info.untracked_files.iter().take(3) {
71                println!("    - {file}");
72            }
73            println!("    ... and {} more", repo_info.untracked_files.len() - 3);
74        }
75    } else {
76        println!("  Untracked files: None");
77    }
78
79    // Branches
80    let branches = git_repo.list_branches()?;
81    println!("  Local branches: {} total", branches.len());
82
83    Ok(())
84}
85
86fn show_cascade_status(git_repo: &GitRepository) -> Result<()> {
87    println!("\n🌊 Cascade Status:");
88
89    let repo_path = git_repo.path();
90
91    if !is_repo_initialized(repo_path) {
92        println!("  Status: āŒ Not initialized");
93        println!("  Run 'ca init' to initialize this repository for Cascade");
94        return Ok(());
95    }
96
97    println!("  Status: āœ… Initialized");
98
99    // Load and show configuration
100    let config_dir = get_repo_config_dir(repo_path)?;
101    let config_file = config_dir.join("config.json");
102    let settings = Settings::load_from_file(&config_file)?;
103
104    // Check Bitbucket configuration
105    println!("\nšŸ“” Bitbucket Configuration:");
106
107    let mut config_complete = true;
108
109    if !settings.bitbucket.url.is_empty() {
110        println!("  Server URL: āœ… {}", settings.bitbucket.url);
111    } else {
112        println!("  Server URL: āŒ Not configured");
113        config_complete = false;
114    }
115
116    if !settings.bitbucket.project.is_empty() {
117        println!("  Project Key: āœ… {}", settings.bitbucket.project);
118    } else {
119        println!("  Project Key: āŒ Not configured");
120        config_complete = false;
121    }
122
123    if !settings.bitbucket.repo.is_empty() {
124        println!("  Repository: āœ… {}", settings.bitbucket.repo);
125    } else {
126        println!("  Repository: āŒ Not configured");
127        config_complete = false;
128    }
129
130    if let Some(token) = &settings.bitbucket.token {
131        if !token.is_empty() {
132            println!("  Auth Token: āœ… Configured");
133        } else {
134            println!("  Auth Token: āŒ Not configured");
135            config_complete = false;
136        }
137    } else {
138        println!("  Auth Token: āŒ Not configured");
139        config_complete = false;
140    }
141
142    // Configuration status summary
143    println!("\nāš™ļø  Configuration:");
144    if config_complete {
145        println!("  Status: āœ… Ready for use");
146    } else {
147        println!("  Status: āš ļø  Incomplete configuration");
148        println!("  Run 'ca config list' to see all settings");
149        println!("  Run 'ca doctor' for configuration recommendations");
150    }
151
152    // Show stack information
153    println!("\nšŸ“š Stacks:");
154
155    match crate::stack::StackManager::new(repo_path) {
156        Ok(manager) => {
157            let stacks = manager.get_all_stacks();
158            let active_stack = manager.get_active_stack();
159
160            if stacks.is_empty() {
161                println!("  No stacks created yet");
162                println!("  Run 'ca stacks create \"Stack Name\"' to create your first stack");
163            } else {
164                println!("  Total stacks: {}", stacks.len());
165
166                // Show each stack with detailed status
167                for stack in &stacks {
168                    let is_active = active_stack
169                        .as_ref()
170                        .map(|a| a.name == stack.name)
171                        .unwrap_or(false);
172                    let active_marker = if is_active { "ā—‰" } else { "ā—Æ" };
173
174                    let submitted = stack.entries.iter().filter(|e| e.is_submitted).count();
175
176                    let status_info = if submitted > 0 {
177                        format!("{}/{} submitted", submitted, stack.entries.len())
178                    } else if !stack.entries.is_empty() {
179                        format!("{} entries, none submitted", stack.entries.len())
180                    } else {
181                        "empty".to_string()
182                    };
183
184                    println!("  {} {} - {}", active_marker, stack.name, status_info);
185
186                    // Show additional details for active stack
187                    if is_active && !stack.entries.is_empty() {
188                        let first_branch = stack
189                            .entries
190                            .first()
191                            .map(|e| e.branch.as_str())
192                            .unwrap_or("unknown");
193                        println!("    Base: {} → {}", stack.base_branch, first_branch);
194                    }
195                }
196
197                if active_stack.is_none() && !stacks.is_empty() {
198                    println!(
199                        "\n  šŸ’” No active stack. Use 'ca stacks switch <name>' to activate one"
200                    );
201                }
202            }
203        }
204        Err(_) => {
205            println!("  Unable to load stack information");
206        }
207    }
208
209    Ok(())
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::config::initialize_repo;
216    use git2::{Repository, Signature};
217    use std::env;
218    use tempfile::TempDir;
219
220    async fn create_test_repo() -> (TempDir, std::path::PathBuf) {
221        let temp_dir = TempDir::new().unwrap();
222        let repo_path = temp_dir.path().to_path_buf();
223
224        // Initialize git repository
225        let repo = Repository::init(&repo_path).unwrap();
226
227        // Create initial commit
228        let signature = Signature::now("Test User", "test@example.com").unwrap();
229        let tree_id = {
230            let mut index = repo.index().unwrap();
231            index.write_tree().unwrap()
232        };
233        let tree = repo.find_tree(tree_id).unwrap();
234
235        repo.commit(
236            Some("HEAD"),
237            &signature,
238            &signature,
239            "Initial commit",
240            &tree,
241            &[],
242        )
243        .unwrap();
244
245        (temp_dir, repo_path)
246    }
247
248    #[tokio::test]
249    async fn test_status_uninitialized() {
250        let (_temp_dir, repo_path) = create_test_repo().await;
251
252        // Change to the repo directory (with proper error handling)
253        let original_dir = env::current_dir().map_err(|_| "Failed to get current dir");
254        match env::set_current_dir(&repo_path) {
255            Ok(_) => {
256                let result = run().await;
257
258                // Restore original directory (best effort)
259                if let Ok(orig) = original_dir {
260                    let _ = env::set_current_dir(orig);
261                }
262
263                assert!(result.is_ok());
264            }
265            Err(_) => {
266                // Skip test if we can't change directories (CI environment issue)
267                println!("Skipping test due to directory access restrictions");
268            }
269        }
270    }
271
272    #[tokio::test]
273    async fn test_status_initialized() {
274        let (_temp_dir, repo_path) = create_test_repo().await;
275
276        // Initialize Cascade
277        initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string())).unwrap();
278
279        // Change to the repo directory (with proper error handling)
280        let original_dir = env::current_dir().map_err(|_| "Failed to get current dir");
281        match env::set_current_dir(&repo_path) {
282            Ok(_) => {
283                let result = run().await;
284
285                // Restore original directory (best effort)
286                if let Ok(orig) = original_dir {
287                    let _ = env::set_current_dir(orig);
288                }
289
290                assert!(result.is_ok());
291            }
292            Err(_) => {
293                // Skip test if we can't change directories (CI environment issue)
294                println!("Skipping test due to directory access restrictions");
295            }
296        }
297    }
298}