From cf7f6d5245a6ba2db805c4e15af343139f31ee88 Mon Sep 17 00:00:00 2001 From: isabel Date: Tue, 13 May 2025 04:27:57 +0100 Subject: [PATCH] feat: add version parsing --- Cargo.toml | 8 +++ src/diff.rs | 10 +++- src/main.rs | 5 +- src/package.rs | 138 ++++++++++++++++++++++++++++++++-------------- src/parser.rs | 102 +++++++++++++++++----------------- src/versioning.rs | 75 +++++++++++++++++++++++++ 6 files changed, 240 insertions(+), 98 deletions(-) create mode 100644 src/versioning.rs diff --git a/Cargo.toml b/Cargo.toml index 3314e5a..d17a189 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,11 @@ serde_json = "1.0.140" all = "warn" pedantic = "warn" unreadable_literal = { level = "allow", priority = 1 } + +[profile.release] +opt-level = "z" +lto = "fat" +codegen-units = 1 +panic = "abort" +strip = true + diff --git a/src/diff.rs b/src/diff.rs index e2dcef8..a70413f 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -2,8 +2,10 @@ use humansize::{format_size, DECIMAL}; use nu_ansi_term::Color::{self, Green, Red, Yellow}; use std::collections::BTreeMap; -use crate::package::{DiffType, Package}; -use crate::parser::DiffRoot; +use super::{ + package::{DiffType, Package}, + parser::DiffRoot, +}; #[derive(Debug)] pub struct PackageListDiff { @@ -24,7 +26,9 @@ impl From for PackageListDiff { longest_name: 0, }; - for (name, package) in diff.packages { + for (name, diff_package) in diff.packages { + let package = Package::from(diff_package); + out.size_delta += package.size_delta; out.longest_name = out.longest_name.max(name.len()); diff --git a/src/main.rs b/src/main.rs index 48ada21..3fd89d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,9 @@ use nu_ansi_term::{Color, Style}; mod diff; mod package; mod parser; +mod versioning; + +use self::parser::DiffRoot; #[derive(FromArgs, PartialEq, Debug)] /// List the package differences between two `NixOS` generations @@ -45,7 +48,7 @@ fn main() -> Result<()> { std::process::exit(1); } - let packages: PackageListDiff = parser::diff(&before, &after)?.into(); + let packages: PackageListDiff = DiffRoot::new(&before, &after)?.into(); let arrow_style = Style::new().bold().fg(Color::LightGray); diff --git a/src/package.rs b/src/package.rs index 8a64f31..b3b95de 100644 --- a/src/package.rs +++ b/src/package.rs @@ -1,8 +1,9 @@ -use std::borrow::Cow; +use std::{cmp::Ordering, fmt::Display}; -use nu_ansi_term::Color::{Green, Red}; -use serde::de::Deserializer; -use serde::Deserialize; +use super::{ + parser::DiffPackage, + versioning::{Version, VersionComponent, VersionList}, +}; #[derive(Default, Debug, PartialEq, Eq)] pub enum DiffType { @@ -14,37 +15,13 @@ pub enum DiffType { Unknown, } -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] +#[derive(Debug)] pub struct Package { pub size_delta: i64, - - #[serde(deserialize_with = "version_deserializer")] - pub versions_before: Vec, - - #[serde(deserialize_with = "version_deserializer")] - pub versions_after: Vec, - - /// This is not a part of the JSON schema, but is used to determine the type of diff - #[serde(skip)] pub diff_type: DiffType, -} -fn version_deserializer<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let vec = Vec::>::deserialize(deserializer)?; - Ok(vec - .into_iter() - .map(|s| { - if s.is_empty() { - "".to_string() - } else { - s.into_owned() - } - }) - .collect()) + pub versions_before: VersionList, + pub versions_after: VersionList, } impl DiffType { @@ -53,28 +30,22 @@ impl DiffType { (true, false) => DiffType::Added, (false, true) => DiffType::Removed, (false, false) => DiffType::Changed, - (true, true) => DiffType::Unknown, // should be unreachable but im not sure + (true, true) => DiffType::Unknown, // should be unreachable but I'm not sure } } } -impl std::fmt::Display for Package { +impl Display for Package { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // added package match self.diff_type { DiffType::Added => { - write!(f, "{}", Green.paint(self.versions_after.join(", ")))?; + write!(f, "{}", self.versions_after)?; } DiffType::Removed => { - write!(f, "{}", Red.paint(self.versions_before.join(", ")))?; + write!(f, "{}", self.versions_before)?; } DiffType::Changed => { - write!( - f, - "{} -> {}", - Red.paint(self.versions_before.join(", ")), - Green.paint(self.versions_after.join(", ")) - )?; + write!(f, "{} -> {}", self.versions_before, self.versions_after)?; } DiffType::Unknown => unreachable!(), } @@ -82,3 +53,86 @@ impl std::fmt::Display for Package { Ok(()) } } + +impl From for Package { + fn from(diff: DiffPackage) -> Self { + let diff_type = DiffType::from_versions(&diff.versions_before, &diff.versions_after); + + let (parsed_before, parsed_after) = match diff_type { + DiffType::Added => handle_diff_added(&diff.versions_after), + DiffType::Removed => handle_diff_removed(&diff.versions_before), + DiffType::Changed => handle_diff_changed(&diff.versions_before, &diff.versions_after), + DiffType::Unknown => unreachable!(), + }; + + Package { + size_delta: diff.size_delta, + versions_before: parsed_before, + versions_after: parsed_after, + diff_type, + } + } +} + +fn handle_diff_added(versions_after: &[String]) -> (VersionList, VersionList) { + let mut parsed_after = VersionList::new(); + + for after in versions_after { + let parts_after = after.split('.').map(String::from); + let mut version = Version::new(); + for part in parts_after { + version.push(VersionComponent::new(part, Ordering::Greater)); + } + parsed_after.push(version); + } + + (VersionList::new(), parsed_after) +} + +fn handle_diff_removed(versions_before: &[String]) -> (VersionList, VersionList) { + let mut parsed_before = VersionList::new(); + for before in versions_before { + let parts_before = before.split('.').map(String::from); + let mut version = Version::new(); + for part in parts_before { + version.push(VersionComponent::new(part, Ordering::Less)); + } + parsed_before.push(version); + } + (parsed_before, VersionList::new()) +} + +fn handle_diff_changed( + versions_before: &[String], + versions_after: &[String], +) -> (VersionList, VersionList) { + let mut parsed_before = VersionList::new(); + let mut parsed_after = VersionList::new(); + + for (before, after) in versions_before.iter().zip(versions_after.iter()) { + let mut parts_before = before.split('.').map(String::from).collect::>(); + let mut parts_after = after.split('.').map(String::from).collect::>(); + + let max_len = parts_before.len().max(parts_after.len()); + parts_before.resize(max_len, String::new()); + parts_after.resize(max_len, String::new()); + + let mut ordering = Ordering::Equal; + + let mut line_before = Version::new(); + let mut line_after = Version::new(); + + for (b, a) in parts_before.into_iter().zip(parts_after.into_iter()) { + if ordering == Ordering::Equal { + ordering = b.cmp(&a); + } + line_before.push(VersionComponent::new(b, ordering)); + line_after.push(VersionComponent::new(a, ordering.reverse())); + } + + parsed_before.push(line_before); + parsed_after.push(line_after); + } + + (parsed_before, parsed_after) +} diff --git a/src/parser.rs b/src/parser.rs index 8bdfad5..ea61613 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,72 +1,70 @@ use color_eyre::Result; +use serde::de::Deserializer; use serde::Deserialize; -use std::{collections::BTreeMap, process::Command}; +use std::{borrow::Cow, collections::BTreeMap, process::Command}; -use crate::package::{DiffType, Package}; - -#[derive(Debug)] +#[derive(Deserialize, Debug)] pub struct DiffRoot { - pub packages: BTreeMap, + pub packages: BTreeMap, #[expect(dead_code)] pub schema: String, } -impl<'de> Deserialize<'de> for DiffRoot { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct Raw { - packages: BTreeMap, - schema: String, - } - - let Raw { - mut packages, - schema, - } = Raw::deserialize(deserializer)?; +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DiffPackage { + pub size_delta: i64, - for pkg in packages.values_mut() { - pkg.diff_type = DiffType::from_versions(&pkg.versions_before, &pkg.versions_after); - } + #[serde(deserialize_with = "version_deserializer")] + pub versions_before: Vec, - Ok(DiffRoot { packages, schema }) - } + #[serde(deserialize_with = "version_deserializer")] + pub versions_after: Vec, } -fn run_diff(before: &str, after: &str) -> String { - let raw_diff = Command::new("nix") - .args(["store", "diff-closures", "--json", before, after]) - .output() - .expect("Failed to execute nix command"); +fn version_deserializer<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let vec = Vec::>::deserialize(deserializer)?; + Ok(vec + .into_iter() + .map(|s| { + if s.is_empty() { + "".to_string() + } else { + s.into_owned() + } + }) + .collect()) +} - if !raw_diff.status.success() { - eprintln!("{}", String::from_utf8_lossy(&raw_diff.stderr)); - std::process::exit(1); - } +impl DiffRoot { + pub fn new(before: &str, after: &str) -> Result { + let raw_diff = Command::new("nix") + .args(["store", "diff-closures", "--json", before, after]) + .output()?; - let stdout = raw_diff.stdout; - if stdout.is_empty() { - eprintln!("No differences found."); - std::process::exit(0); - } + if !raw_diff.status.success() { + eprintln!("{}", String::from_utf8_lossy(&raw_diff.stderr)); + std::process::exit(1); + } - // Assume nix output is valid UTF-8 - String::from_utf8(stdout).expect("Output was not valid UTF-8") -} + let stdout = raw_diff.stdout; + if stdout.is_empty() { + eprintln!("No differences found."); + std::process::exit(0); + } -fn parse_diff(input: &str) -> Result { - serde_json::from_str::(input).map_err(|e| { - eprintln!("Failed to parse JSON: {e}"); - std::process::exit(1); - }) -} + // Assume nix output is valid UTF-8 + let diff_out = String::from_utf8(stdout)?; -pub fn diff(before: &str, after: &str) -> Result { - let diff_output = run_diff(before, after); - let diff_root: DiffRoot = parse_diff(&diff_output)?; + let diff_root = serde_json::from_str::(&diff_out).map_err(|e| { + eprintln!("Failed to parse JSON: {e}"); + std::process::exit(1); + })?; - Ok(diff_root) + Ok(diff_root) + } } diff --git a/src/versioning.rs b/src/versioning.rs new file mode 100644 index 0000000..23b0e9a --- /dev/null +++ b/src/versioning.rs @@ -0,0 +1,75 @@ +use nu_ansi_term::Color::{Green, Red, Yellow}; +use std::{cmp::Ordering, fmt::Display}; + +#[derive(Debug, Clone)] +pub struct VersionComponent(String, Ordering); + +#[derive(Debug, Clone)] +pub struct Version(Vec); + +#[derive(Debug, Clone)] +pub struct VersionList(pub Vec); + +impl VersionComponent { + pub fn new(version: String, ordering: Ordering) -> Self { + Self(version, ordering) + } +} + +impl Version { + pub fn new() -> Self { + Self(Vec::new()) + } + + pub fn push(&mut self, version: VersionComponent) { + self.0.push(version); + } +} + +impl Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut out = String::new(); + + for component in &self.0 { + let val = &component.0; + let cmp = component.1; + + let text = if cmp == Ordering::Less { + format!("{}", Red.paint(val)) + } else if cmp == Ordering::Greater { + format!("{}", Green.paint(val)) + } else { + format!("{}", Yellow.paint(val)) + }; + + out.push_str(&text); + out.push('.'); + } + + out.pop(); // remove last comma + write!(f, "{out}") + } +} + +impl VersionList { + pub fn new() -> Self { + Self(Vec::new()) + } + + pub fn push(&mut self, version: Version) { + self.0.push(version); + } +} + +impl Display for VersionList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut out = String::new(); + for version in &self.0 { + out.push_str(&version.to_string()); + out.push_str(", "); + } + out.pop(); // remove last comma + out.pop(); // remove last space + write!(f, "{out}") + } +} -- knotmirror 0.0.1