mirror of
https://github.com/RGBCube/dix
synced 2025-07-31 21:57:46 +00:00
feat: diff printing
This commit is contained in:
parent
47bd2d9657
commit
07dd524d30
6 changed files with 158 additions and 236 deletions
25
Cargo.lock
generated
25
Cargo.lock
generated
|
@ -131,7 +131,7 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
|
|||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"textwrap",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -210,7 +210,7 @@ dependencies = [
|
|||
"clap 2.34.0",
|
||||
"criterion-plot",
|
||||
"csv",
|
||||
"itertools",
|
||||
"itertools 0.10.5",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
|
@ -232,7 +232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -320,6 +320,7 @@ dependencies = [
|
|||
"derive_more",
|
||||
"diff",
|
||||
"env_logger",
|
||||
"itertools 0.14.0",
|
||||
"libc",
|
||||
"log",
|
||||
"ref-cast",
|
||||
|
@ -327,6 +328,7 @@ dependencies = [
|
|||
"rusqlite",
|
||||
"rustc-hash",
|
||||
"thiserror",
|
||||
"unicode-width 0.2.0",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
|
@ -448,6 +450,15 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
|
@ -798,7 +809,7 @@ version = "0.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -849,6 +860,12 @@ version = "0.1.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
|
|
|
@ -11,12 +11,14 @@ clap-verbosity-flag = "3.0.2"
|
|||
derive_more = { version = "2.0.1", features = [ "full" ] }
|
||||
diff = "0.1.13"
|
||||
env_logger = "0.11.3"
|
||||
itertools = "0.14.0"
|
||||
log = "0.4.20"
|
||||
ref-cast = "1.0.24"
|
||||
regex = "1.11.1"
|
||||
rusqlite = { version = "0.35.0", features = [ "bundled" ] }
|
||||
rustc-hash = "2.1.1"
|
||||
thiserror = "2.0.12"
|
||||
unicode-width = "0.2.0"
|
||||
yansi = { version = "1.0.1", features = [ "detect-env", "detect-tty" ] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
87
src/diff.rs
87
src/diff.rs
|
@ -1,9 +1,15 @@
|
|||
use std::fmt;
|
||||
use std::fmt::{
|
||||
self,
|
||||
Write as _,
|
||||
};
|
||||
|
||||
use itertools::EitherOrBoth;
|
||||
use ref_cast::RefCast as _;
|
||||
use rustc_hash::{
|
||||
FxBuildHasher,
|
||||
FxHashMap,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr as _;
|
||||
use yansi::Paint as _;
|
||||
|
||||
use crate::{
|
||||
|
@ -26,17 +32,13 @@ enum DiffStatus {
|
|||
Changed,
|
||||
}
|
||||
|
||||
impl fmt::Display for DiffStatus {
|
||||
fn fmt(&self, writer: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
writer,
|
||||
"[{letter}]",
|
||||
letter = match *self {
|
||||
impl DiffStatus {
|
||||
fn char(self) -> impl fmt::Display {
|
||||
match self {
|
||||
DiffStatus::Added => "A".green(),
|
||||
DiffStatus::Removed => "R".red(),
|
||||
DiffStatus::Changed => "C".yellow(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,7 +76,10 @@ pub fn diff<'a>(
|
|||
|
||||
let mut diffs = paths
|
||||
.into_iter()
|
||||
.filter_map(|(name, versions)| {
|
||||
.filter_map(|(name, mut versions)| {
|
||||
versions.old.sort_unstable();
|
||||
versions.new.sort_unstable();
|
||||
|
||||
let status = match (versions.old.len(), versions.new.len()) {
|
||||
(0, 0) => unreachable!(),
|
||||
(0, _) => DiffStatus::Removed,
|
||||
|
@ -91,9 +96,15 @@ pub fn diff<'a>(
|
|||
a_status.cmp(&b_status).then_with(|| a_name.cmp(b_name))
|
||||
});
|
||||
|
||||
let name_width = diffs
|
||||
.iter()
|
||||
.map(|&(name, ..)| name.width())
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut last_status = None::<DiffStatus>;
|
||||
|
||||
for (name, _versions, status) in diffs {
|
||||
for (name, versions, status) in diffs {
|
||||
if last_status != Some(status) {
|
||||
last_status = Some(status);
|
||||
HEADER_STYLE.fmt_prefix(writer)?;
|
||||
|
@ -101,7 +112,59 @@ pub fn diff<'a>(
|
|||
HEADER_STYLE.fmt_suffix(writer)?;
|
||||
}
|
||||
|
||||
write!(writer, "{status} {name}")?;
|
||||
write!(
|
||||
writer,
|
||||
"[{status}] {name:<name_width$}",
|
||||
status = status.char()
|
||||
)?;
|
||||
|
||||
let mut oldacc = String::new();
|
||||
let mut newacc = String::new();
|
||||
|
||||
for diff in itertools::Itertools::zip_longest(
|
||||
versions.old.iter(),
|
||||
versions.new.iter(),
|
||||
) {
|
||||
match diff {
|
||||
// I have no idea why itertools is returning `versions.new` in `Left`.
|
||||
EitherOrBoth::Left(new) => {
|
||||
write!(
|
||||
newacc,
|
||||
" {new}",
|
||||
new = new.unwrap_or(Version::ref_cast("<none>")).green()
|
||||
)?;
|
||||
},
|
||||
|
||||
EitherOrBoth::Both(old, new) => {
|
||||
let (old, new) = Version::diff(
|
||||
old.unwrap_or(Version::ref_cast("<none>")),
|
||||
new.unwrap_or(Version::ref_cast("<none>")),
|
||||
);
|
||||
|
||||
write!(oldacc, " {old}")?;
|
||||
write!(newacc, " {new}")?;
|
||||
},
|
||||
|
||||
EitherOrBoth::Right(old) => {
|
||||
write!(
|
||||
oldacc,
|
||||
" {old}",
|
||||
old = old.unwrap_or(Version::ref_cast("<none>")).red()
|
||||
)?;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
write!(
|
||||
writer,
|
||||
"{oldacc}{arrow}{newacc}",
|
||||
arrow = if !oldacc.is_empty() && !newacc.is_empty() {
|
||||
" →"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
)?;
|
||||
|
||||
writeln!(writer)?;
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ impl StorePath {
|
|||
static STORE_PATH_REGEX: sync::LazyLock<regex::Regex> =
|
||||
sync::LazyLock::new(|| {
|
||||
regex::Regex::new("(.+?)(-([0-9].*?))?$")
|
||||
.expect("failed to compile regex pattern for nix store paths")
|
||||
.expect("failed to compile regex for Nix store paths")
|
||||
});
|
||||
|
||||
let path = self.to_str().with_context(|| {
|
||||
|
|
206
src/print.rs
206
src/print.rs
|
@ -1,206 +0,0 @@
|
|||
use core::str;
|
||||
use std::{
|
||||
collections::{
|
||||
HashMap,
|
||||
HashSet,
|
||||
},
|
||||
string::ToString,
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
use yansi::Paint;
|
||||
|
||||
/// diffs two strings character by character, and returns a tuple of strings
|
||||
/// colored in a way to represent the differences between the two input strings.
|
||||
///
|
||||
/// # Returns:
|
||||
///
|
||||
/// * (String, String) - The differing chars being red in the left, and green in
|
||||
/// the right one.
|
||||
fn diff_versions(left: &str, right: &str) -> (String, String) {
|
||||
let mut prev = "\x1b[33m".to_string();
|
||||
let mut post = "\x1b[33m".to_string();
|
||||
|
||||
// We only have to filter the left once, since we stop if the left one is
|
||||
// empty. We do this to display things like -man, -dev properly.
|
||||
let matches = name_regex().captures(left);
|
||||
let mut suffix = String::new();
|
||||
|
||||
if let Some(m) = matches {
|
||||
let tmp = m.get(0).map_or("", |m| m.as_str());
|
||||
suffix.push_str(tmp);
|
||||
}
|
||||
// string without the suffix
|
||||
let filtered_left = &left[..left.len() - suffix.len()];
|
||||
let filtered_right = &right[..right.len() - suffix.len()];
|
||||
|
||||
for diff in diff::chars(filtered_left, filtered_right) {
|
||||
match diff {
|
||||
diff::Result::Both(l, _) => {
|
||||
let string_to_push = format!("{l}");
|
||||
prev.push_str(&string_to_push);
|
||||
post.push_str(&string_to_push);
|
||||
},
|
||||
diff::Result::Left(l) => {
|
||||
let string_to_push = format!("\x1b[1;91m{l}");
|
||||
prev.push_str(&string_to_push);
|
||||
},
|
||||
|
||||
diff::Result::Right(r) => {
|
||||
let string_to_push = format!("\x1b[1;92m{r}");
|
||||
post.push_str(&string_to_push);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// push removed suffix
|
||||
prev.push_str(&format!("\x1b[33m{}", &suffix));
|
||||
post.push_str(&format!("\x1b[33m{}", &suffix));
|
||||
|
||||
// reset
|
||||
prev.push_str("\x1b[0m");
|
||||
post.push_str("\x1b[0m");
|
||||
|
||||
(prev, post)
|
||||
}
|
||||
|
||||
/// print the packages added between two closures.
|
||||
pub fn print_added(
|
||||
set: &HashSet<&str>,
|
||||
post: &HashMap<&str, HashSet<&str>>,
|
||||
col_width: usize,
|
||||
) {
|
||||
println!("{}", "Packages added:".underline().bold());
|
||||
|
||||
// Use sorted outpu
|
||||
let mut sorted: Vec<_> = set
|
||||
.iter()
|
||||
.filter_map(|p| post.get(p).map(|ver| (*p, ver)))
|
||||
.collect();
|
||||
|
||||
// Sort by package name for consistent output
|
||||
sorted.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
|
||||
for (p, ver) in sorted {
|
||||
let mut version_vec = ver.iter().copied().collect::<Vec<_>>();
|
||||
version_vec.sort_unstable();
|
||||
let version_str = version_vec.join(", ");
|
||||
println!(
|
||||
"[{}] {:col_width$} \x1b[33m{}\x1b[0m",
|
||||
"A:".green().bold(),
|
||||
p,
|
||||
version_str
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// print the packages removed between two closures.
|
||||
pub fn print_removed(
|
||||
set: &HashSet<&str>,
|
||||
pre: &HashMap<&str, HashSet<&str>>,
|
||||
col_width: usize,
|
||||
) {
|
||||
println!("{}", "Packages removed:".underline().bold());
|
||||
|
||||
// Use sorted output for more predictable and readable results
|
||||
let mut sorted: Vec<_> = set
|
||||
.iter()
|
||||
.filter_map(|p| pre.get(p).map(|ver| (*p, ver)))
|
||||
.collect();
|
||||
|
||||
// Sort by package name for consistent output
|
||||
sorted.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
|
||||
for (p, ver) in sorted {
|
||||
let mut version_vec = ver.iter().copied().collect::<Vec<_>>();
|
||||
version_vec.sort_unstable();
|
||||
let version_str = version_vec.join(", ");
|
||||
println!(
|
||||
"[{}] {:col_width$} \x1b[33m{}\x1b[0m",
|
||||
"R:".red().bold(),
|
||||
p,
|
||||
version_str
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_changes(
|
||||
set: &HashSet<&str>,
|
||||
pre: &HashMap<&str, HashSet<&str>>,
|
||||
post: &HashMap<&str, HashSet<&str>>,
|
||||
col_width: usize,
|
||||
) {
|
||||
println!("{}", "Versions changed:".underline().bold());
|
||||
|
||||
// Use sorted output for more predictable and readable results
|
||||
let mut changes = Vec::new();
|
||||
|
||||
for p in set.iter().filter(|p| !p.is_empty()) {
|
||||
if let (Some(ver_pre), Some(ver_post)) = (pre.get(p), post.get(p)) {
|
||||
if ver_pre != ver_post {
|
||||
changes.push((*p, ver_pre, ver_post));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by package name for consistent output
|
||||
changes.sort_by(|(a, ..), (b, ..)| a.cmp(b));
|
||||
|
||||
for (p, ver_pre, ver_post) in changes {
|
||||
let mut version_vec_pre =
|
||||
ver_pre.difference(ver_post).copied().collect::<Vec<_>>();
|
||||
let mut version_vec_post =
|
||||
ver_post.difference(ver_pre).copied().collect::<Vec<_>>();
|
||||
|
||||
version_vec_pre.sort_unstable();
|
||||
version_vec_post.sort_unstable();
|
||||
|
||||
let mut diffed_pre: String;
|
||||
let diffed_post: String;
|
||||
|
||||
if version_vec_pre.len() == version_vec_post.len() {
|
||||
let mut diff_pre: Vec<String> = vec![];
|
||||
let mut diff_post: Vec<String> = vec![];
|
||||
|
||||
for (pre, post) in version_vec_pre.iter().zip(version_vec_post.iter()) {
|
||||
let (a, b) = diff_versions(pre, post);
|
||||
diff_pre.push(a);
|
||||
diff_post.push(b);
|
||||
}
|
||||
diffed_pre = diff_pre.join(", ");
|
||||
diffed_post = diff_post.join(", ");
|
||||
} else {
|
||||
let version_str_pre = version_vec_pre.join(", ");
|
||||
let version_str_post = version_vec_post.join(", ");
|
||||
(diffed_pre, diffed_post) =
|
||||
diff_versions(&version_str_pre, &version_str_post);
|
||||
}
|
||||
|
||||
// push a space to the diffed_pre, if it is non-empty, we do this here and
|
||||
// not in the println in order to properly align the ±.
|
||||
if !version_vec_pre.is_empty() {
|
||||
let mut tmp = " ".to_string();
|
||||
tmp.push_str(&diffed_pre);
|
||||
diffed_pre = tmp;
|
||||
}
|
||||
|
||||
println!(
|
||||
"[{}] {:col_width$}{} \x1b[0m\u{00B1}\x1b[0m {}",
|
||||
"C:".bold().bright_yellow(),
|
||||
p,
|
||||
diffed_pre,
|
||||
diffed_post
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a reference to the compiled regex pattern.
|
||||
// The regex is compiled only once.
|
||||
fn name_regex() -> &'static Regex {
|
||||
static REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
REGEX.get_or_init(|| {
|
||||
Regex::new("(-man|-lib|-doc|-dev|-out|-terminfo)")
|
||||
.expect("Failed to compile regex pattern for name")
|
||||
})
|
||||
}
|
|
@ -1,13 +1,19 @@
|
|||
use std::cmp;
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::Write as _,
|
||||
sync,
|
||||
};
|
||||
|
||||
use derive_more::{
|
||||
Deref,
|
||||
DerefMut,
|
||||
Display,
|
||||
From,
|
||||
};
|
||||
use ref_cast::RefCast;
|
||||
use yansi::Paint as _;
|
||||
|
||||
#[derive(RefCast, Deref, Debug, PartialEq, Eq)]
|
||||
#[derive(RefCast, Deref, Display, Debug, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
pub struct Version(str);
|
||||
|
||||
|
@ -26,7 +32,47 @@ impl cmp::Ord for Version {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
impl Version {
|
||||
pub fn diff(old: &Version, new: &Version) -> (String, String) {
|
||||
static NAME_SUFFIX_REGEX: sync::LazyLock<regex::Regex> =
|
||||
sync::LazyLock::new(|| {
|
||||
regex::Regex::new("(-man|-lib|-doc|-dev|-out|-terminfo)")
|
||||
.expect("failed to compile regex for Nix store path versions")
|
||||
});
|
||||
|
||||
let matches = NAME_SUFFIX_REGEX.captures(old);
|
||||
let suffix = matches.map_or("", |matches| {
|
||||
matches.get(0).map_or("", |capture| capture.as_str())
|
||||
});
|
||||
|
||||
let old = old.strip_suffix(suffix).unwrap_or(old);
|
||||
let new = new.strip_suffix(suffix).unwrap_or(new);
|
||||
|
||||
let mut oldacc = String::new();
|
||||
let mut newacc = String::new();
|
||||
|
||||
for diff in diff::chars(old, new) {
|
||||
match diff {
|
||||
diff::Result::Left(oldc) => {
|
||||
write!(oldacc, "{oldc}", oldc = oldc.red()).unwrap();
|
||||
},
|
||||
|
||||
diff::Result::Both(oldc, newc) => {
|
||||
write!(oldacc, "{oldc}", oldc = oldc.yellow()).unwrap();
|
||||
write!(newacc, "{newc}", newc = newc.yellow()).unwrap();
|
||||
},
|
||||
|
||||
diff::Result::Right(newc) => {
|
||||
write!(newacc, "{newc}", newc = newc.green()).unwrap();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
(oldacc, newacc)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
enum VersionComponent<'a> {
|
||||
Number(u64),
|
||||
Text(&'a str),
|
||||
|
@ -45,10 +91,10 @@ impl cmp::Ord for VersionComponent<'_> {
|
|||
Text,
|
||||
};
|
||||
|
||||
match (self, other) {
|
||||
(Number(x), Number(y)) => x.cmp(y),
|
||||
match (*self, *other) {
|
||||
(Number(x), Number(y)) => x.cmp(&y),
|
||||
(Text(x), Text(y)) => {
|
||||
match (*x, *y) {
|
||||
match (x, y) {
|
||||
("pre", _) => cmp::Ordering::Less,
|
||||
(_, "pre") => cmp::Ordering::Greater,
|
||||
_ => x.cmp(y),
|
||||
|
@ -79,8 +125,8 @@ impl<'a> Iterator for VersionComponentIter<'a> {
|
|||
// Based on this collect characters after this into the component.
|
||||
let component_len = self
|
||||
.chars()
|
||||
.take_while(|&c| {
|
||||
c.is_ascii_digit() == is_digit && !matches!(c, '.' | '-')
|
||||
.take_while(|&char| {
|
||||
char.is_ascii_digit() == is_digit && !matches!(char, '.' | '-')
|
||||
})
|
||||
.map(char::len_utf8)
|
||||
.sum();
|
||||
|
@ -116,10 +162,10 @@ mod tests {
|
|||
Number(132),
|
||||
Number(1),
|
||||
Number(2),
|
||||
Text("test".into()),
|
||||
Text("test"),
|
||||
Number(234),
|
||||
Number(1),
|
||||
Text("man".into())
|
||||
Text("man")
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue