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