Skip to main content

cargo_resolvediff/
diff.rs

1// Copyright (C) 2026 by GiGa infosystems
2
3//! Generate a diff between two [`resolve::Resolved`]s, see [`Diff::between`].
4
5use crate::Platform;
6use crate::resolve::{
7    DependencyKind, IncludedDependencyReason, IncludedDependencyVersion, Reasons, Resolved,
8    SpecificCrateIdent,
9};
10use semver::Version;
11use serde::Serialize;
12use std::collections::{BTreeMap, BTreeSet};
13
14/// Added dependencies on the right
15///
16/// These only get emitted if no comparison was emitted for this dependency
17#[derive(Serialize, Debug)]
18pub struct Added<'a> {
19    /// The name & version of the this dependency
20    pub ident: SpecificCrateIdent,
21    pub kind: DependencyKind,
22    pub has_build_rs: bool,
23    pub is_proc_macro: bool,
24    /// The platform this dependency is built (and potentially run at build time) for
25    pub platforms: &'a BTreeSet<Platform>,
26    /// The reasons for the inclusion of this dependency
27    pub reasons: &'a Reasons,
28}
29
30/// Dependencies on the right that are different from dependencies with the same name on the left
31/// (in version, kind or platform inclusion)
32#[derive(Serialize, Debug)]
33pub struct Comparison<'a> {
34    /// The name & version of this dependency
35    pub ident: SpecificCrateIdent,
36    pub kind: DependencyKind,
37    pub has_build_rs: bool,
38    pub is_proc_macro: bool,
39    /// The platform this dependency is built (and potentially run at build time) for
40    pub platforms: &'a BTreeSet<Platform>,
41    pub reasons: &'a Reasons,
42
43    /// The closest version from the left, or [`None`] if the same version existed (in this case
44    /// [`Comparison`]s are only emitted if the `kind` or set of platforms changed)
45    pub closest_different_old_version: Option<Version>,
46    /// The list of all other versions from the left that are different from this version _and_
47    /// different from `closest_different_old_version`
48    pub all_other_old_versions: Vec<Version>,
49
50    /// The platforms this version was not built for on the left, but is now, with the reasons for
51    /// the addition
52    pub added_in_platforms: BTreeMap<&'a Platform, Vec<&'a IncludedDependencyReason>>,
53    /// The reasons (mapping to platforms) for this dependency to be run at build time
54    pub added_in_build: BTreeMap<&'a IncludedDependencyReason, &'a BTreeSet<Platform>>,
55    /// The reasons (mapping to platforms) for this dependency to included outside of dev
56    /// dependencies
57    pub added_in_non_debug: BTreeMap<&'a IncludedDependencyReason, &'a BTreeSet<Platform>>,
58}
59
60impl Comparison<'_> {
61    fn requires_review(&self) -> bool {
62        self.closest_different_old_version.is_some()
63            || !self.added_in_platforms.is_empty()
64            || !self.added_in_build.is_empty()
65            || !self.added_in_non_debug.is_empty()
66    }
67}
68
69/// Removed dependencies on the right
70///
71/// These only get emitted if no comparison was emitted for this dependency
72#[derive(Serialize, Debug)]
73pub struct Removed {
74    /// The name & version of the this dependency
75    pub ident: SpecificCrateIdent,
76    /// The remaining versions of the same name included on the right
77    pub remaining_versions: Vec<Version>,
78}
79
80/// The differences (for code reviews of dependencies) between two dependency resolutions
81#[derive(Serialize, Debug)]
82pub struct Diff<'a> {
83    pub added: Vec<Added<'a>>,
84    pub changed: Vec<Comparison<'a>>,
85    pub removed: Vec<Removed>,
86    /// Crate versions that are part of the right but not the left, which weren't included in the
87    /// platforms the resolution ran for
88    pub filtered_added: Vec<SpecificCrateIdent>,
89    /// Crate versions that are part of the left but not the right, which weren't included in the
90    /// platforms the resolution ran for
91    pub filtered_removed: Vec<SpecificCrateIdent>,
92}
93
94impl<'a> Diff<'a> {
95    fn compare(
96        name: &'a str,
97        old: &'a BTreeMap<Version, IncludedDependencyVersion>,
98        new_version: Version,
99        new: &'a IncludedDependencyVersion,
100    ) -> Comparison<'a> {
101        // NOTE: The assumption is that checking for removals is probably usually easier,
102        // so giving out downgrades for reviews is preferred:
103        let (closest_old_version, closest_old_info) =
104            old.range(&new_version..).next().unwrap_or_else(|| {
105                old.last_key_value()
106                    .expect("Higher ones were already checked, version set is never empty")
107            });
108
109        let closest_different_old_version =
110            (*closest_old_version != new_version).then(|| closest_old_version.clone());
111
112        let all_other_old_versions =
113            if let Some(ref already_mentioned) = closest_different_old_version {
114                old.keys()
115                    .filter(|i| *i != already_mentioned)
116                    .cloned()
117                    .collect::<Vec<_>>()
118            } else {
119                Vec::new()
120            };
121
122        let added_in_platforms = new
123            .platforms
124            .iter()
125            .filter(|i| !closest_old_info.platforms.contains(i))
126            .map(|platform| {
127                let reasons = new
128                    .reasons
129                    .iter()
130                    .filter(|(_, platforms)| platforms.contains(platform))
131                    .map(|(reason, _)| reason)
132                    .collect::<Vec<_>>();
133                (platform, reasons)
134            })
135            .collect();
136
137        let added_in_build = if new.kind.run_at_build && !closest_old_info.kind.run_at_build {
138            new.reasons
139                .iter()
140                .filter(|(reason, _)| reason.kind.run_at_build)
141                .collect()
142        } else {
143            BTreeMap::new()
144        };
145
146        let added_in_non_debug =
147            if !new.kind.only_debug_builds && closest_old_info.kind.only_debug_builds {
148                new.reasons
149                    .iter()
150                    .filter(|(reason, _)| !reason.kind.only_debug_builds)
151                    .collect()
152            } else {
153                BTreeMap::new()
154            };
155
156        Comparison {
157            ident: SpecificCrateIdent {
158                name: name.to_owned(),
159                version: new_version,
160            },
161            kind: new.kind,
162            has_build_rs: new.has_build_rs,
163            is_proc_macro: new.is_proc_macro,
164            platforms: &new.platforms,
165            reasons: &new.reasons,
166
167            closest_different_old_version,
168            all_other_old_versions,
169
170            added_in_platforms,
171            added_in_build,
172            added_in_non_debug,
173        }
174    }
175
176    /// Returns the differences between two [`Resolved`]s for code reviews of dependencies
177    pub fn between(old: &'a Resolved, new: &'a Resolved) -> Self {
178        let added = new
179            .included
180            .iter()
181            .filter(|(name, _)| !old.included.contains_key(*name))
182            .flat_map(|(name, versions)| {
183                versions
184                    .iter()
185                    .map(move |(version, item)| (name, version, item))
186            })
187            .map(|(name, version, info)| Added {
188                ident: SpecificCrateIdent {
189                    name: name.clone(),
190                    version: version.clone(),
191                },
192                kind: info.kind,
193                has_build_rs: info.has_build_rs,
194                is_proc_macro: info.is_proc_macro,
195                platforms: &info.platforms,
196                reasons: &info.reasons,
197            })
198            .collect();
199
200        let changed = new
201            .included
202            .iter()
203            .filter_map(|(name, new_versions)| {
204                old.included
205                    .get(name)
206                    .map(|old_versions| (name, old_versions, new_versions))
207            })
208            .flat_map(|(name, old_versions, new_versions)| {
209                new_versions.iter().map(move |(new_version, new_info)| {
210                    Self::compare(name, old_versions, new_version.clone(), new_info)
211                })
212            })
213            .filter(|comparison| comparison.requires_review())
214            .collect();
215
216        let removed = old
217            .included
218            .iter()
219            .filter_map(|(name, versions)| {
220                let new_versions = new.included.get(name);
221                let has_change = new_versions
222                    .is_some_and(|new| new.keys().any(|key| !versions.contains_key(key)));
223                if has_change {
224                    // NOTE: This isn't a removal because there is an change of some sort for this
225                    // package (= a version that wasn't included previously is now included while
226                    // the package did exist before for some version)
227                    None
228                } else {
229                    Some((name, versions, new_versions))
230                }
231            })
232            .flat_map(|(name, versions, new_versions)| {
233                let is_in_new = move |version: &Version| {
234                    new_versions.is_some_and(|new| new.contains_key(version))
235                };
236                let remaining_versions = versions
237                    .keys()
238                    .filter(|version| is_in_new(version))
239                    .cloned()
240                    .collect::<Vec<_>>();
241                versions
242                    .keys()
243                    .filter(move |version| !is_in_new(version))
244                    .map(move |version| Removed {
245                        ident: SpecificCrateIdent {
246                            name: name.clone(),
247                            version: version.clone(),
248                        },
249                        remaining_versions: remaining_versions.clone(),
250                    })
251            })
252            .collect();
253
254        // NOTE: The type has to be specified to `SpecificCrateIdent` here for some reason even
255        // though it should be inferrable:
256        let in_right_set = |left: &BTreeSet<SpecificCrateIdent>, right: &BTreeSet<_>| {
257            right
258                .iter()
259                .filter(|item| !left.contains(item))
260                .cloned()
261                .collect()
262        };
263
264        let filtered_added = in_right_set(&old.filtered, &new.filtered);
265        let filtered_removed = in_right_set(&old.filtered, &new.filtered);
266
267        Diff {
268            added,
269            changed,
270            removed,
271            filtered_added,
272            filtered_removed,
273        }
274    }
275}