gix_diff/blob/
platform.rs

1use bstr::{BStr, BString, ByteSlice};
2use std::cmp::Ordering;
3use std::{io::Write, process::Stdio};
4
5use super::Algorithm;
6use crate::blob::{pipeline, Pipeline, Platform, ResourceKind};
7
8/// A key to uniquely identify either a location in the worktree, or in the object database.
9#[derive(Clone)]
10pub(crate) struct CacheKey {
11    id: gix_hash::ObjectId,
12    location: BString,
13    /// If `true`, this is an `id` based key, otherwise it's location based.
14    use_id: bool,
15    /// Only relevant when `id` is not null, to further differentiate content and allow us to
16    /// keep track of both links and blobs with the same content (rare, but possible).
17    is_link: bool,
18}
19
20/// A stored value representing a diffable resource.
21#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
22pub(crate) struct CacheValue {
23    /// The outcome of converting a resource into a diffable format using [Pipeline::convert_to_diffable()].
24    conversion: pipeline::Outcome,
25    /// The kind of the resource we are looking at. Only possible values are `Blob`, `BlobExecutable` and `Link`.
26    mode: gix_object::tree::EntryKind,
27    /// A possibly empty buffer, depending on `conversion.data` which may indicate the data is considered binary.
28    buffer: Vec<u8>,
29}
30
31impl std::hash::Hash for CacheKey {
32    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
33        if self.use_id {
34            self.id.hash(state);
35            self.is_link.hash(state);
36        } else {
37            self.location.hash(state);
38        }
39    }
40}
41
42impl PartialEq for CacheKey {
43    fn eq(&self, other: &Self) -> bool {
44        match (self.use_id, other.use_id) {
45            (false, false) => self.location.eq(&other.location),
46            (true, true) => self.id.eq(&other.id) && self.is_link.eq(&other.is_link),
47            _ => false,
48        }
49    }
50}
51
52impl Eq for CacheKey {}
53
54impl Default for CacheKey {
55    fn default() -> Self {
56        CacheKey {
57            id: gix_hash::Kind::Sha1.null(),
58            use_id: false,
59            is_link: false,
60            location: BString::default(),
61        }
62    }
63}
64
65impl CacheKey {
66    fn set_location(&mut self, rela_path: &BStr) {
67        self.location.clear();
68        self.location.extend_from_slice(rela_path);
69    }
70}
71
72/// A resource ready to be diffed in one way or another.
73#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
74pub struct Resource<'a> {
75    /// If available, an index into the `drivers` field to access more diff-related information of the driver for items
76    /// at the given path, as previously determined by git-attributes.
77    ///
78    /// Note that drivers are queried even if there is no object available.
79    pub driver_index: Option<usize>,
80    /// The data itself, suitable for diffing, and if the object or worktree item is present at all.
81    pub data: resource::Data<'a>,
82    /// The kind of the resource we are looking at. Only possible values are `Blob`, `BlobExecutable` and `Link`.
83    pub mode: gix_object::tree::EntryKind,
84    /// The location of the resource, relative to the working tree.
85    pub rela_path: &'a BStr,
86    /// The id of the content as it would be stored in `git`, or `null` if the content doesn't exist anymore at
87    /// `rela_path` or if it was never computed. This can happen with content read from the worktree, which has to
88    /// go through a filter to be converted back to what `git` would store.
89    pub id: &'a gix_hash::oid,
90}
91
92///
93pub mod resource {
94    use crate::blob::{
95        pipeline,
96        platform::{CacheKey, CacheValue, Resource},
97    };
98
99    impl<'a> Resource<'a> {
100        pub(crate) fn new(key: &'a CacheKey, value: &'a CacheValue) -> Self {
101            Resource {
102                driver_index: value.conversion.driver_index,
103                data: value.conversion.data.map_or(Data::Missing, |data| match data {
104                    pipeline::Data::Buffer => Data::Buffer(&value.buffer),
105                    pipeline::Data::Binary { size } => Data::Binary { size },
106                }),
107                mode: value.mode,
108                rela_path: key.location.as_ref(),
109                id: &key.id,
110            }
111        }
112
113        /// Produce an iterator over lines, separated by LF or CRLF, suitable to create tokens using
114        /// [`imara_diff::intern::InternedInput`].
115        pub fn intern_source(&self) -> imara_diff::sources::ByteLines<'a, true> {
116            crate::blob::sources::byte_lines_with_terminator(self.data.as_slice().unwrap_or_default())
117        }
118    }
119
120    /// The data of a diffable resource, as it could be determined and computed previously.
121    #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
122    pub enum Data<'a> {
123        /// The object is missing, either because it didn't exist in the working tree or because its `id` was null.
124        Missing,
125        /// The textual data as processed to be in a diffable state.
126        Buffer(&'a [u8]),
127        /// The size that the binary blob had at the given revision, without having applied filters, as it's either
128        /// considered binary or above the big-file threshold.
129        ///
130        /// In this state, the binary file cannot be diffed.
131        Binary {
132            /// The size of the object prior to performing any filtering or as it was found on disk.
133            ///
134            /// Note that technically, the size isn't always representative of the same 'state' of the
135            /// content, as once it can be the size of the blob in git, and once it's the size of file
136            /// in the worktree.
137            size: u64,
138        },
139    }
140
141    impl<'a> Data<'a> {
142        /// Return ourselves as slice of bytes if this instance stores data.
143        pub fn as_slice(&self) -> Option<&'a [u8]> {
144            match self {
145                Data::Buffer(d) => Some(d),
146                Data::Binary { .. } | Data::Missing => None,
147            }
148        }
149    }
150}
151
152///
153pub mod set_resource {
154    use bstr::BString;
155
156    use crate::blob::{pipeline, ResourceKind};
157
158    /// The error returned by [Platform::set_resource](super::Platform::set_resource).
159    #[derive(Debug, thiserror::Error)]
160    #[allow(missing_docs)]
161    pub enum Error {
162        #[error("Can only diff blobs and links, not {mode:?}")]
163        InvalidMode { mode: gix_object::tree::EntryKind },
164        #[error("Failed to read {kind} worktree data from '{rela_path}'")]
165        Io {
166            rela_path: BString,
167            kind: ResourceKind,
168            source: std::io::Error,
169        },
170        #[error("Failed to obtain attributes for {kind} resource at '{rela_path}'")]
171        Attributes {
172            rela_path: BString,
173            kind: ResourceKind,
174            source: std::io::Error,
175        },
176        #[error(transparent)]
177        ConvertToDiffable(#[from] pipeline::convert_to_diffable::Error),
178    }
179}
180
181///
182pub mod prepare_diff {
183    use bstr::BStr;
184
185    use crate::blob::platform::Resource;
186
187    /// The kind of operation that should be performed based on the configuration of the resources involved in the diff.
188    #[derive(Debug, Copy, Clone, Eq, PartialEq)]
189    pub enum Operation<'a> {
190        /// The [internal diff algorithm](imara_diff::diff) should be called with the provided arguments.
191        /// This only happens if none of the resources are binary, and if there is no external diff program configured via git-attributes
192        /// *or* [Options::skip_internal_diff_if_external_is_configured](super::Options::skip_internal_diff_if_external_is_configured)
193        /// is `false`.
194        ///
195        /// Use [`Outcome::interned_input()`] to easily obtain an interner for use with [`imara_diff::diff()`], or maintain one yourself
196        /// for greater reuse.
197        InternalDiff {
198            /// The algorithm we determined should be used, which is one of (in order, first set one wins):
199            ///
200            /// * the driver's override
201            /// * the platforms own configuration (typically from git-config)
202            /// * the default algorithm
203            algorithm: imara_diff::Algorithm,
204        },
205        /// Run the external diff program according as configured in the `source`-resources driver.
206        /// This only happens if [Options::skip_internal_diff_if_external_is_configured](super::Options::skip_internal_diff_if_external_is_configured)
207        /// was `true`, preventing the usage of the internal diff implementation.
208        ExternalCommand {
209            /// The command as extracted from [Driver::command](super::super::Driver::command).
210            /// Use it in [`Platform::prepare_diff_command`](super::Platform::prepare_diff_command()) to easily prepare a compatible invocation.
211            command: &'a BStr,
212        },
213        /// One of the involved resources, [`old`](Outcome::old) or [`new`](Outcome::new), were binary and thus no diff
214        /// cannot be performed.
215        SourceOrDestinationIsBinary,
216    }
217
218    /// The outcome of a [`prepare_diff`](super::Platform::prepare_diff()) operation.
219    #[derive(Debug, Copy, Clone, Eq, PartialEq)]
220    pub struct Outcome<'a> {
221        /// The kind of diff that was actually performed. This may include skipping the internal diff as well.
222        pub operation: Operation<'a>,
223        /// The old or source of the diff operation.
224        pub old: Resource<'a>,
225        /// The new or destination of the diff operation.
226        pub new: Resource<'a>,
227    }
228
229    impl<'a> Outcome<'a> {
230        /// Produce an instance of an interner which `git` would use to perform diffs.
231        pub fn interned_input(&self) -> imara_diff::intern::InternedInput<&'a [u8]> {
232            crate::blob::intern::InternedInput::new(self.old.intern_source(), self.new.intern_source())
233        }
234    }
235
236    /// The error returned by [Platform::prepare_diff()](super::Platform::prepare_diff()).
237    #[derive(Debug, thiserror::Error)]
238    #[allow(missing_docs)]
239    pub enum Error {
240        #[error("Either the source or the destination of the diff operation were not set")]
241        SourceOrDestinationUnset,
242        #[error("Tried to diff resources that are both considered removed")]
243        SourceAndDestinationRemoved,
244    }
245}
246
247///
248pub mod prepare_diff_command {
249    use std::ops::{Deref, DerefMut};
250
251    use bstr::BString;
252
253    /// The error returned by [Platform::prepare_diff_command()](super::Platform::prepare_diff_command()).
254    #[derive(Debug, thiserror::Error)]
255    #[allow(missing_docs)]
256    pub enum Error {
257        #[error("Either the source or the destination of the diff operation were not set")]
258        SourceOrDestinationUnset,
259        #[error("Binary resources can't be diffed with an external command (as we don't have the data anymore)")]
260        SourceOrDestinationBinary,
261        #[error(
262            "Tempfile to store content of '{rela_path}' for passing to external diff command could not be created"
263        )]
264        CreateTempfile { rela_path: BString, source: std::io::Error },
265        #[error("Could not write content of '{rela_path}' to tempfile for passing to external diff command")]
266        WriteTempfile { rela_path: BString, source: std::io::Error },
267    }
268
269    /// The outcome of a [`prepare_diff_command`](super::Platform::prepare_diff_command()) operation.
270    ///
271    /// This type acts like [`std::process::Command`], ready to run, with `stdin`, `stdout` and `stderr` set to *inherit*
272    /// all handles as this is expected to be for visual inspection.
273    pub struct Command {
274        pub(crate) cmd: std::process::Command,
275        /// Possibly a tempfile to be removed after the run, or `None` if there is no old version.
276        pub(crate) old: Option<gix_tempfile::Handle<gix_tempfile::handle::Closed>>,
277        /// Possibly a tempfile to be removed after the run, or `None` if there is no new version.
278        pub(crate) new: Option<gix_tempfile::Handle<gix_tempfile::handle::Closed>>,
279    }
280
281    impl Deref for Command {
282        type Target = std::process::Command;
283
284        fn deref(&self) -> &Self::Target {
285            &self.cmd
286        }
287    }
288
289    impl DerefMut for Command {
290        fn deref_mut(&mut self) -> &mut Self::Target {
291            &mut self.cmd
292        }
293    }
294}
295
296/// Options for use in [Platform::new()].
297#[derive(Default, Copy, Clone)]
298pub struct Options {
299    /// The algorithm to use when diffing.
300    /// If unset, it uses the [default algorithm](Algorithm::default()).
301    pub algorithm: Option<Algorithm>,
302    /// If `true`, default `false`, then an external `diff` configured using gitattributes and drivers,
303    /// will cause the built-in diff [to be skipped](prepare_diff::Operation::ExternalCommand).
304    /// Otherwise, the internal diff is called despite the configured external diff, which is
305    /// typically what callers expect by default.
306    pub skip_internal_diff_if_external_is_configured: bool,
307}
308
309/// Lifecycle
310impl Platform {
311    /// Create a new instance with `options`, and a way to `filter` data from the object database to data that is diff-able.
312    /// `filter_mode` decides how to do that specifically.
313    /// Use `attr_stack` to access attributes pertaining worktree filters and diff settings.
314    pub fn new(
315        options: Options,
316        filter: Pipeline,
317        filter_mode: pipeline::Mode,
318        attr_stack: gix_worktree::Stack,
319    ) -> Self {
320        Platform {
321            old: None,
322            new: None,
323            diff_cache: Default::default(),
324            free_list: Vec::with_capacity(2),
325            options,
326            filter,
327            filter_mode,
328            attr_stack,
329        }
330    }
331}
332
333/// Conversions
334impl Platform {
335    /// Store enough information about a resource to eventually diff it, where…
336    ///
337    /// * `id` is the hash of the resource. If it [is null](gix_hash::ObjectId::is_null()), it should either
338    ///   be a resource in the worktree, or it's considered a non-existing, deleted object.
339    ///   If an `id` is known, as the hash of the object as (would) be stored in `git`, then it should be provided
340    ///   for completeness.
341    /// * `mode` is the kind of object (only blobs and links are allowed)
342    /// * `rela_path` is the relative path as seen from the (work)tree root.
343    /// * `kind` identifies the side of the diff this resource will be used for.
344    ///    A diff needs both `OldOrSource` *and* `NewOrDestination`.
345    /// * `objects` provides access to the object database in case the resource can't be read from a worktree.
346    ///
347    /// Note that it's assumed that either `id + mode (` or `rela_path` can serve as unique identifier for the resource,
348    /// depending on whether or not a [worktree root](pipeline::WorktreeRoots) is set for the resource of `kind`,
349    /// with resources with worktree roots using the `rela_path` as unique identifier.
350    ///
351    /// ### Important
352    ///
353    /// If an error occurs, the previous resource of `kind` will be cleared, preventing further diffs
354    /// unless another attempt succeeds.
355    pub fn set_resource(
356        &mut self,
357        id: gix_hash::ObjectId,
358        mode: gix_object::tree::EntryKind,
359        rela_path: &BStr,
360        kind: ResourceKind,
361        objects: &impl gix_object::FindObjectOrHeader, // TODO: make this `dyn` once https://github.com/rust-lang/rust/issues/65991 is stable, then also make tracker.rs `objects` dyn
362    ) -> Result<(), set_resource::Error> {
363        let res = self.set_resource_inner(id, mode, rela_path, kind, objects);
364        if res.is_err() {
365            *match kind {
366                ResourceKind::OldOrSource => &mut self.old,
367                ResourceKind::NewOrDestination => &mut self.new,
368            } = None;
369        }
370        res
371    }
372
373    /// Given `diff_command` and `context`, typically obtained from git-configuration, and the currently set diff-resources,
374    /// prepare the invocation and temporary files needed to launch it according to protocol.
375    /// `count` / `total` are used for progress indication passed as environment variables `GIT_DIFF_PATH_(COUNTER|TOTAL)`
376    /// respectively (0-based), so the first path has `count=0` and `total=1` (assuming there is only one path).
377    /// Returns `None` if at least one resource is unset, see [`set_resource()`](Self::set_resource()).
378    ///
379    /// Please note that this is an expensive operation this will always create up to two temporary files to hold the data
380    /// for the old and new resources.
381    ///
382    /// ### Deviation
383    ///
384    /// If one of the resources is binary, the operation reports an error as such resources don't make their data available
385    /// which is required for the external diff to run.
386    // TODO: fix this - the diff shouldn't fail if binary (or large) files are used, just copy them into tempfiles.
387    pub fn prepare_diff_command(
388        &self,
389        diff_command: BString,
390        context: gix_command::Context,
391        count: usize,
392        total: usize,
393    ) -> Result<prepare_diff_command::Command, prepare_diff_command::Error> {
394        fn add_resource(
395            cmd: &mut std::process::Command,
396            res: Resource<'_>,
397        ) -> Result<Option<gix_tempfile::Handle<gix_tempfile::handle::Closed>>, prepare_diff_command::Error> {
398            let tmpfile = match res.data {
399                resource::Data::Missing => {
400                    cmd.args(["/dev/null", ".", "."]);
401                    None
402                }
403                resource::Data::Buffer(buf) => {
404                    let mut tmp = gix_tempfile::new(
405                        std::env::temp_dir(),
406                        gix_tempfile::ContainingDirectory::Exists,
407                        gix_tempfile::AutoRemove::Tempfile,
408                    )
409                    .map_err(|err| prepare_diff_command::Error::CreateTempfile {
410                        rela_path: res.rela_path.to_owned(),
411                        source: err,
412                    })?;
413                    tmp.write_all(buf)
414                        .map_err(|err| prepare_diff_command::Error::WriteTempfile {
415                            rela_path: res.rela_path.to_owned(),
416                            source: err,
417                        })?;
418                    tmp.with_mut(|f| {
419                        cmd.arg(f.path());
420                    })
421                    .map_err(|err| prepare_diff_command::Error::WriteTempfile {
422                        rela_path: res.rela_path.to_owned(),
423                        source: err,
424                    })?;
425                    cmd.arg(res.id.to_string()).arg(res.mode.as_octal_str().to_string());
426                    let tmp = tmp.close().map_err(|err| prepare_diff_command::Error::WriteTempfile {
427                        rela_path: res.rela_path.to_owned(),
428                        source: err,
429                    })?;
430                    Some(tmp)
431                }
432                resource::Data::Binary { .. } => return Err(prepare_diff_command::Error::SourceOrDestinationBinary),
433            };
434            Ok(tmpfile)
435        }
436
437        let (old, new) = self
438            .resources()
439            .ok_or(prepare_diff_command::Error::SourceOrDestinationUnset)?;
440        let mut cmd: std::process::Command = gix_command::prepare(gix_path::from_bstring(diff_command))
441            .with_context(context)
442            .env("GIT_DIFF_PATH_COUNTER", (count + 1).to_string())
443            .env("GIT_DIFF_PATH_TOTAL", total.to_string())
444            .stdin(Stdio::inherit())
445            .stdout(Stdio::inherit())
446            .stderr(Stdio::inherit())
447            .into();
448
449        cmd.arg(gix_path::from_bstr(old.rela_path).into_owned());
450        let mut out = prepare_diff_command::Command {
451            cmd,
452            old: None,
453            new: None,
454        };
455
456        out.old = add_resource(&mut out.cmd, old)?;
457        out.new = add_resource(&mut out.cmd, new)?;
458
459        if old.rela_path != new.rela_path {
460            out.cmd.arg(gix_path::from_bstr(new.rela_path).into_owned());
461        }
462
463        Ok(out)
464    }
465
466    /// Returns the resource of the given kind if it was set.
467    pub fn resource(&self, kind: ResourceKind) -> Option<Resource<'_>> {
468        let key = match kind {
469            ResourceKind::OldOrSource => self.old.as_ref(),
470            ResourceKind::NewOrDestination => self.new.as_ref(),
471        }?;
472        Resource::new(key, self.diff_cache.get(key)?).into()
473    }
474
475    /// Obtain the two resources that were previously set as `(OldOrSource, NewOrDestination)`, if both are set and available.
476    ///
477    /// This is useful if one wishes to manually prepare the diff, maybe for invoking external programs, instead of relying on
478    /// [`Self::prepare_diff()`].
479    pub fn resources(&self) -> Option<(Resource<'_>, Resource<'_>)> {
480        let key = &self.old.as_ref()?;
481        let value = self.diff_cache.get(key)?;
482        let old = Resource::new(key, value);
483
484        let key = &self.new.as_ref()?;
485        let value = self.diff_cache.get(key)?;
486        let new = Resource::new(key, value);
487        Some((old, new))
488    }
489
490    /// Prepare a diff operation on the [previously set](Self::set_resource()) [old](ResourceKind::OldOrSource) and
491    /// [new](ResourceKind::NewOrDestination) resources.
492    ///
493    /// The returned outcome allows to easily perform diff operations, based on the [`prepare_diff::Outcome::operation`] field,
494    /// which hints at what should be done.
495    pub fn prepare_diff(&mut self) -> Result<prepare_diff::Outcome<'_>, prepare_diff::Error> {
496        let old_key = &self.old.as_ref().ok_or(prepare_diff::Error::SourceOrDestinationUnset)?;
497        let old = self
498            .diff_cache
499            .get(old_key)
500            .ok_or(prepare_diff::Error::SourceOrDestinationUnset)?;
501        let new_key = &self.new.as_ref().ok_or(prepare_diff::Error::SourceOrDestinationUnset)?;
502        let new = self
503            .diff_cache
504            .get(new_key)
505            .ok_or(prepare_diff::Error::SourceOrDestinationUnset)?;
506        let mut out = prepare_diff::Outcome {
507            operation: prepare_diff::Operation::SourceOrDestinationIsBinary,
508            old: Resource::new(old_key, old),
509            new: Resource::new(new_key, new),
510        };
511
512        match (old.conversion.data, new.conversion.data) {
513            (None, None) => return Err(prepare_diff::Error::SourceAndDestinationRemoved),
514            (Some(pipeline::Data::Binary { .. }), _) | (_, Some(pipeline::Data::Binary { .. })) => return Ok(out),
515            _either_missing_or_non_binary => {
516                if let Some(command) = old
517                    .conversion
518                    .driver_index
519                    .and_then(|idx| self.filter.drivers[idx].command.as_deref())
520                    .filter(|_| self.options.skip_internal_diff_if_external_is_configured)
521                {
522                    out.operation = prepare_diff::Operation::ExternalCommand {
523                        command: command.as_bstr(),
524                    };
525                    return Ok(out);
526                }
527            }
528        }
529
530        out.operation = prepare_diff::Operation::InternalDiff {
531            algorithm: old
532                .conversion
533                .driver_index
534                .and_then(|idx| self.filter.drivers[idx].algorithm)
535                .or(self.options.algorithm)
536                .unwrap_or_default(),
537        };
538        Ok(out)
539    }
540
541    /// Every call to [set_resource()](Self::set_resource()) will keep the diffable data in memory, and that will never be cleared.
542    ///
543    /// Use this method to clear the cache, releasing memory. Note that this will also lose all information about resources
544    /// which means diffs would fail unless the resources are set again.
545    ///
546    /// Note that this also has to be called if the same resource is going to be diffed in different states, i.e. using different
547    /// `id`s, but the same `rela_path`.
548    pub fn clear_resource_cache(&mut self) {
549        self.old = None;
550        self.new = None;
551        self.diff_cache.clear();
552        self.free_list.clear();
553    }
554
555    /// Every call to [set_resource()](Self::set_resource()) will keep the diffable data in memory, and that will never be cleared.
556    ///
557    /// Use this method to clear the cache, but keep the previously used buffers around for later re-use.
558    ///
559    /// If there are more buffers on the free-list than there are stored sources, we half that amount each time this method is called,
560    /// or keep as many resources as were previously stored, or 2 buffers, whatever is larger.
561    /// If there are fewer buffers in the free-list than are in the resource cache, we will keep as many as needed to match the
562    /// number of previously stored resources.
563    ///
564    /// Returns the number of available buffers.
565    pub fn clear_resource_cache_keep_allocation(&mut self) -> usize {
566        self.old = None;
567        self.new = None;
568
569        let diff_cache = std::mem::take(&mut self.diff_cache);
570        match self.free_list.len().cmp(&diff_cache.len()) {
571            Ordering::Less => {
572                let to_take = diff_cache.len() - self.free_list.len();
573                self.free_list
574                    .extend(diff_cache.into_values().map(|v| v.buffer).take(to_take));
575            }
576            Ordering::Equal => {}
577            Ordering::Greater => {
578                let new_len = (self.free_list.len() / 2).max(diff_cache.len()).max(2);
579                self.free_list.truncate(new_len);
580            }
581        }
582        self.free_list.len()
583    }
584}
585
586impl Platform {
587    fn set_resource_inner(
588        &mut self,
589        id: gix_hash::ObjectId,
590        mode: gix_object::tree::EntryKind,
591        rela_path: &BStr,
592        kind: ResourceKind,
593        objects: &impl gix_object::FindObjectOrHeader,
594    ) -> Result<(), set_resource::Error> {
595        if matches!(
596            mode,
597            gix_object::tree::EntryKind::Commit | gix_object::tree::EntryKind::Tree
598        ) {
599            return Err(set_resource::Error::InvalidMode { mode });
600        }
601        let storage = match kind {
602            ResourceKind::OldOrSource => &mut self.old,
603            ResourceKind::NewOrDestination => &mut self.new,
604        }
605        .get_or_insert_with(Default::default);
606
607        storage.id = id;
608        storage.set_location(rela_path);
609        storage.is_link = matches!(mode, gix_object::tree::EntryKind::Link);
610        storage.use_id = self.filter.roots.by_kind(kind).is_none();
611
612        if self.diff_cache.contains_key(storage) {
613            return Ok(());
614        }
615        let entry =
616            self.attr_stack
617                .at_entry(rela_path, None, objects)
618                .map_err(|err| set_resource::Error::Attributes {
619                    source: err,
620                    kind,
621                    rela_path: rela_path.to_owned(),
622                })?;
623        let mut buf = self.free_list.pop().unwrap_or_default();
624        let out = self.filter.convert_to_diffable(
625            &id,
626            mode,
627            rela_path,
628            kind,
629            &mut |_, out| {
630                let _ = entry.matching_attributes(out);
631            },
632            objects,
633            self.filter_mode,
634            &mut buf,
635        )?;
636        let key = storage.clone();
637        assert!(
638            self.diff_cache
639                .insert(
640                    key,
641                    CacheValue {
642                        conversion: out,
643                        mode,
644                        buffer: buf,
645                    },
646                )
647                .is_none(),
648            "The key impl makes clashes impossible with our usage"
649        );
650        Ok(())
651    }
652}