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 with upstream tracking
80    let repo_path = git_repo.path();
81    let new_git_repo = crate::git::GitRepository::open(repo_path)?;
82    let branch_manager = crate::git::branch_manager::BranchManager::new(new_git_repo);
83    let branch_info = branch_manager.get_branch_info()?;
84
85    Output::sub_item(format!("Local branches: {} total", branch_info.len()));
86
87    // Show current branch with upstream info
88    if let Some(current_branch) = branch_info.iter().find(|b| b.is_current) {
89        if let Some(upstream) = &current_branch.upstream {
90            let ahead_behind = if upstream.ahead > 0 || upstream.behind > 0 {
91                format!(" (↑{} ↓{})", upstream.ahead, upstream.behind)
92            } else {
93                " (up to date)".to_string()
94            };
95            Output::sub_item(format!(
96                "Current branch: {} → {}{}",
97                current_branch.name, upstream.full_name, ahead_behind
98            ));
99        } else {
100            Output::sub_item(format!(
101                "Current branch: {} (no upstream)",
102                current_branch.name
103            ));
104        }
105    }
106
107    Ok(())
108}
109
110fn show_cascade_status(git_repo: &GitRepository) -> Result<()> {
111    Output::section("Cascade Status");
112
113    let repo_path = git_repo.path();
114
115    if !is_repo_initialized(repo_path) {
116        Output::error("Status: Not initialized");
117        Output::sub_item("Run 'ca init' to initialize this repository for Cascade");
118        return Ok(());
119    }
120
121    Output::success("Status: Initialized");
122
123    // Load and show configuration
124    let config_dir = get_repo_config_dir(repo_path)?;
125    let config_file = config_dir.join("config.json");
126    let settings = Settings::load_from_file(&config_file)?;
127
128    // Check Bitbucket configuration
129    Output::section("Bitbucket Configuration");
130
131    let mut config_complete = true;
132
133    if !settings.bitbucket.url.is_empty() {
134        Output::success(format!("Server URL: {}", settings.bitbucket.url));
135    } else {
136        Output::error("Server URL: Not configured");
137        config_complete = false;
138    }
139
140    if !settings.bitbucket.project.is_empty() {
141        Output::success(format!("Project Key: {}", settings.bitbucket.project));
142    } else {
143        Output::error("Project Key: Not configured");
144        config_complete = false;
145    }
146
147    if !settings.bitbucket.repo.is_empty() {
148        Output::success(format!("Repository: {}", settings.bitbucket.repo));
149    } else {
150        Output::error("Repository: Not configured");
151        config_complete = false;
152    }
153
154    if let Some(token) = &settings.bitbucket.token {
155        if !token.is_empty() {
156            Output::success("Auth Token: Configured");
157        } else {
158            Output::error("Auth Token: Not configured");
159            config_complete = false;
160        }
161    } else {
162        Output::error("Auth Token: Not configured");
163        config_complete = false;
164    }
165
166    // Configuration status summary
167    Output::section("Configuration");
168    if config_complete {
169        Output::success("Status: Ready for use");
170    } else {
171        Output::warning("Status: Incomplete configuration");
172        Output::sub_item("Run 'ca config list' to see all settings");
173        Output::sub_item("Run 'ca doctor' for configuration recommendations");
174    }
175
176    // Show stack information
177    Output::section("Stacks");
178
179    match crate::stack::StackManager::new(repo_path) {
180        Ok(manager) => {
181            let stacks = manager.get_all_stacks();
182            let active_stack = manager.get_active_stack();
183
184            if stacks.is_empty() {
185                Output::sub_item("No stacks created yet");
186                Output::sub_item(
187                    "Run 'ca stacks create \"Stack Name\"' to create your first stack",
188                );
189            } else {
190                Output::sub_item(format!("Total stacks: {}", stacks.len()));
191
192                // Show each stack with detailed status
193                for stack in &stacks {
194                    let is_active = active_stack
195                        .as_ref()
196                        .map(|a| a.name == stack.name)
197                        .unwrap_or(false);
198                    let active_marker = if is_active { "◉" } else { "◯" };
199
200                    let submitted = stack.entries.iter().filter(|e| e.is_submitted).count();
201
202                    let status_info = if submitted > 0 {
203                        format!("{}/{} submitted", submitted, stack.entries.len())
204                    } else if !stack.entries.is_empty() {
205                        format!("{} entries, none submitted", stack.entries.len())
206                    } else {
207                        "empty".to_string()
208                    };
209
210                    Output::sub_item(format!(
211                        "{} {} - {}",
212                        active_marker, stack.name, status_info
213                    ));
214
215                    // Show additional details for active stack
216                    if is_active && !stack.entries.is_empty() {
217                        let first_branch = stack
218                            .entries
219                            .first()
220                            .map(|e| e.branch.as_str())
221                            .unwrap_or("unknown");
222                        println!("    Base: {} → {}", stack.base_branch, first_branch);
223                    }
224                }
225
226                if active_stack.is_none() && !stacks.is_empty() {
227                    Output::tip("No active stack. Use 'ca stacks switch <name>' to activate one");
228                }
229            }
230        }
231        Err(_) => {
232            Output::sub_item("Unable to load stack information");
233        }
234    }
235
236    Ok(())
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use crate::config::initialize_repo;
243    use git2::{Repository, Signature};
244    use std::env;
245    use tempfile::TempDir;
246
247    async fn create_test_repo() -> (TempDir, std::path::PathBuf) {
248        let temp_dir = TempDir::new().unwrap();
249        let repo_path = temp_dir.path().to_path_buf();
250
251        // Initialize git repository
252        let repo = Repository::init(&repo_path).unwrap();
253
254        // Create initial commit
255        let signature = Signature::now("Test User", "test@example.com").unwrap();
256        let tree_id = {
257            let mut index = repo.index().unwrap();
258            index.write_tree().unwrap()
259        };
260        let tree = repo.find_tree(tree_id).unwrap();
261
262        repo.commit(
263            Some("HEAD"),
264            &signature,
265            &signature,
266            "Initial commit",
267            &tree,
268            &[],
269        )
270        .unwrap();
271
272        (temp_dir, repo_path)
273    }
274
275    #[tokio::test]
276    async fn test_status_uninitialized() {
277        let (_temp_dir, repo_path) = create_test_repo().await;
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
299    #[tokio::test]
300    async fn test_status_initialized() {
301        let (_temp_dir, repo_path) = create_test_repo().await;
302
303        // Initialize Cascade
304        initialize_repo(&repo_path, Some("https://test.bitbucket.com".to_string())).unwrap();
305
306        // Commit the cascade config to avoid "untracked files" in status
307        std::process::Command::new("git")
308            .args(["add", ".cascade/"])
309            .current_dir(&repo_path)
310            .output()
311            .unwrap();
312        std::process::Command::new("git")
313            .args(["commit", "-m", "Initialize cascade"])
314            .current_dir(&repo_path)
315            .output()
316            .unwrap();
317
318        // Change to the repo directory (with proper error handling)
319        let original_dir = env::current_dir().map_err(|_| "Failed to get current dir");
320        match env::set_current_dir(&repo_path) {
321            Ok(_) => {
322                let result = run().await;
323
324                // Restore original directory (best effort)
325                if let Ok(orig) = original_dir {
326                    let _ = env::set_current_dir(orig);
327                }
328
329                assert!(result.is_ok());
330            }
331            Err(_) => {
332                // Skip test if we can't change directories (CI environment issue)
333                println!("Skipping test due to directory access restrictions");
334            }
335        }
336    }
337}