cargo_guppy/
diff.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use guppy::{graph::PackageMetadata, PackageId};
5use serde::{ser::SerializeStruct, Serialize, Serializer};
6use std::{collections::HashMap, ops::Deref};
7
8#[derive(Debug, Default)]
9pub struct DiffOptions;
10
11impl DiffOptions {
12    pub fn diff<'a>(
13        &self,
14        old_packages: &'a [PackageMetadata<'a>],
15        new_packages: &'a [PackageMetadata<'a>],
16    ) -> Diff<'a> {
17        let mut new: HashMap<&PackageId, Package> =
18            new_packages.iter().map(|p| (p.id(), Package(p))).collect();
19
20        let mut removed = old_packages
21            .iter()
22            .filter_map(|package| {
23                if new.remove(package.id()).is_none() {
24                    Some(Package(package))
25                } else {
26                    None
27                }
28            })
29            .map(|removed_package| {
30                let remaining_packages = new_packages
31                    .iter()
32                    .filter_map(|package| {
33                        if (package.id() != removed_package.id())
34                            && (package.name() == removed_package.name())
35                        {
36                            Some(Package(package))
37                        } else {
38                            None
39                        }
40                    })
41                    .collect::<Vec<_>>();
42
43                if remaining_packages.is_empty() {
44                    (removed_package.id().clone(), (removed_package, None))
45                } else {
46                    (
47                        removed_package.id().clone(),
48                        (removed_package, Some(remaining_packages)),
49                    )
50                }
51            })
52            .collect::<HashMap<_, _>>();
53
54        let mut added = new
55            .into_iter()
56            .map(|(added_package_id, added_package)| {
57                let existing_packages = new_packages
58                    .iter()
59                    .filter_map(|package| {
60                        if (package.id() != added_package_id)
61                            && (package.name() == added_package.name())
62                        {
63                            Some(Package(package))
64                        } else {
65                            None
66                        }
67                    })
68                    .collect::<Vec<_>>();
69
70                if existing_packages.is_empty() {
71                    (added_package_id, (added_package, None))
72                } else {
73                    (added_package_id, (added_package, Some(existing_packages)))
74                }
75            })
76            .collect::<HashMap<_, _>>();
77
78        let mut updated = removed
79            .iter()
80            .filter_map(|(_, (removed_package, _remaining_packages))| {
81                if let Some((_updated_package_id, (updated_package, _))) =
82                    added
83                        .iter()
84                        .find(|(_added_package_id, (added_package, _))| {
85                            removed_package.name() == added_package.name()
86                        })
87                {
88                    Some((removed_package.clone(), updated_package.clone()))
89                } else {
90                    None
91                }
92            })
93            .collect::<Vec<_>>();
94        updated.sort_by(|a, b| a.1.name().cmp(b.1.name()));
95
96        // Remove entries from Added and Removed
97        for (removed_pkg, added_pkg) in &updated {
98            removed.remove(removed_pkg.id());
99            added.remove(added_pkg.id());
100        }
101
102        let mut removed = removed.into_iter().map(|x| x.1).collect::<Vec<_>>();
103        removed.sort_by(|(a, _), (b, _)| a.name().cmp(b.name()));
104        let mut added = added.into_iter().map(|x| x.1).collect::<Vec<_>>();
105        added.sort_by(|(a, _), (b, _)| a.name().cmp(b.name()));
106
107        Diff {
108            updated,
109            removed,
110            added,
111        }
112    }
113}
114
115#[derive(Clone, Debug)]
116struct Package<'a>(pub &'a PackageMetadata<'a>);
117
118impl<'a> Deref for Package<'a> {
119    type Target = PackageMetadata<'a>;
120
121    fn deref(&self) -> &Self::Target {
122        self.0
123    }
124}
125
126impl Serialize for Package<'_> {
127    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
128    where
129        S: Serializer,
130    {
131        let mut state = serializer.serialize_struct("Package", 3)?;
132        state.serialize_field("id", self.0.id().repr())?;
133        state.serialize_field("name", self.0.name())?;
134        state.serialize_field("version", self.0.version())?;
135        state.end()
136    }
137}
138
139#[derive(Debug, Serialize)]
140pub struct Diff<'a> {
141    updated: Vec<(Package<'a>, Package<'a>)>,
142    removed: Vec<(Package<'a>, Option<Vec<Package<'a>>>)>,
143    added: Vec<(Package<'a>, Option<Vec<Package<'a>>>)>,
144}
145
146impl ::std::fmt::Display for Diff<'_> {
147    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
148        fn write_dups(
149            f: &mut ::std::fmt::Formatter<'_>,
150            dups: &Option<Vec<Package>>,
151        ) -> ::std::fmt::Result {
152            if let Some(dups) = dups {
153                write!(f, " ({}", dups[0].version())?;
154                for p in &dups[1..] {
155                    write!(f, ", {}", p.version())?;
156                }
157                write!(f, ")")?;
158            }
159
160            Ok(())
161        }
162
163        if !self.added.is_empty() {
164            writeln!(f, "Added Packages (Duplicate versions in '()'):")?;
165            for (added, dups) in &self.added {
166                write!(f, "\t{} {}", added.name(), added.version(),)?;
167
168                write_dups(f, dups)?;
169                writeln!(f)?;
170            }
171            writeln!(f)?;
172        }
173
174        if !self.removed.is_empty() {
175            writeln!(f, "Removed Packages (Remaining versions in '()'):")?;
176            for (removed, dups) in &self.removed {
177                write!(f, "\t{} {}", removed.name(), removed.version(),)?;
178
179                write_dups(f, dups)?;
180                writeln!(f)?;
181            }
182            writeln!(f)?;
183        }
184
185        if !self.updated.is_empty() {
186            writeln!(f, "Updated Packages:")?;
187            for (removed, added) in &self.updated {
188                writeln!(
189                    f,
190                    "\t{}: {} -> {}",
191                    removed.name(),
192                    removed.version(),
193                    added.version(),
194                )?;
195            }
196            writeln!(f)?;
197        }
198
199        Ok(())
200    }
201}