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