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