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}