1pub use crate::report::SummaryReport;
12use crate::{PackageInfo, PackageMap, PackageStatus, Summary, SummaryId, SummarySource};
13use ahash::AHashMap;
14use diffus::{edit, Diffable};
15use semver::Version;
16use serde::{ser::SerializeStruct, Serialize};
17use std::{
18 collections::{BTreeMap, BTreeSet},
19 fmt, mem,
20};
21
22#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
73#[serde(rename_all = "kebab-case")]
74pub struct SummaryDiff<'a> {
75 pub target_packages: PackageDiff<'a>,
77
78 pub host_packages: PackageDiff<'a>,
80}
81
82impl<'a> SummaryDiff<'a> {
83 pub fn new(old: &'a Summary, new: &'a Summary) -> Self {
85 Self {
86 target_packages: PackageDiff::new(&old.target_packages, &new.target_packages),
87 host_packages: PackageDiff::new(&old.host_packages, &new.host_packages),
88 }
89 }
90
91 pub fn is_changed(&self) -> bool {
93 !self.is_unchanged()
94 }
95
96 pub fn is_unchanged(&self) -> bool {
98 self.target_packages.is_unchanged() && self.host_packages.is_unchanged()
99 }
100
101 pub fn report<'b>(&'b self) -> SummaryReport<'a, 'b> {
105 SummaryReport::new(self)
106 }
107}
108
109pub type UnchangedInfo<'a> = (&'a Version, &'a SummarySource, &'a PackageInfo);
111
112#[derive(Clone, Debug, Eq, PartialEq)]
114pub struct PackageDiff<'a> {
115 pub changed: BTreeMap<&'a SummaryId, SummaryDiffStatus<'a>>,
117
118 pub unchanged: BTreeMap<&'a str, Vec<UnchangedInfo<'a>>>,
120}
121
122impl<'a> PackageDiff<'a> {
123 pub fn new(old: &'a PackageMap, new: &'a PackageMap) -> Self {
125 let mut changed = BTreeMap::new();
126 let mut unchanged = BTreeMap::new();
127
128 let mut add_unchanged = |summary_id: &'a SummaryId, info: &'a PackageInfo| {
129 unchanged
130 .entry(summary_id.name.as_str())
131 .or_insert_with(Vec::new)
132 .push((&summary_id.version, &summary_id.source, info));
133 };
134
135 match (*old).diff(new) {
136 edit::Edit::Copy(_) => {
137 for (summary_id, info) in new {
139 add_unchanged(summary_id, info);
140 }
141 }
142 edit::Edit::Change(diff) => {
143 for (summary_id, diff) in diff {
144 match diff {
145 edit::map::Edit::Copy(info) => {
146 add_unchanged(summary_id, info);
148 }
149 edit::map::Edit::Insert(info) => {
150 let status = SummaryDiffStatus::Added { info };
152 changed.insert(summary_id, status);
153 }
154 edit::map::Edit::Remove(old_info) => {
155 let status = SummaryDiffStatus::Removed { old_info };
157 changed.insert(summary_id, status);
158 }
159 edit::map::Edit::Change((old_info, new_info)) => {
160 let status =
162 SummaryDiffStatus::make_changed(None, None, old_info, new_info);
163 changed.insert(summary_id, status);
164 }
165 }
166 }
167 }
168 }
169
170 Self::combine_insert_remove(&mut changed);
172
173 Self { changed, unchanged }
174 }
175
176 pub fn is_unchanged(&self) -> bool {
178 self.changed.is_empty()
179 }
180
181 fn combine_insert_remove(changed: &mut BTreeMap<&'a SummaryId, SummaryDiffStatus<'a>>) {
186 let mut combine_statuses: AHashMap<&str, CombineStatus<'_>> =
187 AHashMap::with_capacity(changed.len());
188
189 for (summary_id, status) in &*changed {
190 let entry = combine_statuses
191 .entry(summary_id.name.as_str())
192 .or_insert_with(|| CombineStatus::None);
193 match status {
194 SummaryDiffStatus::Added { .. } => entry.record_added(summary_id),
195 SummaryDiffStatus::Removed { .. } => entry.record_removed(summary_id),
196 SummaryDiffStatus::Modified { .. } => entry.record_changed(),
197 }
198 }
199
200 for status in combine_statuses.values() {
201 if let CombineStatus::Combine { added, removed } = status {
202 let removed_status = changed
203 .remove(removed)
204 .expect("removed ID should be present");
205
206 let old_info = match removed_status {
207 SummaryDiffStatus::Removed { old_info } => old_info,
208 other => panic!("expected Removed, found {:?}", other),
209 };
210
211 let added_status = changed.get_mut(added).expect("added ID should be present");
212 let new_info = match &*added_status {
213 SummaryDiffStatus::Added { info } => *info,
214 other => panic!("expected Added, found {:?}", other),
215 };
216
217 let old_version = if added.version != removed.version {
218 Some(&removed.version)
219 } else {
220 None
221 };
222 let old_source = if added.source != removed.source {
223 Some(&removed.source)
224 } else {
225 None
226 };
227
228 let _ = mem::replace(
230 added_status,
231 SummaryDiffStatus::make_changed(old_version, old_source, old_info, new_info),
232 );
233 }
234 }
235 }
236}
237
238pub(crate) fn changed_sort_key<'a>(
239 summary_id: &'a SummaryId,
240 status: &SummaryDiffStatus<'_>,
241) -> impl Ord + 'a {
242 (status.tag(), status.latest_status(), summary_id)
248}
249
250impl Serialize for PackageDiff<'_> {
251 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
252 where
253 S: serde::Serializer,
254 {
255 #[derive(Serialize)]
256 struct Changed<'a> {
257 #[serde(flatten)]
260 package: &'a SummaryId,
261 #[serde(flatten)]
262 changes: &'a SummaryDiffStatus<'a>,
263 }
264
265 let mut changed: Vec<Changed> = self
266 .changed
267 .iter()
268 .map(|(package, changes)| Changed { package, changes })
269 .collect();
270 changed.sort_by_key(|item| changed_sort_key(item.package, item.changes));
272
273 let mut state = serializer.serialize_struct("PackageDiff", 2)?;
274 state.serialize_field("changed", &changed)?;
275
276 #[derive(Serialize)]
277 struct Unchanged<'a> {
278 name: &'a str,
280 version: &'a Version,
281 #[serde(flatten)]
282 source: &'a SummarySource,
283 #[serde(flatten)]
284 info: &'a PackageInfo,
285 }
286
287 if !self.unchanged.is_empty() {
290 let mut unchanged: Vec<_> = self
291 .unchanged
292 .iter()
293 .flat_map(|(&name, info)| {
294 info.iter().map(move |(version, source, info)| Unchanged {
295 name,
296 version,
297 source,
298 info,
299 })
300 })
301 .collect();
302 unchanged.sort_by_key(|item| (item.name, item.version, item.source));
304 state.serialize_field("unchanged", &unchanged)?;
305 }
306
307 state.end()
308 }
309}
310
311#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
313#[serde(rename_all = "kebab-case", tag = "change")]
314pub enum SummaryDiffStatus<'a> {
315 #[serde(rename_all = "kebab-case")]
317 Added {
318 #[serde(flatten)]
320 info: &'a PackageInfo,
321 },
322
323 #[serde(rename_all = "kebab-case")]
325 Removed {
326 #[serde(flatten, with = "removed_impl")]
328 old_info: &'a PackageInfo,
329 },
330
331 #[serde(rename_all = "kebab-case")]
335 Modified {
336 old_version: Option<&'a Version>,
338
339 old_source: Option<&'a SummarySource>,
341
342 old_status: Option<PackageStatus>,
344
345 new_status: PackageStatus,
347
348 added_features: BTreeSet<&'a str>,
350
351 removed_features: BTreeSet<&'a str>,
353
354 unchanged_features: BTreeSet<&'a str>,
356
357 #[serde(default)]
359 added_optional_deps: BTreeSet<&'a str>,
360
361 #[serde(default)]
363 removed_optional_deps: BTreeSet<&'a str>,
364
365 #[serde(default)]
367 unchanged_optional_deps: BTreeSet<&'a str>,
368 },
369}
370
371impl<'a> SummaryDiffStatus<'a> {
372 fn make_changed(
373 old_version: Option<&'a Version>,
374 old_source: Option<&'a SummarySource>,
375 old_info: &'a PackageInfo,
376 new_info: &'a PackageInfo,
377 ) -> Self {
378 let old_status = if old_info.status != new_info.status {
379 Some(old_info.status)
380 } else {
381 None
382 };
383
384 let [added_features, removed_features, unchanged_features] =
385 Self::make_changed_diff(&old_info.features, &new_info.features);
386
387 let [added_optional_deps, removed_optional_deps, unchanged_optional_deps] =
388 Self::make_changed_diff(&old_info.optional_deps, &new_info.optional_deps);
389
390 SummaryDiffStatus::Modified {
391 old_version,
392 old_source,
393 old_status,
394 new_status: new_info.status,
395 added_features,
396 removed_features,
397 unchanged_features,
398 added_optional_deps,
399 removed_optional_deps,
400 unchanged_optional_deps,
401 }
402 }
403
404 fn make_changed_diff(
405 old_features: &'a BTreeSet<String>,
406 new_features: &'a BTreeSet<String>,
407 ) -> [BTreeSet<&'a str>; 3] {
408 let mut added_features = BTreeSet::new();
409 let mut removed_features = BTreeSet::new();
410 let mut unchanged_features = BTreeSet::new();
411
412 match old_features.diff(new_features) {
413 edit::Edit::Copy(features) => {
414 unchanged_features.extend(features.iter().map(|feature| feature.as_str()));
415 }
416 edit::Edit::Change(diff) => {
417 for (_, diff) in diff {
418 match diff {
419 edit::set::Edit::Copy(feature) => {
420 unchanged_features.insert(feature.as_str());
421 }
422 edit::set::Edit::Insert(feature) => {
423 added_features.insert(feature.as_str());
424 }
425 edit::set::Edit::Remove(feature) => {
426 removed_features.insert(feature.as_str());
427 }
428 }
429 }
430 }
431 }
432
433 [added_features, removed_features, unchanged_features]
434 }
435
436 pub fn tag(&self) -> SummaryDiffTag {
440 match self {
441 SummaryDiffStatus::Added { .. } => SummaryDiffTag::Added,
442 SummaryDiffStatus::Removed { .. } => SummaryDiffTag::Removed,
443 SummaryDiffStatus::Modified { .. } => SummaryDiffTag::Modified,
444 }
445 }
446
447 pub fn latest_status(&self) -> PackageStatus {
449 match self {
450 SummaryDiffStatus::Added { info } => info.status,
451 SummaryDiffStatus::Removed { old_info } => old_info.status,
452 SummaryDiffStatus::Modified { new_status, .. } => *new_status,
453 }
454 }
455}
456
457mod removed_impl {
458 use super::*;
459 use serde::Serializer;
460
461 pub fn serialize<S>(item: &PackageInfo, serializer: S) -> Result<S::Ok, S::Error>
462 where
463 S: Serializer,
464 {
465 #[derive(Serialize)]
466 #[serde(rename_all = "kebab-case")]
467 struct OldPackageInfo<'a> {
468 old_status: &'a PackageStatus,
469 old_features: &'a BTreeSet<String>,
470 }
471
472 let old_info = OldPackageInfo {
473 old_status: &item.status,
474 old_features: &item.features,
475 };
476
477 old_info.serialize(serializer)
478 }
479}
480
481#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
485pub enum SummaryDiffTag {
486 Added,
488
489 Modified,
491
492 Removed,
494}
495
496impl fmt::Display for SummaryDiffTag {
497 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
498 match self {
499 SummaryDiffTag::Added => write!(f, "A"),
500 SummaryDiffTag::Modified => write!(f, "M"),
501 SummaryDiffTag::Removed => write!(f, "R"),
502 }
503 }
504}
505
506impl<'a> Diffable<'a> for PackageInfo {
507 type Diff = (&'a PackageInfo, &'a PackageInfo);
508
509 fn diff(&'a self, other: &'a Self) -> edit::Edit<'a, Self> {
510 if self == other {
511 edit::Edit::Copy(self)
512 } else {
513 edit::Edit::Change((self, other))
514 }
515 }
516}
517
518impl<'a> Diffable<'a> for PackageStatus {
519 type Diff = (&'a PackageStatus, &'a PackageStatus);
520
521 fn diff(&'a self, other: &'a Self) -> edit::Edit<'a, Self> {
522 if self == other {
523 edit::Edit::Copy(self)
524 } else {
525 edit::Edit::Change((self, other))
526 }
527 }
528}
529
530enum CombineStatus<'a> {
532 None,
533 Added(&'a SummaryId),
534 Removed(&'a SummaryId),
535 Combine {
536 added: &'a SummaryId,
537 removed: &'a SummaryId,
538 },
539 Ignore,
540}
541
542impl<'a> CombineStatus<'a> {
543 fn record_added(&mut self, summary_id: &'a SummaryId) {
544 let new = match self {
545 CombineStatus::None => CombineStatus::Added(summary_id),
546 CombineStatus::Removed(removed) => CombineStatus::Combine {
547 added: summary_id,
548 removed,
549 },
550 _ => CombineStatus::Ignore,
551 };
552
553 let _ = mem::replace(self, new);
554 }
555
556 fn record_removed(&mut self, summary_id: &'a SummaryId) {
557 let new = match self {
558 CombineStatus::None => CombineStatus::Removed(summary_id),
559 CombineStatus::Added(added) => CombineStatus::Combine {
560 added,
561 removed: summary_id,
562 },
563 _ => CombineStatus::Ignore,
564 };
565
566 let _ = mem::replace(self, new);
567 }
568
569 fn record_changed(&mut self) {
570 let _ = mem::replace(self, CombineStatus::Ignore);
573 }
574}