1mod difference;
2
3use colored::Colorize;
4use difference::Difference;
5use serde::Deserialize;
6use std::{collections::HashMap, fmt::Debug, fs::read_to_string, path::Path};
7
8#[derive(Deserialize, Debug, Eq, PartialEq, Clone, Hash)]
9pub struct Package {
10 name: String,
11 version: String,
12 source: Option<String>,
13 checksum: Option<String>,
14 #[serde(default)]
15 dependencies: Vec<String>,
16}
17
18#[derive(Debug, Eq, PartialEq)]
19pub struct PackageDiff {
20 pub name: String,
21 pub version: Difference<String>,
22 pub source: Difference<String>,
23 pub checksum: Difference<String>,
24 pub dependencies: Vec<Difference<String>>,
25}
26
27impl PartialOrd for PackageDiff {
28 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
29 self.name.partial_cmp(&other.name)
30 }
31}
32
33impl Ord for PackageDiff {
34 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
35 self.name.cmp(&other.name)
36 }
37}
38
39impl PackageDiff {
40 pub fn diff(a: Package, b: Package) -> PackageDiff {
41 if a.name != b.name {
42 panic!("diffing different packages is not supported");
43 }
44 PackageDiff {
45 name: a.name,
46 version: Difference::diff(a.version, b.version),
47 source: Difference::diff_opt(a.source, b.source),
48 checksum: Difference::diff_opt(a.checksum, b.checksum),
49 dependencies: Difference::diff_vec(a.dependencies, b.dependencies),
50 }
51 }
52
53 pub fn added(p: Package) -> PackageDiff {
54 PackageDiff {
55 name: p.name,
56 version: Difference::Added(p.version),
57 source: p
58 .source
59 .map_or(Difference::Empty, |source| Difference::Added(source)),
60 checksum: p
61 .checksum
62 .map_or(Difference::Empty, |checksum| Difference::Added(checksum)),
63 dependencies: p
64 .dependencies
65 .into_iter()
66 .map(|dependency| Difference::Added(dependency))
67 .collect(),
68 }
69 }
70
71 pub fn removed(p: Package) -> PackageDiff {
72 PackageDiff {
73 name: p.name,
74 version: Difference::Removed(p.version),
75 source: p
76 .source
77 .map_or(Difference::Empty, |source| Difference::Removed(source)),
78 checksum: p
79 .checksum
80 .map_or(Difference::Empty, |checksum| Difference::Removed(checksum)),
81 dependencies: p
82 .dependencies
83 .into_iter()
84 .map(|dependency| Difference::Removed(dependency))
85 .collect(),
86 }
87 }
88
89 pub fn is_equal_or_empty(&self) -> bool {
90 self.version.is_equal()
91 && (self.source.is_equal() || self.source.is_empty())
92 && (self.checksum.is_equal() || self.checksum.is_empty())
93 && self
94 .dependencies
95 .iter()
96 .all(|dependency| dependency.is_equal() || dependency.is_empty())
97 }
98
99 fn pretty_print_version(&self) {
100 match &self.version {
101 Difference::Removed(version) => {
102 println!("{}", format!("-version = \"{}\"", version).red());
103 }
104 Difference::Equal(version) => println!(" version = \"{}\"", version),
105 Difference::Modified { old, new } => {
106 println!("{}", format!("-version = \"{}\"", old).red());
107 println!("{}", format!("+version = \"{}\"", new).green());
108 }
109 Difference::Added(version) => {
110 println!("{}", format!("+version = \"{}\"", version).green())
111 }
112 _ => unreachable!("oh what have you done"),
113 }
114 }
115
116 fn pretty_print_source(&self) {
117 match &self.source {
118 Difference::Removed(source) => {
119 println!("{}", format!("-source = \"{}\"", source).red())
120 }
121 Difference::Equal(source) => println!(" source = \"{}\"", source),
122 Difference::Modified { old, new } => {
123 println!("{}", format!("-source = \"{}\"", old).red());
124 println!("{}", format!("+source = \"{}\"", new).green());
125 }
126 Difference::Added(source) => {
127 println!("{}", format!("+source = \"{}\"", source).green())
128 }
129 _ => {}
130 }
131 }
132
133 fn pretty_print_checksum(&self) {
134 match &self.checksum {
135 Difference::Removed(checksum) => {
136 println!("{}", format!("-checksum = \"{}\"", checksum).red())
137 }
138 Difference::Equal(checksum) => println!(" checksum = \"{}\"", checksum),
139 Difference::Modified { old, new } => {
140 println!("{}", format!("-checksum = \"{}\"", old).red());
141 println!("{}", format!("+checksum = \"{}\"", new).green());
142 }
143 Difference::Added(checksum) => {
144 println!("{}", format!("+checksum = \"{}\"", checksum).green())
145 }
146 _ => {}
147 }
148 }
149
150 fn pretty_print_dependencies(&self, verbose: bool) {
151 println!(" dependencies = [");
152 for dependency in self.dependencies.iter() {
153 match dependency {
154 Difference::Removed(dependency) => {
155 println!("{}", format!("- \"{}\",", dependency).red())
156 }
157 Difference::Equal(dependency) => {
158 if verbose {
159 println!(" \"{}\",", dependency);
160 }
161 }
162 Difference::Modified { old, new } => {
163 println!("{}", format!("- \"{}\",", old).red());
164 println!("{}", format!("+ \"{}\",", new).green());
165 }
166 Difference::Added(dependency) => {
167 println!("{}", format!("+ \"{}\",", dependency).green())
168 }
169 _ => {}
170 }
171 }
172 println!(" ]");
173 }
174
175 pub fn pretty_print_package(&self, verbose: bool) {
176 println!(" [[package]]");
177 println!(" name = \"{}\"", self.name);
178 self.pretty_print_version();
179 self.pretty_print_source();
180 self.pretty_print_checksum();
181 self.pretty_print_dependencies(verbose);
182 }
183}
184
185#[derive(Deserialize, Debug, PartialEq, Eq)]
186pub struct CargoLock {
187 pub version: u8,
188 pub package: Vec<Package>,
189}
190
191impl CargoLock {
192 pub fn load_lock<P: AsRef<Path>>(path: P) -> Self {
193 let contents = read_to_string(path).expect("reading should succeed");
194 toml::from_str(&contents).expect("parsing should succeed")
195 }
196}
197
198#[derive(Debug, PartialEq, Eq)]
199pub struct CargoLockDiff {
200 pub version: Difference<u8>,
201 pub package: Vec<PackageDiff>,
202}
203
204impl CargoLockDiff {
205 pub fn difference(a: CargoLock, b: CargoLock) -> Self {
206 let version = Difference::diff(a.version, b.version);
207
208 let a: HashMap<String, Package> = HashMap::from_iter(
209 a.package
210 .into_iter()
211 .map(|package| (package.name.clone(), package)),
212 );
213 let b: HashMap<String, Package> = HashMap::from_iter(
214 b.package
215 .into_iter()
216 .map(|package| (package.name.clone(), package)),
217 );
218
219 let mut package = Vec::with_capacity(a.len().max(b.len()));
220
221 for (name, old_package) in a.iter() {
222 if let Some(new_package) = b.get(name) {
223 package.push(PackageDiff::diff(old_package.clone(), new_package.clone()));
224 } else {
225 package.push(PackageDiff::removed(old_package.clone()));
226 }
227 }
228
229 for (name, new_package) in b.into_iter() {
230 if a.contains_key(&name) {
231 continue;
232 }
233 package.push(PackageDiff::added(new_package));
234 }
235
236 package.sort();
237
238 Self { version, package }
239 }
240
241 fn pretty_print_version(&self) {
242 match self.version {
243 Difference::Equal(version) => println!(" version = {}", version),
244 Difference::Modified { old, new } => {
245 println!("{}", format!("-version = {}", old).red());
246 println!("{}", format!("+version = {}", new).green());
247 }
248 _ => unreachable!("oh what have you done"),
249 }
250 }
251
252 pub fn pretty_print(&self, verbose: bool) {
253 self.pretty_print_version();
254 if !self.package.is_empty() {
255 println!();
256 }
257
258 for package in self.package[..self.package.len() - 1].iter() {
259 if !package.is_equal_or_empty() {
260 package.pretty_print_package(verbose);
261 println!();
262 }
263 }
264
265 if !self.package[self.package.len() - 1].is_equal_or_empty() {
266 self.package[self.package.len() - 1].pretty_print_package(verbose);
267 }
268 }
269}
270
271#[cfg(test)]
272mod test {
273 use super::*;
274
275 fn tokio_1_15_0_lock() -> Package {
276 Package {
277 name: "tokio".to_string(),
278 version: "1.15.0".to_string(),
279 source: Some("registry+https://github.com/rust-lang/crates.io-index".to_string()),
280 checksum: Some(
281 "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838".to_string(),
282 ),
283 dependencies: vec![
284 "bytes",
285 "libc",
286 "memchr",
287 "mio",
288 "num_cpus",
289 "once_cell",
290 "parking_lot",
291 "pin-project-lite",
292 "signal-hook-registry",
293 "tokio-macros",
294 "winapi",
295 ]
296 .into_iter()
297 .map(|s| s.to_string())
298 .collect(),
299 }
300 }
301
302 fn tokio_1_34_0_lock() -> Package {
303 Package {
304 name: "tokio".to_string(),
305 version: "1.34.0".to_string(),
306 source: Some("registry+https://github.com/rust-lang/crates.io-index".to_string()),
307 checksum: Some(
308 "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9".to_string(),
309 ),
310 dependencies: vec![
311 "backtrace",
312 "bytes",
313 "libc",
314 "mio",
315 "num_cpus",
316 "parking_lot",
317 "pin-project-lite",
318 "signal-hook-registry",
319 "socket2",
320 "tokio-macros",
321 "windows-sys 0.48.0",
322 ]
323 .into_iter()
324 .map(|s| s.to_string())
325 .collect(),
326 }
327 }
328
329 #[test]
330 fn test_package_diff() {
331 let a = tokio_1_15_0_lock();
332 let b = tokio_1_34_0_lock();
333 let diff = PackageDiff::diff(a, b);
334 let expected = PackageDiff {
335 name: "tokio".to_string(),
336 version: Difference::Modified {
337 old: "1.15.0".to_string(),
338 new: "1.34.0".to_string(),
339 },
340 source: Difference::Equal(
341 "registry+https://github.com/rust-lang/crates.io-index".to_string(),
342 ),
343 checksum: Difference::Modified {
344 old: "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838".to_string(),
345 new: "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9".to_string(),
346 },
347 dependencies: vec![
348 Difference::Removed("memchr".to_string()),
349 Difference::Removed("once_cell".to_string()),
350 Difference::Removed("winapi".to_string()),
351 Difference::Equal("bytes".to_string()),
352 Difference::Equal("libc".to_string()),
353 Difference::Equal("mio".to_string()),
354 Difference::Equal("num_cpus".to_string()),
355 Difference::Equal("parking_lot".to_string()),
356 Difference::Equal("pin-project-lite".to_string()),
357 Difference::Equal("signal-hook-registry".to_string()),
358 Difference::Equal("tokio-macros".to_string()),
359 Difference::Added("backtrace".to_string()),
360 Difference::Added("socket2".to_string()),
361 Difference::Added("windows-sys 0.48.0".to_string()),
362 ],
363 };
364
365 assert_eq!(diff, expected);
366 }
367
368 #[test]
369 fn test_cargo_lock_diff() {
370 let a = CargoLock {
371 version: 3,
372 package: vec![tokio_1_15_0_lock()],
373 };
374
375 let b = CargoLock {
376 version: 3,
377 package: vec![tokio_1_34_0_lock()],
378 };
379
380 let diff = CargoLockDiff::difference(a, b);
381 let expected = CargoLockDiff {
382 version: Difference::Equal(3),
383 package: vec![PackageDiff {
384 name: "tokio".to_string(),
385 version: Difference::Modified {
386 old: "1.15.0".to_string(),
387 new: "1.34.0".to_string(),
388 },
389 source: Difference::Equal(
390 "registry+https://github.com/rust-lang/crates.io-index".to_string(),
391 ),
392 checksum: Difference::Modified {
393 old: "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
394 .to_string(),
395 new: "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
396 .to_string(),
397 },
398 dependencies: vec![
399 Difference::Removed("memchr".to_string()),
400 Difference::Removed("once_cell".to_string()),
401 Difference::Removed("winapi".to_string()),
402 Difference::Equal("bytes".to_string()),
403 Difference::Equal("libc".to_string()),
404 Difference::Equal("mio".to_string()),
405 Difference::Equal("num_cpus".to_string()),
406 Difference::Equal("parking_lot".to_string()),
407 Difference::Equal("pin-project-lite".to_string()),
408 Difference::Equal("signal-hook-registry".to_string()),
409 Difference::Equal("tokio-macros".to_string()),
410 Difference::Added("backtrace".to_string()),
411 Difference::Added("socket2".to_string()),
412 Difference::Added("windows-sys 0.48.0".to_string()),
413 ],
414 }],
415 };
416
417 assert_eq!(diff, expected);
418 }
419}