use anyhow::Result;
use chrono::{DateTime, Utc};
use crate::config::Registry;
use crate::constants::HISTORY_STARTS_AT;
use crate::output;
const PASSES_SHOWN: usize = 10;
const REPOS_SHOWN: usize = 10;
pub fn run(json_output: bool) -> Result<()> {
let registry = Registry::load()?;
if json_output {
return crate::json::emit(&crate::json::stats_document(®istry));
}
output::print_header("Lifetime");
output::print_info(&format!(
"Space reclaimed: {}",
output::format_bytes_styled(registry.total_freed_bytes)
));
output::print_info(&format!(
"Prune passes: {}",
registry.total_pruned_count
));
output::print_info(&format!(
"Repositories: {} tracked",
registry.repo_count()
));
print_last_pass(®istry);
print_recent_passes(®istry);
print_biggest_repositories(®istry);
Ok(())
}
fn print_last_pass(registry: &Registry) {
output::print_header("Most recent pass");
let Some(last) = ®istry.last_prune else {
output::print_info(
"Nothing recorded yet — `devp run --dry-run` shows what a pass would do.",
);
return;
};
let bytes: u64 = last.dirs.iter().map(|d| d.size_freed).sum();
output::print_info(&format!(
"{} ({}) — {} from {} {}",
last.at.format("%Y-%m-%d %H:%M UTC"),
describe_age(last.at),
output::format_bytes_styled(bytes),
last.dirs.len(),
output::plural(last.dirs.len(), "directory", "directories"),
));
output::print_info("Put it back with: devp restore --last-run");
}
fn print_recent_passes(registry: &Registry) {
if registry.prune_history.is_empty() {
return;
}
output::print_header("Recent passes");
for summary in registry.prune_history.iter().rev().take(PASSES_SHOWN) {
use colored::Colorize;
println!(
" {} {} {} {} across {} {}",
summary.at.format("%Y-%m-%d %H:%M"),
format!("{:>10}", output::format_bytes(summary.bytes_freed)).green(),
summary.dirs_removed,
output::plural(summary.dirs_removed, "directory", "directories"),
summary.repos_touched,
output::plural(summary.repos_touched, "repository", "repositories"),
);
}
let total = registry.prune_history.len();
if total > PASSES_SHOWN {
output::print_info(&format!(
"{total} passes recorded; showing the last {PASSES_SHOWN}."
));
}
}
fn print_biggest_repositories(registry: &Registry) {
let mut ranked: Vec<_> = registry
.repositories
.iter()
.filter(|(_, entry)| entry.total_freed_bytes > 0)
.collect();
output::print_header("Biggest reclaims");
if ranked.is_empty() {
output::print_info(&format!(
"No per-repository figures yet — these are recorded from {HISTORY_STARTS_AT} onward."
));
return;
}
ranked.sort_by(|a, b| {
b.1.total_freed_bytes
.cmp(&a.1.total_freed_bytes)
.then_with(|| a.0.cmp(b.0))
});
for (path, entry) in ranked.iter().take(REPOS_SHOWN) {
use colored::Colorize;
let last = entry
.last_pruned_at
.map(|at| format!("last pruned {}", describe_age(at)))
.unwrap_or_else(|| "never pruned by this install".to_string());
println!(
" {} {} ({last})",
format!("{:>10}", output::format_bytes(entry.total_freed_bytes)).green(),
output::styled_path(path),
);
}
}
fn describe_age(at: DateTime<Utc>) -> String {
let elapsed = Utc::now().signed_duration_since(at);
let days = elapsed.num_days();
if days >= 1 {
return format!(
"{days} {} ago",
output::plural(days as usize, "day", "days")
);
}
let hours = elapsed.num_hours();
if hours >= 1 {
return format!(
"{hours} {} ago",
output::plural(hours as usize, "hour", "hours")
);
}
let minutes = elapsed.num_minutes().max(0);
format!(
"{minutes} {} ago",
output::plural(minutes as usize, "minute", "minutes")
)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
#[test]
fn an_age_is_described_in_the_coarsest_unit_that_fits() {
assert_eq!(describe_age(Utc::now() - Duration::days(3)), "3 days ago");
assert_eq!(describe_age(Utc::now() - Duration::days(1)), "1 day ago");
assert_eq!(describe_age(Utc::now() - Duration::hours(5)), "5 hours ago");
assert_eq!(
describe_age(Utc::now() - Duration::minutes(2)),
"2 minutes ago"
);
}
#[test]
fn a_timestamp_in_the_future_does_not_render_as_negative() {
assert_eq!(
describe_age(Utc::now() + Duration::minutes(5)),
"0 minutes ago"
);
}
}