Skip to main content

gix_diff/blob/
platform.rs

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