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}