Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ops): gc info: print [gone] instead of [dead], sort by last updated #149

Merged
merged 2 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions release.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''
Expand Down
66 changes: 51 additions & 15 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -235,24 +236,59 @@ impl From<PathBuf> 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<u64>),
/// multiple Hours ago
Hours(Reverse<u64>),
/// multiple Minutes ago
Mins(Reverse<u64>),
/// multiple Seconds ago
Secs(Reverse<u64>),
}

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)]
Expand Down
13 changes: 7 additions & 6 deletions src/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(),
Expand All @@ -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::<Vec<_>>(),
Expand All @@ -805,10 +805,11 @@ pub fn op_gc(logger: &slog::Logger, opts: cli::GcOptions, paths: &Paths) -> Resu
dry_run,
} => {
let files_to_remove: HashSet<PathBuf> = 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 {
Expand Down Expand Up @@ -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!({
Expand All @@ -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
}
}),
})
Expand Down
35 changes: 30 additions & 5 deletions src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Vec<(GcRootInfo, Project)>, ExitError> {
let mut res = Vec::new();
let gc_root_dir_iter = std::fs::read_dir(paths.gc_root_dir()).map_err(|e| {
Expand Down Expand Up @@ -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,
Expand All @@ -398,7 +419,7 @@ pub struct GcRootInfo {
/// timestamp of the last build
pub timestamp: Option<SystemTime>,
/// whether `nix_file` still exists
pub alive: bool,
pub project_file_exists: bool,
}

impl GcRootInfo {
Expand All @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down
Loading