dev_prune/commands/
status.rs1use anyhow::Result;
11use std::io::{self, IsTerminal};
12
13use crate::commands::hook::HookState;
14use crate::config::Registry;
15use crate::engine::{self, PruneStatus};
16use crate::output;
17use crate::tui::status_view;
18
19pub fn run(json_output: bool) -> Result<()> {
25 let mut registry = Registry::load()?;
26
27 let daemon_st = crate::daemon::daemon_status()
28 .map(|s| s.to_string())
29 .unwrap_or_else(|_| "Unknown".to_string());
30 let hook_st = match crate::commands::hook::state() {
34 Ok(HookState::Active) => "Active (post-commit, post-checkout, post-merge)".to_string(),
35 Ok(HookState::Chained { previous, drifted }) if drifted.is_empty() => {
36 format!("Active, chained to {previous}")
37 }
38 Ok(HookState::Chained { previous, drifted }) => format!(
39 "Active, chained to {previous} ({} hook(s) not forwarded)",
40 drifted.len()
41 ),
42 Ok(HookState::Foreign(path)) => format!("Inactive (core.hooksPath belongs to {path})"),
43 Ok(HookState::Absent) | Err(_) => "Inactive".to_string(),
44 };
45
46 if json_output {
47 let repos = engine::get_full_status(®istry);
48 return crate::json::emit(&crate::json::status_document(
49 ®istry, &repos, &daemon_st, &hook_st,
50 ));
51 }
52
53 output::print_banner();
54
55 if crate::commands::update::notify_if_outdated(&mut registry) {
58 let _ = registry.save();
59 }
60
61 let reg_path = Registry::registry_path()
62 .map(|p| output::clean_path(&p))
63 .unwrap_or_else(|_| "unknown".to_string());
64
65 output::print_info(&format!("Global Config Location: {}", reg_path));
66 output::print_info(&format!("Background OS Daemon: {}", daemon_st));
67 output::print_info(&format!("Background Git Hooks: {}", hook_st));
68 let timeout = registry.settings.command_timeout_secs;
71 output::print_info(&format!(
72 "Global Command Timeout: {timeout}s ({})",
73 format_duration(timeout)
74 ));
75 if registry.settings.min_size_mb > 0 {
76 output::print_info(&format!(
77 "Minimum Directory Size: {} MiB (smaller ones are left alone)",
78 registry.settings.min_size_mb
79 ));
80 }
81 output::print_info(&format!(
82 "Tracked Repositories: {}",
83 registry.repo_count()
84 ));
85 output::print_info(&format!(
86 "Historical Space Saved: {} across {} prune passes",
87 output::format_bytes(registry.total_freed_bytes),
88 registry.total_pruned_count
89 ));
90 println!();
91
92 let repos = engine::get_full_status(®istry);
94
95 if io::stdout().is_terminal() {
96 let registry_ref = ®istry;
99 match status_view::render_status_tui(&|| engine::get_full_status(registry_ref)) {
100 Ok(Some(selected_indices)) if !selected_indices.is_empty() => {
101 let candidates: Vec<_> = selected_indices
103 .iter()
104 .map(|&i| repos[i].path.clone())
105 .collect();
106
107 output::print_header("Pruning Selected Repositories");
108
109 let mut total_freed: u64 = 0;
110 let mut pruned_count = 0;
111 let mut error_count = 0;
112
113 for path in &candidates {
114 let results = engine::prune_repo(path, 0, false, true);
115 for result in results {
116 match &result.status {
117 PruneStatus::Pruned => {
118 total_freed += result.size_freed;
119 pruned_count += 1;
120 registry.mark_pruned(&result.repo_path, result.size_freed);
121 output::print_success(&format!(
122 "{} → {} ({}) — {}",
123 output::clean_path(&result.repo_path),
124 result.bloat_dir,
125 output::format_bytes(result.size_freed),
126 result.adapter_name,
127 ));
128 }
129 PruneStatus::LockfileError(e) => {
130 error_count += 1;
131 output::print_error(&format!(
132 "{} lockfile sync failed: {}",
133 output::clean_path(&result.repo_path),
134 e,
135 ));
136 }
137 PruneStatus::DeleteError(e) => {
138 error_count += 1;
139 output::print_error(&format!(
140 "{} delete failed: {}",
141 output::clean_path(&result.repo_path),
142 e,
143 ));
144 }
145 PruneStatus::ConfigError(e) => {
146 error_count += 1;
147 output::print_error(&format!(
148 "{} skipped — unreadable .devprune.json: {}",
149 output::clean_path(&result.repo_path),
150 e,
151 ));
152 }
153 _ => {}
154 }
155 }
156 }
157
158 registry.save()?;
159
160 output::print_header("Summary");
161 output::print_success(&format!(
162 "Freed: {} across {pruned_count} directories",
163 output::format_bytes(total_freed)
164 ));
165 if error_count > 0 {
168 anyhow::bail!("{error_count} directories could not be pruned.");
169 }
170 }
171 Ok(_) => {
172 }
174 Err(e) => {
175 output::print_warning(&format!("Interactive view ended: {e:#}"));
179 status_view::render_status_plain(&repos);
180 }
181 }
182 } else {
183 status_view::render_status_plain(&repos);
185 }
186
187 Ok(())
188}
189
190fn format_duration(secs: u64) -> String {
192 match secs {
193 s if s > 0 && s % 3600 == 0 => format!("{}h", s / 3600),
194 s if s > 0 && s % 60 == 0 => format!("{}m", s / 60),
195 s => format!("{s}s"),
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn the_timeout_is_described_in_whatever_unit_fits_it() {
205 assert_eq!(format_duration(600), "10m");
207 assert_eq!(format_duration(3600), "1h");
208 assert_eq!(format_duration(90), "90s");
209 assert_eq!(format_duration(0), "0s");
210 }
211}