cascade_cli/cli/commands/
status.rs1use 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
6pub async fn run() -> Result<()> {
8 println!("š Repository Overview");
9 println!("āāāāāāāāāāāāāāāāāāāāāā");
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 println!("ā 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 println!("\nš§ Git Repository:");
34
35 let repo_info = git_repo.get_info()?;
36
37 println!(" Path: {}", repo_info.path.display());
39
40 if let Some(branch) = &repo_info.head_branch {
42 println!(" Current branch: {branch}");
43 } else {
44 println!(" Current branch: (detached HEAD)");
45 }
46
47 if let Some(commit) = &repo_info.head_commit {
49 println!(" HEAD commit: {}", &commit[..12]);
50 }
51
52 if repo_info.is_dirty {
54 println!(" Working directory: ā ļø Has uncommitted changes");
55 } else {
56 println!(" Working directory: ā
Clean");
57 }
58
59 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 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 'cc init' to initialize this repository for Cascade");
94 return Ok(());
95 }
96
97 println!(" 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 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 println!("\nāļø Configuration:");
144 if config_complete {
145 println!(" Status: ā
Ready for use");
146 } else {
147 println!(" Status: ā ļø Incomplete configuration");
148 println!(" Run 'cc config list' to see all settings");
149 println!(" Run 'cc doctor' for configuration recommendations");
150 }
151
152 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 'cc stacks create \"Stack Name\"' to create your first stack");
163 } else {
164 println!(" Total stacks: {}", stacks.len());
165
166 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 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 'cc 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 let repo = Repository::init(&repo_path).unwrap();
226
227 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 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 if let Ok(orig) = original_dir {
260 let _ = env::set_current_dir(orig);
261 }
262
263 assert!(result.is_ok());
264 }
265 Err(_) => {
266 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_repo(&repo_path, Some("https://test.bitbucket.com".to_string())).unwrap();
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}