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}