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