cascade_cli/cli/commands/
status.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, GitRepository};
5use std::env;
6
7pub async fn run() -> Result<()> {
9 Output::section("Repository Overview");
10
11 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_status(&git_repo)?;
25
26 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 Output::sub_item(format!("Path: {}", repo_info.path.display()));
39
40 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 if let Some(commit) = &repo_info.head_commit {
49 Output::sub_item(format!("HEAD commit: {}", &commit[..12]));
50 }
51
52 if repo_info.is_dirty {
54 Output::warning("Working directory: Has uncommitted changes");
55 } else {
56 Output::success("Working directory: Clean");
57 }
58
59 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 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 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 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 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 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 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 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 let repo = Repository::init(&repo_path).unwrap();
229
230 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 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 if let Ok(orig) = original_dir {
263 let _ = env::set_current_dir(orig);
264 }
265
266 assert!(result.is_ok());
267 }
268 Err(_) => {
269 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_repo(&repo_path, Some("https://test.bitbucket.com".to_string())).unwrap();
281
282 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 if let Ok(orig) = original_dir {
290 let _ = env::set_current_dir(orig);
291 }
292
293 assert!(result.is_ok());
294 }
295 Err(_) => {
296 println!("Skipping test due to directory access restrictions");
298 }
299 }
300 }
301}