lock_diff/
lib.rs

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}