xvc_pipeline/pipeline/deps/
compare.rs

1//! Compare all different types of dependencies
2use crate::error::Result;
3use crate::pipeline::StepStateParams;
4use crate::XvcEntity;
5use anyhow::anyhow;
6
7use xvc_core::types::diff::Diffable;
8use xvc_core::XvcPathMetadataMap;
9
10use xvc_core::{Diff, HashAlgorithm, TextOrBinary, XvcPath, XvcRoot};
11use xvc_core::{HStore, Storable};
12
13use super::glob::GlobDep;
14use super::line_items::LineItemsDep;
15use super::lines::LinesDep;
16use super::regex::RegexDep;
17use super::regex_items::RegexItemsDep;
18use super::sqlite_query::SqliteQueryDep;
19use super::step::StepDep;
20use super::{ParamDep, XvcDependency};
21
22use super::file::FileDep;
23use super::generic::GenericDep;
24use super::glob_items::GlobItemsDep;
25use super::url::UrlDigestDep;
26
27#[derive(Clone, Debug)]
28/// Stored and gathered data to decide the validation of dependencies
29pub struct DependencyComparisonParams<'a> {
30    /// The base Xvc directory
31    pub xvc_root: &'a XvcRoot,
32    /// Where the pipeline runs
33    pub pipeline_rundir: &'a XvcPath,
34    /// All xvc paths and their metadata in the repository
35    pub pmm: &'a XvcPathMetadataMap,
36    /// The hash algorithm co compare files
37    pub algorithm: &'a HashAlgorithm,
38    /// Dependencies for each step
39    pub step_dependencies: &'a HStore<XvcDependency>,
40}
41
42impl Diffable for XvcDependency {
43    type Item = XvcDependency;
44
45    fn diff(record: Option<&XvcDependency>, actual: Option<&XvcDependency>) -> Diff<XvcDependency> {
46        match (record, actual) {
47            (None, None) => Diff::Skipped,
48            (None, Some(actual)) => Diff::RecordMissing {
49                actual: actual.clone(),
50            },
51            (Some(record), None) => Diff::ActualMissing {
52                record: record.clone(),
53            },
54            (Some(record), Some(actual)) => match (record, actual) {
55                (XvcDependency::Step(record), XvcDependency::Step(actual)) => {
56                    diff_of_dep(StepDep::diff(Some(record), Some(actual)))
57                }
58
59                (XvcDependency::Generic(record), XvcDependency::Generic(actual)) => {
60                    diff_of_dep(GenericDep::diff(Some(record), Some(actual)))
61                }
62
63                (XvcDependency::File(record), XvcDependency::File(actual)) => {
64                    diff_of_dep(FileDep::diff(Some(record), Some(actual)))
65                }
66
67                (XvcDependency::GlobItems(record), XvcDependency::GlobItems(actual)) => {
68                    diff_of_dep(GlobItemsDep::diff(Some(record), Some(actual)))
69                }
70
71                (XvcDependency::Glob(record), XvcDependency::Glob(actual)) => {
72                    diff_of_dep(GlobDep::diff(Some(record), Some(actual)))
73                }
74
75                (XvcDependency::RegexItems(record), XvcDependency::RegexItems(actual)) => {
76                    diff_of_dep(RegexItemsDep::diff(Some(record), Some(actual)))
77                }
78
79                (XvcDependency::Regex(record), XvcDependency::Regex(actual)) => {
80                    diff_of_dep(RegexDep::diff(Some(record), Some(actual)))
81                }
82
83                (XvcDependency::Param(record), XvcDependency::Param(actual)) => {
84                    diff_of_dep(ParamDep::diff(Some(record), Some(actual)))
85                }
86
87                (XvcDependency::LineItems(record), XvcDependency::LineItems(actual)) => {
88                    diff_of_dep(LineItemsDep::diff(Some(record), Some(actual)))
89                }
90
91                (XvcDependency::UrlDigest(record), XvcDependency::UrlDigest(actual)) => {
92                    diff_of_dep(UrlDigestDep::diff(Some(record), Some(actual)))
93                }
94
95                (XvcDependency::Lines(record), XvcDependency::Lines(actual)) => {
96                    diff_of_dep(LinesDep::diff(Some(record), Some(actual)))
97                }
98                _ => unreachable!("All dependencies should be of the same type"),
99            },
100        }
101    }
102
103    fn diff_superficial(record: &Self::Item, actual: &Self::Item) -> Diff<Self::Item> {
104        match (record, actual) {
105            (XvcDependency::Step(record), XvcDependency::Step(actual)) => {
106                diff_of_dep(StepDep::diff_superficial(record, actual))
107            }
108
109            (XvcDependency::Generic(record), XvcDependency::Generic(actual)) => {
110                diff_of_dep(GenericDep::diff_superficial(record, actual))
111            }
112
113            (XvcDependency::File(record), XvcDependency::File(actual)) => {
114                diff_of_dep(FileDep::diff_superficial(record, actual))
115            }
116
117            (XvcDependency::GlobItems(record), XvcDependency::GlobItems(actual)) => {
118                diff_of_dep(GlobItemsDep::diff_superficial(record, actual))
119            }
120
121            (XvcDependency::Glob(record), XvcDependency::Glob(actual)) => {
122                diff_of_dep(GlobDep::diff_superficial(record, actual))
123            }
124
125            (XvcDependency::RegexItems(record), XvcDependency::RegexItems(actual)) => {
126                diff_of_dep(RegexItemsDep::diff_superficial(record, actual))
127            }
128
129            (XvcDependency::Regex(record), XvcDependency::Regex(actual)) => {
130                diff_of_dep(RegexDep::diff_superficial(record, actual))
131            }
132
133            (XvcDependency::Param(record), XvcDependency::Param(actual)) => {
134                diff_of_dep(ParamDep::diff_superficial(record, actual))
135            }
136
137            (XvcDependency::LineItems(record), XvcDependency::LineItems(actual)) => {
138                diff_of_dep(LineItemsDep::diff_superficial(record, actual))
139            }
140
141            (XvcDependency::Lines(record), XvcDependency::Lines(actual)) => {
142                diff_of_dep(LinesDep::diff_superficial(record, actual))
143            }
144
145            (XvcDependency::UrlDigest(record), XvcDependency::UrlDigest(actual)) => {
146                diff_of_dep(UrlDigestDep::diff_superficial(record, actual))
147            }
148
149            _ => unreachable!("All dependencies should be of the same type"),
150        }
151    }
152
153    fn diff_thorough(record: &Self::Item, actual: &Self::Item) -> Diff<Self::Item> {
154        match (record, actual) {
155            (XvcDependency::Step(record), XvcDependency::Step(actual)) => {
156                diff_of_dep(StepDep::diff_thorough(record, actual))
157            }
158
159            (XvcDependency::Generic(record), XvcDependency::Generic(actual)) => {
160                let actual = actual.clone().update_output_digest().unwrap();
161                diff_of_dep(GenericDep::diff_thorough(record, &actual))
162            }
163
164            (XvcDependency::File(record), XvcDependency::File(actual)) => {
165                diff_of_dep(FileDep::diff_thorough(record, actual))
166            }
167
168            (XvcDependency::GlobItems(record), XvcDependency::GlobItems(actual)) => {
169                diff_of_dep(GlobItemsDep::diff_thorough(record, actual))
170            }
171
172            (XvcDependency::Glob(record), XvcDependency::Glob(actual)) => {
173                diff_of_dep(GlobDep::diff_thorough(record, actual))
174            }
175
176            (XvcDependency::RegexItems(record), XvcDependency::RegexItems(actual)) => {
177                diff_of_dep(RegexItemsDep::diff_thorough(record, actual))
178            }
179
180            (XvcDependency::Regex(record), XvcDependency::Regex(actual)) => {
181                diff_of_dep(RegexDep::diff_thorough(record, actual))
182            }
183
184            (XvcDependency::Param(record), XvcDependency::Param(actual)) => {
185                diff_of_dep(ParamDep::diff_thorough(record, actual))
186            }
187
188            (XvcDependency::LineItems(record), XvcDependency::LineItems(actual)) => {
189                diff_of_dep(LineItemsDep::diff_thorough(record, actual))
190            }
191
192            (XvcDependency::Lines(record), XvcDependency::Lines(actual)) => {
193                diff_of_dep(LinesDep::diff_thorough(record, actual))
194            }
195
196            (XvcDependency::UrlDigest(record), XvcDependency::UrlDigest(actual)) => {
197                diff_of_dep(UrlDigestDep::diff_thorough(record, actual))
198            }
199
200            _ => unreachable!("All dependencies should be of the same type"),
201        }
202    }
203}
204
205///
206/// compares two dependencies of the same type
207///
208/// Decides the dependency type by loading the stored dependency.
209/// Calls the respective comparison function for the loaded dependency type.
210///
211pub fn thorough_compare_dependency(
212    cmp_params: &StepStateParams,
213    stored_dependency_e: XvcEntity,
214) -> Result<Diff<XvcDependency>> {
215    let stored = if cmp_params.all_steps.contains_key(&stored_dependency_e) {
216        let step = cmp_params.all_steps[&stored_dependency_e].clone();
217        Ok(XvcDependency::Step(StepDep {
218            name: step.name.clone(),
219        }))
220    } else {
221        cmp_params
222            .recorded_dependencies
223            .children
224            .get(&stored_dependency_e)
225            .cloned()
226            .ok_or(anyhow!(
227                "Stored dependency {:?} not found in step dependencies",
228                stored_dependency_e
229            ))
230    }?;
231
232    let diff = match stored {
233        // Step dependencies are handled differently
234        XvcDependency::Step(_) => Diff::Skipped,
235        XvcDependency::Generic(generic) => diff_of_dep(thorough_compare_generic(&generic)?),
236        XvcDependency::File(file_dep) => diff_of_dep(thorough_compare_file(cmp_params, &file_dep)?),
237        XvcDependency::GlobItems(glob_dep) => {
238            diff_of_dep(thorough_compare_glob_items(cmp_params, &glob_dep)?)
239        }
240        XvcDependency::UrlDigest(url_dep) => diff_of_dep(thorough_compare_url(&url_dep)?),
241        XvcDependency::Param(param_dep) => {
242            diff_of_dep(thorough_compare_param(cmp_params, &param_dep)?)
243        }
244        XvcDependency::RegexItems(dep) => {
245            diff_of_dep(thorough_compare_regex_items(cmp_params, &dep)?)
246        }
247        XvcDependency::LineItems(lines_dep) => {
248            diff_of_dep(thorough_compare_line_items(cmp_params, &lines_dep)?)
249        }
250        XvcDependency::Glob(dep) => diff_of_dep(thorough_compare_glob(cmp_params, &dep)?),
251        XvcDependency::Regex(dep) => diff_of_dep(thorough_compare_regex(cmp_params, &dep)?),
252        XvcDependency::Lines(dep) => diff_of_dep(thorough_compare_lines(cmp_params, &dep)?),
253        XvcDependency::SqliteQueryDigest(dep) => {
254            diff_of_dep(thorough_compare_query_digest(cmp_params, &dep)?)
255        }
256    };
257
258    Ok(diff)
259}
260
261/// Runs the command and compares the output with the stored dependency
262fn thorough_compare_generic(record: &GenericDep) -> Result<Diff<GenericDep>> {
263    let mut actual = GenericDep::new(record.generic_command.clone());
264    actual = actual.update_output_digest()?;
265    Ok(GenericDep::diff_thorough(record, &actual))
266}
267
268/// Compares a dependency path with the actual metadata and content digest found on disk
269fn thorough_compare_file(cmp_params: &StepStateParams, record: &FileDep) -> Result<Diff<FileDep>> {
270    let actual = FileDep::from_pmp(&record.path, cmp_params.pmp)?;
271    let actual = actual.calculate_content_digest(
272        cmp_params.xvc_root,
273        cmp_params.algorithm,
274        TextOrBinary::Auto,
275    )?;
276    Ok(FileDep::diff_thorough(record, &actual))
277}
278
279fn thorough_compare_url(record: &UrlDigestDep) -> Result<Diff<UrlDigestDep>> {
280    let actual = UrlDigestDep::new(record.url.clone()).update_content_digest()?;
281    Ok(UrlDigestDep::diff_thorough(record, &actual))
282}
283
284fn thorough_compare_param(
285    cmp_params: &StepStateParams,
286    record: &ParamDep,
287) -> Result<Diff<ParamDep>> {
288    let actual = ParamDep::new(&record.path, Some(record.format), record.key.clone())?
289        .update_metadata(cmp_params.pmp)?
290        .update_value(cmp_params.xvc_root)?;
291    Ok(ParamDep::diff_thorough(record, &actual))
292}
293
294fn thorough_compare_line_items(
295    cmp_params: &StepStateParams,
296    record: &LineItemsDep,
297) -> Result<Diff<LineItemsDep>> {
298    let actual = LineItemsDep::new(record.path.clone(), record.begin, record.end)
299        .update_metadata(cmp_params.pmp.get(&record.path))
300        .update_lines(cmp_params.xvc_root);
301    Ok(LineItemsDep::diff(Some(record), Some(&actual)))
302}
303
304/// Compares two globs, one stored and one current.
305fn thorough_compare_glob_items(
306    cmp_params: &StepStateParams,
307    record: &GlobItemsDep,
308) -> Result<Diff<GlobItemsDep>> {
309    let glob_root = cmp_params.pipeline_rundir;
310    let xvc_root = cmp_params.xvc_root;
311    let pmp = cmp_params.pmp;
312    let algorithm = cmp_params.algorithm;
313    // Calls update_paths in the update_changed_paths_digests method
314    let actual = record
315        .clone()
316        .update_changed_paths_digests(record, xvc_root, glob_root, pmp, algorithm)?;
317    Ok(GlobItemsDep::diff(Some(record), Some(&actual)))
318}
319
320fn thorough_compare_glob(cmp_params: &StepStateParams, record: &GlobDep) -> Result<Diff<GlobDep>> {
321    let actual = GlobDep::new(record.glob.clone()).update_collection_digests(cmp_params.pmp)?;
322    match GlobDep::diff_superficial(record, &actual) {
323        Diff::Different { record, actual } => {
324            let actual = actual.update_content_digest(cmp_params.xvc_root, cmp_params.pmp)?;
325            Ok(GlobDep::diff_thorough(&record, &actual))
326        }
327        Diff::RecordMissing { actual } => {
328            let actual = actual.update_content_digest(cmp_params.xvc_root, cmp_params.pmp)?;
329            Ok(GlobDep::diff_thorough(record, &actual))
330        }
331        diff => Ok(diff),
332    }
333}
334
335fn thorough_compare_regex(
336    cmp_params: &StepStateParams,
337    record: &RegexDep,
338) -> Result<Diff<RegexDep>> {
339    let actual = RegexDep::new(record.path.clone(), record.regex.clone())
340        .update_metadata(cmp_params.pmp.get(&record.path));
341    // Short circuit if the metadata is identical
342    match RegexDep::diff_superficial(record, &actual) {
343        Diff::Different { record, actual } => {
344            let actual = actual.update_digest(cmp_params.xvc_root, cmp_params.algorithm);
345            Ok(RegexDep::diff_thorough(&record, &actual))
346        }
347        Diff::RecordMissing { actual } => {
348            let actual = actual.update_digest(cmp_params.xvc_root, cmp_params.algorithm);
349            Ok(Diff::RecordMissing { actual })
350        }
351        diff => Ok(diff),
352    }
353}
354
355fn thorough_compare_regex_items(
356    cmp_params: &StepStateParams,
357    record: &RegexItemsDep,
358) -> Result<Diff<RegexItemsDep>> {
359    let actual = RegexItemsDep::new(record.path.clone(), record.regex.clone())
360        .update_metadata(cmp_params.pmp.get(&record.path));
361    // Shortcircuit if the metadata is identical
362    match RegexItemsDep::diff_superficial(record, &actual) {
363        Diff::Different { record, actual } => {
364            let actual = actual.update_lines(cmp_params.xvc_root);
365            Ok(RegexItemsDep::diff_thorough(&record, &actual))
366        }
367        Diff::RecordMissing { actual } => {
368            let actual = actual.update_lines(cmp_params.xvc_root);
369            Ok(Diff::RecordMissing { actual })
370        }
371        diff => Ok(diff),
372    }
373}
374
375fn thorough_compare_lines(
376    cmp_params: &StepStateParams,
377    record: &LinesDep,
378) -> Result<Diff<LinesDep>> {
379    let actual = LinesDep::new(record.path.clone(), record.begin, record.end)
380        .update_metadata(cmp_params.pmp.get(&record.path));
381
382    // Shortcircuit if the metadata is identical
383    match LinesDep::diff_superficial(record, &actual) {
384        Diff::Different { record, actual } => {
385            let actual = actual.update_digest(cmp_params.xvc_root, cmp_params.algorithm);
386            Ok(LinesDep::diff_thorough(&record, &actual))
387        }
388        Diff::RecordMissing { actual } => {
389            let actual = actual.update_digest(cmp_params.xvc_root, cmp_params.algorithm);
390            Ok(Diff::RecordMissing { actual })
391        }
392        diff => Ok(diff),
393    }
394}
395
396fn thorough_compare_query_digest(
397    cmp_params: &StepStateParams,
398    record: &SqliteQueryDep,
399) -> Result<Diff<SqliteQueryDep>> {
400    let actual = SqliteQueryDep::new(record.path.clone(), record.query.clone())
401        .update_metadata(cmp_params.pmp.get(&record.path));
402
403    // Shortcircuit if the metadata is identical
404    match SqliteQueryDep::diff_superficial(record, &actual) {
405        Diff::Different { record, actual } => {
406            let actual = actual.update_digest(cmp_params.xvc_root, cmp_params.algorithm)?;
407            Ok(SqliteQueryDep::diff_thorough(&record, &actual))
408        }
409        Diff::RecordMissing { actual } => {
410            let actual = actual.update_digest(cmp_params.xvc_root, cmp_params.algorithm)?;
411            Ok(Diff::RecordMissing { actual })
412        }
413        diff => Ok(diff),
414    }
415}
416
417/// Compares dependencies with their earlier version _superficially_, meaning the cost of
418/// comparison is minimized by being optimistic that a dependency is unchanged.
419///
420/// For example, we compare the metadata of a file instead of its content to see if it has not
421/// changed.
422///
423/// This function loads the depdency from the store and calls the respective comparison function.
424pub fn superficial_compare_dependency(
425    cmp_params: &StepStateParams,
426    stored_dependency_e: XvcEntity,
427) -> Result<Diff<XvcDependency>> {
428    // If the dependency is a step, we reify it here
429    // Otherwise we search the dependencies for its key
430    let stored = if cmp_params.all_steps.contains_key(&stored_dependency_e) {
431        let step = cmp_params.all_steps[&stored_dependency_e].clone();
432        Ok(XvcDependency::Step(StepDep { name: step.name }))
433    } else {
434        cmp_params
435            .recorded_dependencies
436            .children
437            .get(&stored_dependency_e)
438            .cloned()
439            .ok_or(anyhow!(
440                "Stored dependency {:?} not found in step dependencies",
441                stored_dependency_e
442            ))
443    }?;
444    let diff = match &stored {
445        // Step dependencies are handled differently
446        XvcDependency::Step(_) => Diff::Skipped,
447        XvcDependency::Generic(generic) => {
448            diff_of_dep(superficial_compare_generic(cmp_params, generic)?)
449        }
450        XvcDependency::File(file_dep) => {
451            diff_of_dep(superficial_compare_file(cmp_params, file_dep)?)
452        }
453        XvcDependency::GlobItems(glob_dep) => {
454            diff_of_dep(superficial_compare_glob_items(cmp_params, glob_dep)?)
455        }
456        XvcDependency::UrlDigest(dep) => diff_of_dep(superficial_compare_url(dep)?),
457        XvcDependency::Param(dep) => diff_of_dep(superficial_compare_param(cmp_params, dep)?),
458        XvcDependency::RegexItems(dep) => {
459            diff_of_dep(superficial_compare_regex_items(cmp_params, dep)?)
460        }
461        XvcDependency::LineItems(dep) => {
462            diff_of_dep(superficial_compare_line_items(cmp_params, dep)?)
463        }
464        XvcDependency::Glob(dep) => diff_of_dep(superficial_compare_glob(cmp_params, dep)?),
465        XvcDependency::Regex(dep) => diff_of_dep(superficial_compare_regex(cmp_params, dep)?),
466        XvcDependency::Lines(dep) => diff_of_dep(superficial_compare_lines(cmp_params, dep)?),
467        XvcDependency::SqliteQueryDigest(dep) => {
468            diff_of_dep(superficial_compare_query_digest(cmp_params, dep)?)
469        }
470    };
471
472    Ok(diff)
473}
474
475/// Runs the command and compares the output with the stored dependency
476fn superficial_compare_generic(
477    _cmp_params: &StepStateParams,
478    record: &GenericDep,
479) -> Result<Diff<GenericDep>> {
480    let actual = GenericDep::new(record.generic_command.clone());
481    let actual = actual.update_output_digest()?;
482    Ok(GenericDep::diff_superficial(record, &actual))
483}
484
485/// Compares a dependency path with the actual metadata and content digest found on disk
486fn superficial_compare_file(
487    cmp_params: &StepStateParams,
488    record: &FileDep,
489) -> Result<Diff<FileDep>> {
490    let actual = FileDep::from_pmp(&record.path, cmp_params.pmp)?;
491    Ok(FileDep::diff_superficial(record, &actual))
492}
493
494fn superficial_compare_url(record: &UrlDigestDep) -> Result<Diff<UrlDigestDep>> {
495    let actual = UrlDigestDep::new(record.url.clone()).update_headers()?;
496    Ok(UrlDigestDep::diff_superficial(record, &actual))
497}
498
499fn superficial_compare_param(
500    cmp_params: &StepStateParams,
501    record: &ParamDep,
502) -> Result<Diff<ParamDep>> {
503    let actual = ParamDep::new(&record.path, Some(record.format), record.key.clone())?
504        .update_metadata(cmp_params.pmp)?;
505    Ok(ParamDep::diff_superficial(record, &actual))
506}
507
508fn superficial_compare_regex_items(
509    cmp_params: &StepStateParams,
510    record: &RegexItemsDep,
511) -> Result<Diff<RegexItemsDep>> {
512    let actual = RegexItemsDep::new(record.path.clone(), record.regex.clone())
513        .update_metadata(cmp_params.pmp.get(&record.path));
514    Ok(RegexItemsDep::diff_superficial(record, &actual))
515}
516
517fn superficial_compare_line_items(
518    cmp_params: &StepStateParams,
519    record: &LineItemsDep,
520) -> Result<Diff<LineItemsDep>> {
521    let actual = LineItemsDep::new(record.path.clone(), record.begin, record.end)
522        .update_metadata(cmp_params.pmp.get(&record.path));
523    Ok(LineItemsDep::diff_superficial(record, &actual))
524}
525
526/// Compares two globs, one stored and one current.
527fn superficial_compare_glob_items(
528    cmp_params: &StepStateParams,
529    record: &GlobItemsDep,
530) -> Result<Diff<GlobItemsDep>> {
531    // We just compare the file list
532    let actual = record
533        .clone()
534        .update_paths(cmp_params.pipeline_rundir, cmp_params.pmp)?;
535    Ok(GlobItemsDep::diff_superficial(record, &actual))
536}
537
538fn superficial_compare_glob(
539    cmp_params: &StepStateParams,
540    record: &GlobDep,
541) -> Result<Diff<GlobDep>> {
542    let actual = GlobDep::new(record.glob.clone()).update_collection_digests(cmp_params.pmp)?;
543    Ok(GlobDep::diff_superficial(record, &actual))
544}
545
546fn superficial_compare_regex(
547    cmp_params: &StepStateParams,
548    record: &RegexDep,
549) -> Result<Diff<RegexDep>> {
550    let actual = RegexDep::new(record.path.clone(), record.regex.clone())
551        .update_metadata(cmp_params.pmp.get(&record.path));
552    let diff = RegexDep::diff_superficial(record, &actual);
553    Ok(diff)
554}
555
556fn superficial_compare_lines(
557    cmp_params: &StepStateParams,
558    record: &LinesDep,
559) -> Result<Diff<LinesDep>> {
560    let actual = LinesDep::new(record.path.clone(), record.begin, record.end)
561        .update_metadata(cmp_params.pmp.get(&record.path));
562
563    Ok(LinesDep::diff_superficial(record, &actual))
564}
565
566fn superficial_compare_query_digest(
567    cmp_params: &StepStateParams,
568    record: &SqliteQueryDep,
569) -> Result<Diff<SqliteQueryDep>> {
570    let actual = SqliteQueryDep::new(record.path.clone(), record.query.clone())
571        .update_metadata(cmp_params.pmp.get(&record.path));
572
573    Ok(SqliteQueryDep::diff_superficial(record, &actual))
574}
575
576fn diff_of_dep<T>(dep: Diff<T>) -> Diff<XvcDependency>
577where
578    T: Storable + Into<XvcDependency>,
579{
580    match dep {
581        Diff::Identical => Diff::Identical,
582        Diff::RecordMissing { actual } => Diff::RecordMissing {
583            actual: actual.into(),
584        },
585        Diff::ActualMissing { record } => Diff::ActualMissing {
586            record: record.into(),
587        },
588        Diff::Different { record, actual } => Diff::Different {
589            record: record.into(),
590            actual: actual.into(),
591        },
592        Diff::Skipped => Diff::Skipped,
593    }
594}