xvc_core/types/diff.rs
1//! Compare two stores and find out which values are different.
2//!
3//! [Diff] keeps possible differences between two stores of the same type.
4//! [DiffStore] keeps the diffs for all entities in a stores.
5//!
6//! Two [Storable] types are compared and the result is a [Diff] of the same
7//! type.
8//! These diffs make up a store, which is a [DiffStore].
9
10use std::collections::HashSet;
11
12use crate::Result;
13use serde::{Deserialize, Serialize};
14use xvc_ecs::{HStore, Storable, XvcEntity, XvcStore};
15use xvc_logging::warn;
16
17/// Shows which information is identical, missing or different in diff calculations.
18///
19/// We use this to compare anything that's Storable.
20#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash, Clone, Default)]
21#[serde(bound = "T: Serialize, for<'lt> T: Deserialize<'lt>")]
22pub enum Diff<T: Storable> {
23 /// Both record and actual values are identical.
24 Identical,
25 /// We don't have the record, but we have the actual value
26 RecordMissing {
27 /// The actual value found (probably) in workspace
28 actual: T,
29 },
30 /// We have the record, but we don't have the actual value
31 ActualMissing {
32 /// The record value found in the stores
33 record: T,
34 },
35 /// Both record and actual values are present, but they differ
36 Different {
37 /// The value found in store
38 record: T,
39 /// The value found in workspace
40 actual: T,
41 },
42 /// We skipped this comparison. It's not an error, but it means we didn't compare this field.
43 /// It may be shortcut, we don't care or irrelevant.
44 #[default]
45 Skipped,
46}
47
48impl<T> Storable for Diff<T>
49where
50 T: Storable,
51{
52 fn type_description() -> String {
53 format!("diff-{}", T::type_description())
54 }
55}
56
57/// Keeping track of differences between two stores of the same type.
58pub type DiffStore<T> = HStore<Diff<T>>;
59
60/// Compare `records` with `actuals` and return the missing or changed values.
61/// This is used find out when something changes in the workspace.
62///
63/// If `subset` is `None`, we compare all entities in both stores. Otherwise we only compare the entities in `subset`.
64pub fn diff_store<T: Storable>(
65 records: &XvcStore<T>,
66 actuals: &HStore<T>,
67 subset: Option<&HashSet<XvcEntity>>,
68) -> DiffStore<T> {
69 let mut all_entities = HashSet::new();
70 let entities = if let Some(subset) = subset {
71 subset
72 } else {
73 all_entities.extend(records.keys().chain(actuals.keys()).copied());
74 &all_entities
75 };
76
77 let mut diff_store = HStore::new();
78
79 for xe in entities {
80 let record_value = records.get(xe);
81 let actual_value = actuals.get(xe);
82
83 match (record_value, actual_value) {
84 (None, None) => {
85 warn!("Both record and actual values are missing for {:?}", xe);
86 continue;
87 }
88 (None, Some(actual_value)) => {
89 diff_store.insert(
90 *xe,
91 Diff::RecordMissing {
92 actual: actual_value.clone(),
93 },
94 );
95 }
96 (Some(record_value), None) => {
97 diff_store.insert(
98 *xe,
99 Diff::ActualMissing {
100 record: record_value.clone(),
101 },
102 );
103 }
104 (Some(record_value), Some(actual_value)) => {
105 if record_value == actual_value {
106 diff_store.insert(*xe, Diff::Identical);
107 } else {
108 diff_store.insert(
109 *xe,
110 Diff::Different {
111 record: record_value.clone(),
112 actual: actual_value.clone(),
113 },
114 );
115 }
116 }
117 }
118 }
119 diff_store
120}
121
122/// Update `records` loaded from store with the changed values in `diffs`.
123/// When the actual values are changed and we want to update the store, we use this function.
124///
125/// It always updates values that are changed. (See [Diff::Different])
126/// If `add_new` is `true`, we add new values to `records`. (See [Diff::RecordMissing])
127/// If `remove_missing` is `true`, we remove missing values from `records`. (See [Diff::ActualMissing])
128///
129/// See [apply_diff] for a version that doesn't modify the original store.
130pub fn update_with_actual<T: Storable>(
131 records: &mut XvcStore<T>,
132 diffs: &DiffStore<T>,
133 add_new: bool,
134 remove_missing: bool,
135) -> Result<()> {
136 for (xe, diff) in diffs.iter() {
137 match diff {
138 Diff::Identical => {}
139 Diff::RecordMissing { actual } => {
140 if add_new {
141 records.insert(*xe, actual.clone());
142 }
143 }
144 Diff::ActualMissing { .. } => {
145 if remove_missing {
146 records.remove(*xe);
147 }
148 }
149 Diff::Different { actual, .. } => {
150 records.insert(*xe, actual.clone());
151 }
152 Diff::Skipped => {}
153 }
154 }
155 Ok(())
156}
157
158/// Create a new store from `records` and with the changed values in `diffs`.
159/// When the actual values are changed and we want a new store with new values, we use this function.
160///
161/// It always updates values that are changed. (See [Diff::Different])
162/// If `add_new` is `true`, we add new values to `records`. (See [Diff::RecordMissing])
163/// If `remove_missing` is `true`, we remove missing values from `records`. (See [Diff::ActualMissing])
164///
165/// See [update_with_actual] for a version that modifies the original store in
166/// place.
167pub fn apply_diff<T: Storable>(
168 records: &XvcStore<T>,
169 diffs: &DiffStore<T>,
170 add_new: bool,
171 remove_missing: bool,
172) -> Result<XvcStore<T>> {
173 let mut new_records = records.clone();
174 for (xe, diff) in diffs.iter() {
175 match diff {
176 Diff::Identical | Diff::Skipped => {}
177 Diff::RecordMissing { actual } => {
178 if add_new {
179 new_records.insert(*xe, actual.clone());
180 }
181 }
182 Diff::ActualMissing { .. } => {
183 if remove_missing {
184 new_records.remove(*xe);
185 }
186 }
187 Diff::Different { actual, .. } => {
188 new_records.insert(*xe, actual.clone());
189 }
190 }
191 }
192 Ok(new_records)
193}
194
195impl<T: Storable> Diff<T> {
196 /// Return true if the diff is not [Diff::Identical] or [Diff::Skipped].
197 /// This is used to find out if T is changed.
198 pub fn changed(&self) -> bool {
199 match self {
200 Diff::Identical => false,
201 Diff::RecordMissing { .. } => true,
202 Diff::ActualMissing { .. } => true,
203 Diff::Different { .. } => true,
204 Diff::Skipped => false,
205 }
206 }
207}
208
209/// Keep two diffs for the same set of entities
210///
211/// This is used, for example, to keep path and metadata diffs for the same
212/// entities.
213pub struct DiffStore2<T, U>(pub DiffStore<T>, pub DiffStore<U>)
214where
215 T: Storable,
216 U: Storable;
217
218impl<T, U> DiffStore2<T, U>
219where
220 T: Storable,
221 U: Storable,
222{
223 /// Return a tuple of diffs for the same entity
224 pub fn diff_tuple(&self, xe: XvcEntity) -> (Diff<T>, Diff<U>) {
225 (
226 self.0.get(&xe).cloned().expect("Missing diff1"),
227 self.1.get(&xe).cloned().expect("Missing diff2"),
228 )
229 }
230}
231
232/// Keep three diffs for the same set of entities
233///
234/// This is used, for example, to keep path, metadata and digest diffs for the
235/// same set of entities.
236pub struct DiffStore3<T, U, V>(pub DiffStore<T>, pub DiffStore<U>, pub DiffStore<V>)
237where
238 T: Storable,
239 U: Storable,
240 V: Storable;
241
242impl<T, U, V> DiffStore3<T, U, V>
243where
244 T: Storable,
245 U: Storable,
246 V: Storable,
247{
248 /// Return a tuple of diffs for the same entity
249 pub fn diff_tuple(&self, xe: XvcEntity) -> (Diff<T>, Diff<U>, Diff<V>) {
250 (
251 self.0.get(&xe).cloned().expect("Missing diff1"),
252 self.1.get(&xe).cloned().expect("Missing diff2"),
253 self.2.get(&xe).cloned().expect("Missing diff3"),
254 )
255 }
256}
257
258/// Keep four diffs for the same set of entities
259///
260/// This is used, for example, to keep path, metadata, digest and collection
261/// diffs for certain entities
262pub struct DiffStore4<T: Storable, U: Storable, V: Storable, W: Storable>(
263 DiffStore<T>,
264 DiffStore<U>,
265 DiffStore<V>,
266 DiffStore<W>,
267);
268
269impl<T: Storable, U: Storable, V: Storable, W: Storable> DiffStore4<T, U, V, W> {
270 /// Return a tuple of diffs for the same entity
271 pub fn diff_tuple(&self, xe: XvcEntity) -> (Diff<T>, Diff<U>, Diff<V>, Diff<W>) {
272 (
273 self.0.get(&xe).cloned().expect("Missing diff1"),
274 self.1.get(&xe).cloned().expect("Missing diff2"),
275 self.2.get(&xe).cloned().expect("Missing diff3"),
276 self.3.get(&xe).cloned().expect("Missing diff4"),
277 )
278 }
279}
280
281/// Used to find out if record and actual are different for type T
282pub trait Diffable {
283 /// The type of the entity to compare.
284 type Item: Storable;
285
286 /// ⚠️ Usually you must update actual's metadata and timestamp before calling this.
287 /// Use diff_superficial and diff_thorough for shortcut comparisons. (e.g. when metadata is not changed, no need to
288 /// compare the content. )
289 ///
290 /// This is to convert optional entities to diffs.
291 /// For example, a file may be missing from the disk, but it may exist in the records.
292 /// ((Some(record), None) -> Diff::ActualMissing)
293 fn diff(record: Option<&Self::Item>, actual: Option<&Self::Item>) -> Diff<Self::Item> {
294 match (record, actual) {
295 (None, None) => unreachable!("Both record and actual are None"),
296 (None, Some(actual)) => Diff::RecordMissing {
297 actual: actual.clone(),
298 },
299 (Some(record), None) => Diff::ActualMissing {
300 record: record.clone(),
301 },
302 (Some(record), Some(actual)) => {
303 if record == actual {
304 Diff::Identical
305 } else {
306 Diff::Different {
307 record: record.clone(),
308 actual: actual.clone(),
309 }
310 }
311 }
312 }
313 }
314
315 /// This is to compare two entities with a quick comparison.
316 /// Example: metadata of a file, timestamp of a URL etc.
317 /// You may need to update actual's metadata or timestamp before calling this.
318 fn diff_superficial(record: &Self::Item, actual: &Self::Item) -> Diff<Self::Item> {
319 if record == actual {
320 Diff::Identical
321 } else {
322 Diff::Different {
323 record: record.clone(),
324 actual: actual.clone(),
325 }
326 }
327 }
328
329 /// This is to calculate two entities with a thorough comparison.
330 /// Example: content of a file, content of a URL etc.
331 /// You may need to update actual's content before calling this.
332 fn diff_thorough(record: &Self::Item, actual: &Self::Item) -> Diff<Self::Item> {
333 Self::diff_superficial(record, actual)
334 }
335}