this repo has no description

feat: add version parsing

+240 -98
+8
Cargo.toml
··· 19 19 all = "warn" 20 20 pedantic = "warn" 21 21 unreadable_literal = { level = "allow", priority = 1 } 22 + 23 + [profile.release] 24 + opt-level = "z" 25 + lto = "fat" 26 + codegen-units = 1 27 + panic = "abort" 28 + strip = true 29 +
+7 -3
src/diff.rs
··· 2 2 use nu_ansi_term::Color::{self, Green, Red, Yellow}; 3 3 use std::collections::BTreeMap; 4 4 5 - use crate::package::{DiffType, Package}; 6 - use crate::parser::DiffRoot; 5 + use super::{ 6 + package::{DiffType, Package}, 7 + parser::DiffRoot, 8 + }; 7 9 8 10 #[derive(Debug)] 9 11 pub struct PackageListDiff { ··· 24 26 longest_name: 0, 25 27 }; 26 28 27 - for (name, package) in diff.packages { 29 + for (name, diff_package) in diff.packages { 30 + let package = Package::from(diff_package); 31 + 28 32 out.size_delta += package.size_delta; 29 33 out.longest_name = out.longest_name.max(name.len()); 30 34
+4 -1
src/main.rs
··· 8 8 mod diff; 9 9 mod package; 10 10 mod parser; 11 + mod versioning; 12 + 13 + use self::parser::DiffRoot; 11 14 12 15 #[derive(FromArgs, PartialEq, Debug)] 13 16 /// List the package differences between two `NixOS` generations ··· 45 48 std::process::exit(1); 46 49 } 47 50 48 - let packages: PackageListDiff = parser::diff(&before, &after)?.into(); 51 + let packages: PackageListDiff = DiffRoot::new(&before, &after)?.into(); 49 52 50 53 let arrow_style = Style::new().bold().fg(Color::LightGray); 51 54
+96 -42
src/package.rs
··· 1 - use std::borrow::Cow; 1 + use std::{cmp::Ordering, fmt::Display}; 2 2 3 - use nu_ansi_term::Color::{Green, Red}; 4 - use serde::de::Deserializer; 5 - use serde::Deserialize; 3 + use super::{ 4 + parser::DiffPackage, 5 + versioning::{Version, VersionComponent, VersionList}, 6 + }; 6 7 7 8 #[derive(Default, Debug, PartialEq, Eq)] 8 9 pub enum DiffType { ··· 14 15 Unknown, 15 16 } 16 17 17 - #[derive(Deserialize, Debug)] 18 - #[serde(rename_all = "camelCase")] 18 + #[derive(Debug)] 19 19 pub struct Package { 20 20 pub size_delta: i64, 21 - 22 - #[serde(deserialize_with = "version_deserializer")] 23 - pub versions_before: Vec<String>, 24 - 25 - #[serde(deserialize_with = "version_deserializer")] 26 - pub versions_after: Vec<String>, 27 - 28 - /// This is not a part of the JSON schema, but is used to determine the type of diff 29 - #[serde(skip)] 30 21 pub diff_type: DiffType, 31 - } 32 22 33 - fn version_deserializer<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error> 34 - where 35 - D: Deserializer<'de>, 36 - { 37 - let vec = Vec::<Cow<'de, str>>::deserialize(deserializer)?; 38 - Ok(vec 39 - .into_iter() 40 - .map(|s| { 41 - if s.is_empty() { 42 - "<none>".to_string() 43 - } else { 44 - s.into_owned() 45 - } 46 - }) 47 - .collect()) 23 + pub versions_before: VersionList, 24 + pub versions_after: VersionList, 48 25 } 49 26 50 27 impl DiffType { ··· 53 30 (true, false) => DiffType::Added, 54 31 (false, true) => DiffType::Removed, 55 32 (false, false) => DiffType::Changed, 56 - (true, true) => DiffType::Unknown, // should be unreachable but im not sure 33 + (true, true) => DiffType::Unknown, // should be unreachable but I'm not sure 57 34 } 58 35 } 59 36 } 60 37 61 - impl std::fmt::Display for Package { 38 + impl Display for Package { 62 39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 63 - // added package 64 40 match self.diff_type { 65 41 DiffType::Added => { 66 - write!(f, "{}", Green.paint(self.versions_after.join(", ")))?; 42 + write!(f, "{}", self.versions_after)?; 67 43 } 68 44 DiffType::Removed => { 69 - write!(f, "{}", Red.paint(self.versions_before.join(", ")))?; 45 + write!(f, "{}", self.versions_before)?; 70 46 } 71 47 DiffType::Changed => { 72 - write!( 73 - f, 74 - "{} -> {}", 75 - Red.paint(self.versions_before.join(", ")), 76 - Green.paint(self.versions_after.join(", ")) 77 - )?; 48 + write!(f, "{} -> {}", self.versions_before, self.versions_after)?; 78 49 } 79 50 DiffType::Unknown => unreachable!(), 80 51 } ··· 82 53 Ok(()) 83 54 } 84 55 } 56 + 57 + impl From<DiffPackage> for Package { 58 + fn from(diff: DiffPackage) -> Self { 59 + let diff_type = DiffType::from_versions(&diff.versions_before, &diff.versions_after); 60 + 61 + let (parsed_before, parsed_after) = match diff_type { 62 + DiffType::Added => handle_diff_added(&diff.versions_after), 63 + DiffType::Removed => handle_diff_removed(&diff.versions_before), 64 + DiffType::Changed => handle_diff_changed(&diff.versions_before, &diff.versions_after), 65 + DiffType::Unknown => unreachable!(), 66 + }; 67 + 68 + Package { 69 + size_delta: diff.size_delta, 70 + versions_before: parsed_before, 71 + versions_after: parsed_after, 72 + diff_type, 73 + } 74 + } 75 + } 76 + 77 + fn handle_diff_added(versions_after: &[String]) -> (VersionList, VersionList) { 78 + let mut parsed_after = VersionList::new(); 79 + 80 + for after in versions_after { 81 + let parts_after = after.split('.').map(String::from); 82 + let mut version = Version::new(); 83 + for part in parts_after { 84 + version.push(VersionComponent::new(part, Ordering::Greater)); 85 + } 86 + parsed_after.push(version); 87 + } 88 + 89 + (VersionList::new(), parsed_after) 90 + } 91 + 92 + fn handle_diff_removed(versions_before: &[String]) -> (VersionList, VersionList) { 93 + let mut parsed_before = VersionList::new(); 94 + for before in versions_before { 95 + let parts_before = before.split('.').map(String::from); 96 + let mut version = Version::new(); 97 + for part in parts_before { 98 + version.push(VersionComponent::new(part, Ordering::Less)); 99 + } 100 + parsed_before.push(version); 101 + } 102 + (parsed_before, VersionList::new()) 103 + } 104 + 105 + fn handle_diff_changed( 106 + versions_before: &[String], 107 + versions_after: &[String], 108 + ) -> (VersionList, VersionList) { 109 + let mut parsed_before = VersionList::new(); 110 + let mut parsed_after = VersionList::new(); 111 + 112 + for (before, after) in versions_before.iter().zip(versions_after.iter()) { 113 + let mut parts_before = before.split('.').map(String::from).collect::<Vec<_>>(); 114 + let mut parts_after = after.split('.').map(String::from).collect::<Vec<_>>(); 115 + 116 + let max_len = parts_before.len().max(parts_after.len()); 117 + parts_before.resize(max_len, String::new()); 118 + parts_after.resize(max_len, String::new()); 119 + 120 + let mut ordering = Ordering::Equal; 121 + 122 + let mut line_before = Version::new(); 123 + let mut line_after = Version::new(); 124 + 125 + for (b, a) in parts_before.into_iter().zip(parts_after.into_iter()) { 126 + if ordering == Ordering::Equal { 127 + ordering = b.cmp(&a); 128 + } 129 + line_before.push(VersionComponent::new(b, ordering)); 130 + line_after.push(VersionComponent::new(a, ordering.reverse())); 131 + } 132 + 133 + parsed_before.push(line_before); 134 + parsed_after.push(line_after); 135 + } 136 + 137 + (parsed_before, parsed_after) 138 + }
+50 -52
src/parser.rs
··· 1 1 use color_eyre::Result; 2 + use serde::de::Deserializer; 2 3 use serde::Deserialize; 3 - use std::{collections::BTreeMap, process::Command}; 4 + use std::{borrow::Cow, collections::BTreeMap, process::Command}; 4 5 5 - use crate::package::{DiffType, Package}; 6 - 7 - #[derive(Debug)] 6 + #[derive(Deserialize, Debug)] 8 7 pub struct DiffRoot { 9 - pub packages: BTreeMap<String, Package>, 8 + pub packages: BTreeMap<String, DiffPackage>, 10 9 11 10 #[expect(dead_code)] 12 11 pub schema: String, 13 12 } 14 13 15 - impl<'de> Deserialize<'de> for DiffRoot { 16 - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 17 - where 18 - D: serde::Deserializer<'de>, 19 - { 20 - #[derive(Deserialize)] 21 - struct Raw { 22 - packages: BTreeMap<String, Package>, 23 - schema: String, 24 - } 14 + #[derive(Deserialize, Debug)] 15 + #[serde(rename_all = "camelCase")] 16 + pub struct DiffPackage { 17 + pub size_delta: i64, 25 18 26 - let Raw { 27 - mut packages, 28 - schema, 29 - } = Raw::deserialize(deserializer)?; 19 + #[serde(deserialize_with = "version_deserializer")] 20 + pub versions_before: Vec<String>, 30 21 31 - for pkg in packages.values_mut() { 32 - pkg.diff_type = DiffType::from_versions(&pkg.versions_before, &pkg.versions_after); 33 - } 34 - 35 - Ok(DiffRoot { packages, schema }) 36 - } 22 + #[serde(deserialize_with = "version_deserializer")] 23 + pub versions_after: Vec<String>, 37 24 } 38 25 39 - fn run_diff(before: &str, after: &str) -> String { 40 - let raw_diff = Command::new("nix") 41 - .args(["store", "diff-closures", "--json", before, after]) 42 - .output() 43 - .expect("Failed to execute nix command"); 26 + fn version_deserializer<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error> 27 + where 28 + D: Deserializer<'de>, 29 + { 30 + let vec = Vec::<Cow<'de, str>>::deserialize(deserializer)?; 31 + Ok(vec 32 + .into_iter() 33 + .map(|s| { 34 + if s.is_empty() { 35 + "<none>".to_string() 36 + } else { 37 + s.into_owned() 38 + } 39 + }) 40 + .collect()) 41 + } 44 42 45 - if !raw_diff.status.success() { 46 - eprintln!("{}", String::from_utf8_lossy(&raw_diff.stderr)); 47 - std::process::exit(1); 48 - } 43 + impl DiffRoot { 44 + pub fn new(before: &str, after: &str) -> Result<DiffRoot> { 45 + let raw_diff = Command::new("nix") 46 + .args(["store", "diff-closures", "--json", before, after]) 47 + .output()?; 49 48 50 - let stdout = raw_diff.stdout; 51 - if stdout.is_empty() { 52 - eprintln!("No differences found."); 53 - std::process::exit(0); 54 - } 49 + if !raw_diff.status.success() { 50 + eprintln!("{}", String::from_utf8_lossy(&raw_diff.stderr)); 51 + std::process::exit(1); 52 + } 55 53 56 - // Assume nix output is valid UTF-8 57 - String::from_utf8(stdout).expect("Output was not valid UTF-8") 58 - } 54 + let stdout = raw_diff.stdout; 55 + if stdout.is_empty() { 56 + eprintln!("No differences found."); 57 + std::process::exit(0); 58 + } 59 59 60 - fn parse_diff(input: &str) -> Result<DiffRoot> { 61 - serde_json::from_str::<DiffRoot>(input).map_err(|e| { 62 - eprintln!("Failed to parse JSON: {e}"); 63 - std::process::exit(1); 64 - }) 65 - } 60 + // Assume nix output is valid UTF-8 61 + let diff_out = String::from_utf8(stdout)?; 66 62 67 - pub fn diff(before: &str, after: &str) -> Result<DiffRoot> { 68 - let diff_output = run_diff(before, after); 69 - let diff_root: DiffRoot = parse_diff(&diff_output)?; 63 + let diff_root = serde_json::from_str::<DiffRoot>(&diff_out).map_err(|e| { 64 + eprintln!("Failed to parse JSON: {e}"); 65 + std::process::exit(1); 66 + })?; 70 67 71 - Ok(diff_root) 68 + Ok(diff_root) 69 + } 72 70 }
+75
src/versioning.rs
··· 1 + use nu_ansi_term::Color::{Green, Red, Yellow}; 2 + use std::{cmp::Ordering, fmt::Display}; 3 + 4 + #[derive(Debug, Clone)] 5 + pub struct VersionComponent(String, Ordering); 6 + 7 + #[derive(Debug, Clone)] 8 + pub struct Version(Vec<VersionComponent>); 9 + 10 + #[derive(Debug, Clone)] 11 + pub struct VersionList(pub Vec<Version>); 12 + 13 + impl VersionComponent { 14 + pub fn new(version: String, ordering: Ordering) -> Self { 15 + Self(version, ordering) 16 + } 17 + } 18 + 19 + impl Version { 20 + pub fn new() -> Self { 21 + Self(Vec::new()) 22 + } 23 + 24 + pub fn push(&mut self, version: VersionComponent) { 25 + self.0.push(version); 26 + } 27 + } 28 + 29 + impl Display for Version { 30 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 + let mut out = String::new(); 32 + 33 + for component in &self.0 { 34 + let val = &component.0; 35 + let cmp = component.1; 36 + 37 + let text = if cmp == Ordering::Less { 38 + format!("{}", Red.paint(val)) 39 + } else if cmp == Ordering::Greater { 40 + format!("{}", Green.paint(val)) 41 + } else { 42 + format!("{}", Yellow.paint(val)) 43 + }; 44 + 45 + out.push_str(&text); 46 + out.push('.'); 47 + } 48 + 49 + out.pop(); // remove last comma 50 + write!(f, "{out}") 51 + } 52 + } 53 + 54 + impl VersionList { 55 + pub fn new() -> Self { 56 + Self(Vec::new()) 57 + } 58 + 59 + pub fn push(&mut self, version: Version) { 60 + self.0.push(version); 61 + } 62 + } 63 + 64 + impl Display for VersionList { 65 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 66 + let mut out = String::new(); 67 + for version in &self.0 { 68 + out.push_str(&version.to_string()); 69 + out.push_str(", "); 70 + } 71 + out.pop(); // remove last comma 72 + out.pop(); // remove last space 73 + write!(f, "{out}") 74 + } 75 + }