cascade_cli/cli/commands/
status.rs

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