diff --git a/release.nix b/release.nix index 28e91cf..e13af4e 100644 --- a/release.nix +++ b/release.nix @@ -5,6 +5,13 @@ # TODO: change the version number to the actual lorri versions, (and transfer the changelog to a real conventional changelog file, then update lorri self-upgrade (maybe use toml?)) # Find the current version number with `git log --pretty=%h | wc -l` entries = [ + { + version = 1111; + changes = '' + `lorri gc info`: print “gone” instead of “dead”. Order output + by most recently built. + ''; + } { version = 1107; changes = '' diff --git a/src/lib.rs b/src/lib.rs index 84f1cf5..3057802 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,9 +29,10 @@ pub mod project; pub mod socket; pub mod watch; +use std::cmp::Reverse; use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use std::time::Duration; +use std::time::{Duration, SystemTime}; use std::{env, io}; // OUT_DIR and build_rev.rs are generated by cargo, see ../build.rs @@ -235,24 +236,59 @@ impl From for DrvFile { } } -/** Pretty print how long ago a Duration was (taken as difference between now and some instance in the past) */ -fn pretty_time_ago(dur: Duration) -> String { - let secs = dur.as_secs(); - let mins = dur.as_secs() / 60; - let hours = dur.as_secs() / (60 * 60); - let days = dur.as_secs() / (60 * 60 * 24); +/// How long ago something happened. +/// Ordering based on showing more recent last. +#[derive(PartialEq, Eq, PartialOrd, Ord)] +pub enum TimeAgo { + /// multiple days ago + Days(Reverse), + /// multiple Hours ago + Hours(Reverse), + /// multiple Minutes ago + Mins(Reverse), + /// multiple Seconds ago + Secs(Reverse), +} - if days > 0 { - return format!("{} days ago", days); - } - if hours > 0 { - return format!("{} hours ago", hours); +/// A simple way of displaying how long something happened +impl TimeAgo { + /// Given the reference time, return how long ago something happened + /// (if it’s in the future return zero seconds) + pub fn from_system_time(now: SystemTime, ago: SystemTime) -> TimeAgo { + TimeAgo::from_duration(now.duration_since(ago).unwrap_or(Duration::ZERO)) } - if mins > 0 { - return format!("{} minutes ago", mins); + + /// Duration to TimeAgo + pub fn from_duration(dur: Duration) -> TimeAgo { + let secs = dur.as_secs(); + let mins = dur.as_secs() / 60; + let hours = dur.as_secs() / (60 * 60); + let days = dur.as_secs() / (60 * 60 * 24); + + if days > 0 { + return TimeAgo::Days(Reverse(days)); + } + if hours > 0 { + return TimeAgo::Hours(Reverse(hours)); + } + if mins > 0 { + return TimeAgo::Mins(Reverse(mins)); + } + + TimeAgo::Secs(Reverse(secs)) } +} - format!("{} seconds ago", secs) +/** Pretty print how long ago a Duration was (taken as difference between now and some instance in the past) */ +fn pretty_time_ago(dur: Duration) -> String { + let ago = TimeAgo::from_duration(dur); + + match ago { + TimeAgo::Secs(secs) => format!("{} seconds ago", secs.0), + TimeAgo::Mins(mins) => return format!("{} minutes ago", mins.0), + TimeAgo::Hours(hours) => return format!("{} hours ago", hours.0), + TimeAgo::Days(days) => return format!("{} days ago", days.0), + } } #[cfg(test)] diff --git a/src/ops.rs b/src/ops.rs index 85f43e3..d90483e 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -34,7 +34,7 @@ use std::{collections::HashSet, env, fs::File}; use anyhow::Context; use crate::daemon::client::Timeout; -use crate::project::{GcRootInfo, Project, ProjectFile}; +use crate::project::{GcRootInfo, ListRootsSort, Project, ProjectFile}; use crate::socket::communicate; use itertools::Itertools; use serde_json::{json, Value}; @@ -773,9 +773,9 @@ async fn main_run_once( /// Print or remove gc roots depending on cli options. pub fn op_gc(logger: &slog::Logger, opts: cli::GcOptions, paths: &Paths) -> Result<(), ExitError> { - let infos = project::list_roots(logger, paths)?; match opts.action { cli::GcSubcommand::Info => { + let infos = project::list_roots(logger, paths, ListRootsSort::MoreRecentLast)?; if opts.json { serde_json::to_writer( std::io::stdout(), @@ -786,7 +786,7 @@ pub fn op_gc(logger: &slog::Logger, opts: cli::GcOptions, paths: &Paths) -> Resu "gc_dir": info.gc_dir.to_json_string(), "nix_file": info.nix_file.to_json_string(), "timestamp": info.timestamp, - "alive": info.alive + "alive": info.project_file_exists }) }) .collect::>(), @@ -805,10 +805,11 @@ pub fn op_gc(logger: &slog::Logger, opts: cli::GcOptions, paths: &Paths) -> Resu dry_run, } => { let files_to_remove: HashSet = shell_file.into_iter().collect(); + let infos = project::list_roots(logger, paths, ListRootsSort::NoSorting)?; let to_remove: Vec<(GcRootInfo, Project)> = infos .into_iter() .filter(|(info, _project)| { - all || !info.alive + all || !info.project_file_exists || files_to_remove.contains(info.nix_file.as_path()) || older_than.map_or(false, |limit| { match info.timestamp { @@ -849,7 +850,7 @@ pub fn op_gc(logger: &slog::Logger, opts: cli::GcOptions, paths: &Paths) -> Resu "nix_file": info.nix_file.to_json_string(), // we use the Serialize instance for SystemTime "timestamp": info.timestamp, - "alive": info.alive + "alive": info.project_file_exists } }), Ok(info) => json!({ @@ -859,7 +860,7 @@ pub fn op_gc(logger: &slog::Logger, opts: cli::GcOptions, paths: &Paths) -> Resu "nix_file": info.nix_file.to_json_string(), // we use the Serialize instance for SystemTime "timestamp": info.timestamp, - "alive": info.alive + "alive": info.project_file_exists } }), }) diff --git a/src/project.rs b/src/project.rs index 1760472..a7f77fa 100644 --- a/src/project.rs +++ b/src/project.rs @@ -7,7 +7,7 @@ use crate::builder::{OutputPath, RootedPath}; use crate::constants::Paths; use crate::nix::StorePath; use crate::ops::error::ExitError; -use crate::{pretty_time_ago, AbsPathBuf, Installable, NixFile}; +use crate::{pretty_time_ago, AbsPathBuf, Installable, NixFile, TimeAgo}; use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; @@ -318,10 +318,20 @@ impl AddRootError { } } +/// How the list_roots result should be sorted +#[derive(Debug, Clone, Copy)] +pub enum ListRootsSort { + /// Return in arbitrary order + NoSorting, + /// Return most recent items last, sort secondary by project_file path name + MoreRecentLast, +} + /// Returns a list of existing gc roots along with some metadata pub fn list_roots( logger: &slog::Logger, paths: &Paths, + list_roots_sort: ListRootsSort, ) -> Result, ExitError> { let mut res = Vec::new(); let gc_root_dir_iter = std::fs::read_dir(paths.gc_root_dir()).map_err(|e| { @@ -380,16 +390,27 @@ pub fn list_roots( gc_dir: project.project_root_dir.clone(), nix_file: project.project_file.as_nix_file().0, timestamp, - alive, + project_file_exists: alive, }, project, )); } + match list_roots_sort { + ListRootsSort::NoSorting => {} + ListRootsSort::MoreRecentLast => { + let now = SystemTime::now(); + res.sort_by_key(|r| { + ( + r.0.timestamp.map(|t| TimeAgo::from_system_time(now, t)), + r.1.project_file.as_nix_file().as_absolute_path().to_owned(), + ) + }) + } + } Ok(res) } /// Represents a gc root along with some metadata, used for json output of lorri gc info -#[derive(Serialize)] pub struct GcRootInfo { /// directory where root is stored pub gc_dir: AbsPathBuf, @@ -398,7 +419,7 @@ pub struct GcRootInfo { /// timestamp of the last build pub timestamp: Option, /// whether `nix_file` still exists - pub alive: bool, + pub project_file_exists: bool, } impl GcRootInfo { @@ -410,7 +431,11 @@ impl GcRootInfo { Some(Err(_)) => "future".to_string(), Some(Ok(d)) => pretty_time_ago(d), }; - let alive = if self.alive { "" } else { "[dead]" }; + let alive = if self.project_file_exists { + "" + } else { + "[gone]" + }; format!( "{} -> {} {} ({})", self.gc_dir.display(), diff --git a/tests/integration/gc.rs b/tests/integration/gc.rs index 0a58edd..169b949 100644 --- a/tests/integration/gc.rs +++ b/tests/integration/gc.rs @@ -98,10 +98,10 @@ derivation { let backup_file = project.join("shell.nix.bak"); std::fs::rename(&nix_file, &backup_file).expect("rename"); assert!(std::fs::metadata(&nix_file_symlink).is_err()); - // it should be labeled as dead, but not removed by --print-roots + // it should be labeled as gone, but not removed by --print-roots let out = run_lorri(vec!["gc", "info"]).unwrap(); assert!(&out.contains(&subdir.display().to_string())); - assert!(out.contains("[dead]")); + assert!(out.contains("[gone]")); // now remove it let out = run_lorri(vec!["gc", "--json", "rm"]).unwrap(); assert!(&out.contains(&subdir.display().to_string()));