Skip to main content

gix_diff/blob/
platform.rs

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