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 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 if let Some(current_branch) = branch_info.iter().find(|b| b.is_current) {
89 if let Some(upstream) = ¤t_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 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 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 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 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 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 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 let repo = Repository::init(&repo_path).unwrap();
253
254 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 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 if let Ok(orig) = original_dir {
287 let _ = env::set_current_dir(orig);
288 }
289
290 assert!(result.is_ok());
291 }
292 Err(_) => {
293 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_repo(&repo_path, Some("https://test.bitbucket.com".to_string())).unwrap();
305
306 let original_dir = env::current_dir().map_err(|_| "Failed to get current dir");
308 match env::set_current_dir(&repo_path) {
309 Ok(_) => {
310 let result = run().await;
311
312 if let Ok(orig) = original_dir {
314 let _ = env::set_current_dir(orig);
315 }
316
317 assert!(result.is_ok());
318 }
319 Err(_) => {
320 println!("Skipping test due to directory access restrictions");
322 }
323 }
324 }
325}