1use crate::report::{ScanReport, ScannedFile};
2use std::collections::{BTreeMap, BTreeSet};
3
4#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum DeltaQuality {
8 ContentHash,
10 Metadata,
12 Partial,
14}
15
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ModifiedFile {
20 pub previous: ScannedFile,
21 pub current: ScannedFile,
22}
23
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct RenamedFile {
28 pub previous: ScannedFile,
29 pub current: ScannedFile,
30}
31
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ScanDelta {
36 pub from_revision: String,
37 pub to_revision: String,
38 pub added: Vec<ScannedFile>,
39 pub removed: Vec<ScannedFile>,
40 pub modified: Vec<ModifiedFile>,
41 pub renamed: Vec<RenamedFile>,
42 pub unchanged: u64,
43 pub selection_inputs_changed: bool,
44 pub scan_state_changed: bool,
45 pub quality: DeltaQuality,
46}
47
48impl ScanDelta {
49 #[must_use]
50 pub fn between(previous: &ScanReport, current: &ScanReport) -> Self {
51 let quality = delta_quality(previous, current);
52 let mut previous_files = previous.files.iter().collect::<Vec<_>>();
53 let mut current_files = current.files.iter().collect::<Vec<_>>();
54 previous_files.sort_unstable_by(|left, right| left.relative.cmp(&right.relative));
55 current_files.sort_unstable_by(|left, right| left.relative.cmp(&right.relative));
56 let mut delta = Self {
57 from_revision: previous.revision.clone(),
58 to_revision: current.revision.clone(),
59 added: Vec::new(),
60 removed: Vec::new(),
61 modified: Vec::new(),
62 renamed: Vec::new(),
63 unchanged: 0,
64 selection_inputs_changed: previous.ignore_sources != current.ignore_sources,
65 scan_state_changed: previous.root != current.root
66 || previous.complete != current.complete
67 || previous.termination != current.termination
68 || previous.portable != current.portable,
69 quality,
70 };
71 merge_files(&previous_files, ¤t_files, &mut delta);
72 detect_unique_renames(previous, current, &mut delta);
73 delta
74 }
75
76 #[must_use]
77 pub fn is_empty(&self) -> bool {
78 self.added.is_empty()
79 && self.removed.is_empty()
80 && self.modified.is_empty()
81 && self.renamed.is_empty()
82 && !self.selection_inputs_changed
83 && !self.scan_state_changed
84 }
85}
86
87fn delta_quality(previous: &ScanReport, current: &ScanReport) -> DeltaQuality {
88 if !previous.complete
89 || !current.complete
90 || previous.termination.is_some()
91 || current.termination.is_some()
92 {
93 return DeltaQuality::Partial;
94 }
95 if previous
96 .files
97 .iter()
98 .chain(¤t.files)
99 .any(|file| file.content_hash.is_none())
100 {
101 DeltaQuality::Metadata
102 } else {
103 DeltaQuality::ContentHash
104 }
105}
106
107fn merge_files(previous: &[&ScannedFile], current: &[&ScannedFile], delta: &mut ScanDelta) {
108 let (mut previous_index, mut current_index) = (0, 0);
109 while previous_index < previous.len() || current_index < current.len() {
110 match (previous.get(previous_index), current.get(current_index)) {
111 (Some(before), Some(after)) if before.relative == after.relative => {
112 if same_content(before, after) {
113 delta.unchanged = delta.unchanged.saturating_add(1);
114 } else {
115 delta.modified.push(ModifiedFile {
116 previous: (*before).clone(),
117 current: (*after).clone(),
118 });
119 }
120 previous_index += 1;
121 current_index += 1;
122 }
123 (Some(before), Some(after)) if before.relative < after.relative => {
124 delta.removed.push((*before).clone());
125 previous_index += 1;
126 }
127 (Some(_) | None, Some(after)) => {
128 delta.added.push((*after).clone());
129 current_index += 1;
130 }
131 (Some(before), None) => {
132 delta.removed.push((*before).clone());
133 previous_index += 1;
134 }
135 (None, None) => break,
136 }
137 }
138}
139
140fn same_content(previous: &ScannedFile, current: &ScannedFile) -> bool {
141 previous.bytes == current.bytes
142 && match (&previous.content_hash, ¤t.content_hash) {
143 (Some(previous), Some(current)) => previous == current,
144 _ => true,
145 }
146}
147
148fn detect_unique_renames(previous: &ScanReport, current: &ScanReport, delta: &mut ScanDelta) {
149 let previous_counts = hash_counts(&previous.files);
150 let current_counts = hash_counts(¤t.files);
151 let added_by_hash = unique_indices(&delta.added);
152 let removed_by_hash = unique_indices(&delta.removed);
153 let mut added_renames = vec![false; delta.added.len()];
154 let mut removed_renames = vec![false; delta.removed.len()];
155 for (hash, &removed_index) in &removed_by_hash {
156 let Some(&added_index) = added_by_hash.get(hash) else {
157 continue;
158 };
159 if previous_counts.get(hash) != Some(&1) || current_counts.get(hash) != Some(&1) {
160 continue;
161 }
162 removed_renames[removed_index] = true;
163 added_renames[added_index] = true;
164 delta.renamed.push(RenamedFile {
165 previous: delta.removed[removed_index].clone(),
166 current: delta.added[added_index].clone(),
167 });
168 }
169 delta.renamed.sort_unstable_by(|left, right| {
170 left.previous
171 .relative
172 .cmp(&right.previous.relative)
173 .then_with(|| left.current.relative.cmp(&right.current.relative))
174 });
175 retain_unmarked(&mut delta.added, &added_renames);
176 retain_unmarked(&mut delta.removed, &removed_renames);
177}
178
179fn hash_counts(files: &[ScannedFile]) -> BTreeMap<&str, usize> {
180 let mut counts = BTreeMap::new();
181 for hash in files.iter().filter_map(|file| file.content_hash.as_deref()) {
182 *counts.entry(hash).or_default() += 1;
183 }
184 counts
185}
186
187fn unique_indices(files: &[ScannedFile]) -> BTreeMap<&str, usize> {
188 let mut indices = BTreeMap::new();
189 let mut duplicates = BTreeSet::new();
190 for (index, hash) in files
191 .iter()
192 .enumerate()
193 .filter_map(|(index, file)| file.content_hash.as_deref().map(|hash| (index, hash)))
194 {
195 if indices.insert(hash, index).is_some() {
196 duplicates.insert(hash);
197 }
198 }
199 indices.retain(|hash, _| !duplicates.contains(hash));
200 indices
201}
202
203fn retain_unmarked(files: &mut Vec<ScannedFile>, marked: &[bool]) {
204 let mut index = 0;
205 files.retain(|_| {
206 let retain = !marked[index];
207 index += 1;
208 retain
209 });
210}