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