diff --git a/gix-merge/src/blob/mod.rs b/gix-merge/src/blob/mod.rs index 408a58cbf52..ab4094e9eaf 100644 --- a/gix-merge/src/blob/mod.rs +++ b/gix-merge/src/blob/mod.rs @@ -125,12 +125,6 @@ pub struct Pipeline { pub filter: gix_filter::Pipeline, /// Options affecting the way we read files. pub options: pipeline::Options, - /// All available merge drivers. - /// - /// They are referenced in git-attributes by name, and we hand out indices into this array. - drivers: Vec, - /// Pre-configured attributes to obtain additional merge-related information. - attrs: gix_filter::attributes::search::Outcome, /// A buffer to produce disk-accessible paths from worktree roots. path: PathBuf, } @@ -152,7 +146,14 @@ pub struct Platform { pub filter: Pipeline, /// A way to access `.gitattributes` pub attr_stack: gix_worktree::Stack, - + /// Further configuration that affects the merge. + pub options: platform::Options, + /// All available merge drivers. + /// + /// They are referenced in git-attributes by name, and we hand out indices into this array. + drivers: Vec, + /// Pre-configured attributes to obtain additional merge-related information. + attrs: gix_filter::attributes::search::Outcome, /// The way we convert resources into mergeable states. filter_mode: pipeline::Mode, } diff --git a/gix-merge/src/blob/pipeline.rs b/gix-merge/src/blob/pipeline.rs index 90adb615051..776d908e203 100644 --- a/gix-merge/src/blob/pipeline.rs +++ b/gix-merge/src/blob/pipeline.rs @@ -1,6 +1,5 @@ -use super::{BuiltinDriver, Pipeline, ResourceKind}; -use bstr::{BStr, ByteSlice}; -use gix_filter::attributes; +use super::{Pipeline, ResourceKind}; +use bstr::BStr; use gix_filter::driver::apply::{Delay, MaybeDelayed}; use gix_filter::pipeline::convert::{ToGitOutcome, ToWorktreeOutcome}; use gix_object::tree::EntryKind; @@ -8,7 +7,7 @@ use std::io::Read; use std::path::{Path, PathBuf}; /// Options for use in a [`Pipeline`]. -#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)] +#[derive(Default, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)] pub struct Options { /// The amount of bytes that an object has to reach before being treated as binary. /// These objects will not be queried, nor will their data be processed in any way. @@ -20,12 +19,6 @@ pub struct Options { /// However, if they are to be retrieved from the worktree, the worktree size is what matters, /// even though that also might be a `git-lfs` file which is small in Git. pub large_file_threshold_bytes: u64, - /// Capabilities of the file system which affect how we read worktree files. - pub fs: gix_fs::Capabilities, - /// Define which driver to use if the `merge` attribute for a resource is unspecified. - /// - /// This is the value of the `merge.default` git configuration. - pub default_driver: Option, } /// The specific way to convert a resource. @@ -78,50 +71,30 @@ impl Pipeline { /// Create a new instance of a pipeline which produces blobs suitable for merging. /// /// `roots` allow to read worktree files directly, and `worktree_filter` is used - /// to transform object database data directly. `drivers` further configure individual paths. - /// `options` are used to further configure the way we act.. - pub fn new( - roots: WorktreeRoots, - worktree_filter: gix_filter::Pipeline, - mut drivers: Vec, - options: Options, - ) -> Self { - drivers.sort_by(|a, b| a.name.cmp(&b.name)); + /// to transform object database data directly. + /// `options` are used to further configure the way we act. + pub fn new(roots: WorktreeRoots, worktree_filter: gix_filter::Pipeline, options: Options) -> Self { Pipeline { roots, filter: worktree_filter, - drivers, options, - attrs: { - let mut out = gix_filter::attributes::search::Outcome::default(); - out.initialize_with_selection(&Default::default(), Some("merge")); - out - }, path: Default::default(), } } } /// Access -impl Pipeline { - /// Return all drivers that this instance was initialized with. - /// - /// They are sorted by [`name`](super::Driver::name) to support binary searches. - pub fn drivers(&self) -> &[super::Driver] { - &self.drivers - } -} +impl Pipeline {} -/// Data as part of an [Outcome]. +/// Data as returned by [`Pipeline::convert_to_mergeable()`]. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] pub enum Data { /// The data to use for merging was written into the buffer that was passed during the call to [`Pipeline::convert_to_mergeable()`]. Buffer, - /// The size that the binary blob had at the given revision, without having applied filters, as it's either - /// considered binary or above the big-file threshold. + /// The file or blob is above the big-file threshold and cannot be processed. /// - /// In this state, the binary file cannot be merged. - Binary { + /// In this state, the file cannot be merged. + TooLarge { /// The size of the object prior to performing any filtering or as it was found on disk. /// /// Note that technically, the size isn't always representative of the same 'state' of the @@ -131,44 +104,6 @@ pub enum Data { }, } -/// The selection of the driver to use by a resource obtained with [`Pipeline::convert_to_mergeable()`]. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] -pub enum DriverChoice { - /// Use the given built-in driver to perform the merge. - BuiltIn(BuiltinDriver), - /// Use the user-provided driver program using the index into [the pipelines driver array](Pipeline::drivers(). - Index(usize), -} - -impl Default for DriverChoice { - fn default() -> Self { - DriverChoice::BuiltIn(Default::default()) - } -} - -/// The outcome returned by [Pipeline::convert_to_mergeable()]. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] -pub struct Outcome { - /// If available, an index into the `drivers` field to access more diff-related information of the driver for items - /// at the given path, as previously determined by git-attributes. - /// - /// * `merge` is set - /// - Use the [`BuiltinDriver::Text`] - /// * `-merge` is unset - /// - Use the [`BuiltinDriver::Binary`] - /// * `!merge` is unspecified - /// - Use [`Options::default_driver`] or [`BuiltinDriver::Text`]. - /// * `merge=name` - /// - Search for a user-configured or built-in driver called `name`. - /// - If not found, silently default to [`BuiltinDriver::Text`] - /// - /// Note that drivers are queried even if there is no object available. - pub driver: DriverChoice, - /// The data itself, suitable for diffing, and if the object or worktree item is present at all. - /// Otherwise, it's `None`. - pub data: Option, -} - /// pub mod convert_to_mergeable { use std::collections::TryReserveError; @@ -202,15 +137,18 @@ pub mod convert_to_mergeable { /// Conversion impl Pipeline { /// Convert the object at `id`, `mode`, `rela_path` and `kind`, providing access to `attributes` and `objects`. - /// The resulting merge-able data is written into `out`, if it's not too large or considered binary. - /// The returned [`Outcome`] contains information on how to use `out`, or if it's filled at all. + /// The resulting merge-able data is written into `out`, if it's not too large. + /// The returned [`Data`] contains information on how to use `out`, which will be cleared if it is `None`, indicating + /// that no object was found at the location *on disk* - it's always an error to provide an object ID that doesn't exist + /// in the object database. /// - /// `attributes` must be returning the attributes at `rela_path`, and `objects` must be usable if `kind` is - /// a resource in the object database, i.e. if no worktree root is available. It's notable that if a worktree root - /// is present for `kind`, then a `rela_path` is used to access it on disk. + /// `attributes` must be returning the attributes at `rela_path` and is used for obtaining worktree filter settings, + /// and `objects` must be usable if `kind` is a resource in the object database, + /// i.e. if no worktree root is available. It's notable that if a worktree root is present for `kind`, + /// then a `rela_path` is used to access it on disk. /// /// If `id` [is null](gix_hash::ObjectId::is_null()) or the file in question doesn't exist in the worktree in case - /// [a root](WorktreeRoots) is present, then `out` will be left cleared and [Outcome::data] will be `None`. + /// [a root](WorktreeRoots) is present, then `out` will be left cleared and the output data will be `None`. /// This is useful to simplify the calling code as empty buffers signal that nothing is there. /// /// Note that `mode` is trusted, and we will not re-validate that the entry in the worktree actually is of that mode. @@ -228,7 +166,7 @@ impl Pipeline { objects: &dyn gix_object::FindObjectOrHeader, convert: Mode, out: &mut Vec, - ) -> Result { + ) -> Result, convert_to_mergeable::Error> { if !matches!(mode, EntryKind::Blob | EntryKind::BlobExecutable) { return Err(convert_to_mergeable::Error::InvalidEntryKind { rela_path: rela_path.to_owned(), @@ -237,31 +175,6 @@ impl Pipeline { } out.clear(); - attributes(rela_path, &mut self.attrs); - let attr = self.attrs.iter_selected().next().expect("pre-initialized with 'diff'"); - let driver = match attr.assignment.state { - attributes::StateRef::Set => DriverChoice::BuiltIn(BuiltinDriver::Text), - attributes::StateRef::Unset => DriverChoice::BuiltIn(BuiltinDriver::Binary), - attributes::StateRef::Value(name) => { - let name = name.as_bstr(); - self.drivers - .binary_search_by(|d| d.name.as_bstr().cmp(name)) - .ok() - .map(DriverChoice::Index) - .or_else(|| { - name.to_str() - .ok() - .and_then(BuiltinDriver::by_name) - .map(DriverChoice::BuiltIn) - }) - .unwrap_or_default() - } - attributes::StateRef::Unspecified => self - .options - .default_driver - .map(DriverChoice::BuiltIn) - .unwrap_or_default(), - }; match self.roots.by_kind(kind) { Some(root) => { self.path.clear(); @@ -279,7 +192,7 @@ impl Pipeline { .transpose()?; let data = match size_in_bytes { Some(None) => None, // missing as identified by the size check - Some(Some(size)) if size > self.options.large_file_threshold_bytes => Some(Data::Binary { size }), + Some(Some(size)) if size > self.options.large_file_threshold_bytes => Some(Data::TooLarge { size }), _ => { let file = none_if_missing(std::fs::File::open(&self.path)).map_err(|err| { convert_to_mergeable::Error::OpenOrRead { @@ -295,7 +208,13 @@ impl Pipeline { file, gix_path::from_bstr(rela_path).as_ref(), attributes, - &mut |buf| objects.try_find(id, buf).map(|obj| obj.map(|_| ())), + &mut |buf| { + if convert == Mode::Renormalize { + Ok(None) + } else { + objects.try_find(id, buf).map(|obj| obj.map(|_| ())) + } + }, )?; match res { @@ -324,19 +243,13 @@ impl Pipeline { } } - Some(if is_binary_buf(out) { - let size = out.len() as u64; - out.clear(); - Data::Binary { size } - } else { - Data::Buffer - }) + Some(Data::Buffer) } else { None } } }; - Ok(Outcome { driver, data }) + Ok(data) } None => { let data = if id.is_null() { @@ -349,7 +262,7 @@ impl Pipeline { let is_binary = self.options.large_file_threshold_bytes > 0 && header.size > self.options.large_file_threshold_bytes; let data = if is_binary { - Data::Binary { size: header.size } + Data::TooLarge { size: header.size } } else { objects .try_find(id, out) @@ -357,66 +270,62 @@ impl Pipeline { .ok_or_else(|| gix_object::find::existing_object::Error::NotFound { oid: id.to_owned() })?; if convert == Mode::Renormalize { - let res = self - .filter - .convert_to_worktree(out, rela_path, attributes, Delay::Forbid)?; + { + let res = self + .filter + .convert_to_worktree(out, rela_path, attributes, Delay::Forbid)?; + + match res { + ToWorktreeOutcome::Unchanged(_) => {} + ToWorktreeOutcome::Buffer(src) => { + out.clear(); + out.try_reserve(src.len())?; + out.extend_from_slice(src); + } + ToWorktreeOutcome::Process(MaybeDelayed::Immediate(mut stream)) => { + std::io::copy(&mut stream, out).map_err(|err| { + convert_to_mergeable::Error::StreamCopy { + rela_path: rela_path.to_owned(), + source: err, + } + })?; + } + ToWorktreeOutcome::Process(MaybeDelayed::Delayed(_)) => { + unreachable!("we prohibit this") + } + }; + } + + let res = self.filter.convert_to_git( + &**out, + &gix_path::from_bstr(rela_path), + attributes, + &mut |_buf| Ok(None), + )?; match res { - ToWorktreeOutcome::Unchanged(_) => {} - ToWorktreeOutcome::Buffer(src) => { - out.clear(); - out.try_reserve(src.len())?; - out.extend_from_slice(src); - } - ToWorktreeOutcome::Process(MaybeDelayed::Immediate(mut stream)) => { - std::io::copy(&mut stream, out).map_err(|err| { - convert_to_mergeable::Error::StreamCopy { + ToGitOutcome::Unchanged(_) => {} + ToGitOutcome::Process(mut stream) => { + stream + .read_to_end(out) + .map_err(|err| convert_to_mergeable::Error::OpenOrRead { rela_path: rela_path.to_owned(), source: err, - } - })?; + })?; } - ToWorktreeOutcome::Process(MaybeDelayed::Delayed(_)) => { - unreachable!("we prohibit this") + ToGitOutcome::Buffer(buf) => { + out.clear(); + out.try_reserve(buf.len())?; + out.extend_from_slice(buf); } - }; - } - - let res = self.filter.convert_to_git( - &**out, - &gix_path::from_bstr(rela_path), - attributes, - &mut |buf| objects.try_find(id, buf).map(|obj| obj.map(|_| ())), - )?; - - match res { - ToGitOutcome::Unchanged(_) => {} - ToGitOutcome::Process(mut stream) => { - stream - .read_to_end(out) - .map_err(|err| convert_to_mergeable::Error::OpenOrRead { - rela_path: rela_path.to_owned(), - source: err, - })?; - } - ToGitOutcome::Buffer(buf) => { - out.clear(); - out.try_reserve(buf.len())?; - out.extend_from_slice(buf); } } - if is_binary_buf(out) { - let size = out.len() as u64; - out.clear(); - Data::Binary { size } - } else { - Data::Buffer - } + Data::Buffer }; Some(data) }; - Ok(Outcome { driver, data }) + Ok(data) } } } @@ -429,8 +338,3 @@ fn none_if_missing(res: std::io::Result) -> std::io::Result> { Err(err) => Err(err), } } - -fn is_binary_buf(buf: &[u8]) -> bool { - let buf = &buf[..buf.len().min(8000)]; - buf.contains(&0) -} diff --git a/gix-merge/src/blob/platform.rs b/gix-merge/src/blob/platform.rs index 6b6175ee408..f749e03c3ec 100644 --- a/gix-merge/src/blob/platform.rs +++ b/gix-merge/src/blob/platform.rs @@ -1,7 +1,6 @@ -use bstr::{BStr, BString}; - -use crate::blob::pipeline::DriverChoice; -use crate::blob::{pipeline, Pipeline, Platform, ResourceKind}; +use crate::blob::{pipeline, BuiltinDriver, Pipeline, Platform, ResourceKind}; +use bstr::{BStr, BString, ByteSlice}; +use gix_filter::attributes; /// A stored value representing a resource that participates in a merge. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] @@ -10,8 +9,8 @@ pub(super) struct Resource { id: gix_hash::ObjectId, /// The repository-relative path where the resource lives in the tree. rela_path: BString, - /// The outcome of converting a resource into a diffable format using [Pipeline::convert_to_mergeable()]. - conversion: pipeline::Outcome, + /// The outcome of converting a resource into a mergable format using [Pipeline::convert_to_mergeable()]. + data: Option, /// The kind of the resource we are looking at. Only possible values are `Blob` and `BlobExecutable`. mode: gix_object::tree::EntryKind, /// A possibly empty buffer, depending on `conversion.data` which may indicate the data is considered binary @@ -26,14 +25,51 @@ pub struct ResourceRef<'a> { pub data: resource::Data<'a>, /// The location of the resource, relative to the working tree. pub rela_path: &'a BStr, - /// Which driver to use according to the resource's configuration. - pub driver_choice: DriverChoice, /// The id of the content as it would be stored in `git`, or `null` if the content doesn't exist anymore at /// `rela_path` or if it was never computed. This can happen with content read from the worktree, which /// after its 'to-git' conversion never had its hash computed. pub id: &'a gix_hash::oid, } +/// Options for use in a [`Platform`]. +#[derive(Default, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)] +pub struct Options { + /// Define which driver to use by name if the `merge` attribute for a resource is unspecified. + /// + /// This is the value of the `merge.default` git configuration. + pub default_driver: Option, +} + +/// The selection of the driver to use by a resource obtained with [`Pipeline::convert_to_mergeable()`]. +/// +/// If available, an index into the `drivers` field to access more diff-related information of the driver for items +/// at the given path, as previously determined by git-attributes. +/// +/// * `merge` is set +/// - Use the [`BuiltinDriver::Text`] +/// * `-merge` is unset +/// - Use the [`BuiltinDriver::Binary`] +/// * `!merge` is unspecified +/// - Use [`Options::default_driver`] or [`BuiltinDriver::Text`]. +/// * `merge=name` +/// - Search for a user-configured or built-in driver called `name`. +/// - If not found, silently default to [`BuiltinDriver::Text`] +/// +/// Note that drivers are queried even if there is no object available. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] +pub enum DriverChoice { + /// Use the given built-in driver to perform the merge. + BuiltIn(BuiltinDriver), + /// Use the user-provided driver program using the index into [the pipelines driver array](Pipeline::drivers(). + Index(usize), +} + +impl Default for DriverChoice { + fn default() -> Self { + DriverChoice::BuiltIn(Default::default()) + } +} + /// pub mod resource { use crate::blob::{ @@ -44,11 +80,10 @@ pub mod resource { impl<'a> ResourceRef<'a> { pub(super) fn new(cache: &'a Resource) -> Self { ResourceRef { - data: cache.conversion.data.map_or(Data::Missing, |data| match data { + data: cache.data.map_or(Data::Missing, |data| match data { pipeline::Data::Buffer => Data::Buffer(&cache.buffer), - pipeline::Data::Binary { size } => Data::Binary { size }, + pipeline::Data::TooLarge { size } => Data::Binary { size }, }), - driver_choice: cache.conversion.driver, rela_path: cache.rela_path.as_ref(), id: &cache.id, } @@ -118,7 +153,7 @@ pub mod set_resource { /// pub mod merge { - use crate::blob::pipeline::DriverChoice; + use crate::blob::platform::DriverChoice; use crate::blob::platform::ResourceRef; use crate::blob::{builtin_driver, BuiltinDriver, Driver, Resolution}; use bstr::BString; @@ -135,6 +170,9 @@ pub mod merge { pub ancestor: ResourceRef<'parent>, /// The other or their side of the merge operation. pub other: ResourceRef<'parent>, + /// Which driver to use according to the resource's configuration, + /// using the path of `current` to read git-attributes. + pub driver_choice: DriverChoice, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -267,9 +305,9 @@ pub mod merge { /// Return the configured driver program for use with [`Self::prepare_external_driver()`], or `Err` /// with the built-in driver to use instead. pub fn configured_driver(&self) -> Result<&'parent Driver, BuiltinDriver> { - match self.current.driver_choice { + match self.driver_choice { DriverChoice::BuiltIn(builtin) => Err(builtin), - DriverChoice::Index(idx) => self.parent.filter.drivers.get(idx).ok_or(BuiltinDriver::default()), + DriverChoice::Index(idx) => self.parent.drivers.get(idx).ok_or(BuiltinDriver::default()), } } } @@ -299,14 +337,21 @@ pub mod merge { /// pub mod prepare_merge { + use crate::blob::ResourceKind; + use bstr::BString; + /// The error returned by [Platform::prepare_merge()](super::Platform::prepare_merge_state()). #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("The 'current', 'ancestor' or 'other' resource for the merge operation were not set")] UnsetResource, - #[error("Tried to merge 'current' and 'other' where at least one of them is removed")] - CurrentOrOtherRemoved, + #[error("Failed to obtain attributes for {kind:?} resource at '{rela_path}'")] + Attributes { + rela_path: BString, + kind: ResourceKind, + source: std::io::Error, + }, } } @@ -315,18 +360,44 @@ impl Platform { /// Create a new instance with a way to `filter` data from the object database and turn it into something that is merge-able. /// `filter_mode` decides how to do that specifically. /// Use `attr_stack` to access attributes pertaining worktree filters and merge settings. - pub fn new(filter: Pipeline, filter_mode: pipeline::Mode, attr_stack: gix_worktree::Stack) -> Self { + /// `drivers` are the list of available merge drivers that individual paths can refer to by means of git attributes. + /// `options` further configure the operation. + pub fn new( + filter: Pipeline, + filter_mode: pipeline::Mode, + attr_stack: gix_worktree::Stack, + mut drivers: Vec, + options: Options, + ) -> Self { + drivers.sort_by(|a, b| a.name.cmp(&b.name)); Platform { + drivers, current: None, ancestor: None, other: None, filter, filter_mode, attr_stack, + attrs: { + let mut out = attributes::search::Outcome::default(); + out.initialize_with_selection(&Default::default(), Some("merge")); + out + }, + options, } } } +/// Access +impl Platform { + /// Return all drivers that this instance was initialized with. + /// + /// They are sorted by [`name`](super::Driver::name) to support binary searches. + pub fn drivers(&self) -> &[super::Driver] { + &self.drivers + } +} + /// Preparation impl Platform { /// Store enough information about a resource to eventually use it in a merge, where… @@ -351,33 +422,62 @@ impl Platform { self.set_resource_inner(id, mode, rela_path, kind, objects) } - /// Returns the resource of the given kind if it was set. - pub fn resource(&self, kind: ResourceKind) -> Option> { - let cache = match kind { - ResourceKind::CurrentOrOurs => self.current.as_ref(), - ResourceKind::CommonAncestorOrBase => self.ancestor.as_ref(), - ResourceKind::OtherOrTheirs => self.other.as_ref(), - }?; - ResourceRef::new(cache).into() - } - /// Prepare all state needed for performing a merge, using all [previously set](Self::set_resource()) resources. - pub fn prepare_merge_state(&self) -> Result, prepare_merge::Error> { + /// Note that no additional validation is performed here to facilitate inspection. + pub fn prepare_merge_state( + &mut self, + objects: &impl gix_object::Find, + ) -> Result, prepare_merge::Error> { let current = self.current.as_ref().ok_or(prepare_merge::Error::UnsetResource)?; let ancestor = self.ancestor.as_ref().ok_or(prepare_merge::Error::UnsetResource)?; let other = self.other.as_ref().ok_or(prepare_merge::Error::UnsetResource)?; + let entry = self + .attr_stack + .at_entry(current.rela_path.as_bstr(), None, objects) + .map_err(|err| prepare_merge::Error::Attributes { + source: err, + kind: ResourceKind::CurrentOrOurs, + rela_path: current.rela_path.clone(), + })?; + entry.matching_attributes(&mut self.attrs); + let attr = self.attrs.iter_selected().next().expect("pre-initialized with 'diff'"); + let driver = match attr.assignment.state { + attributes::StateRef::Set => DriverChoice::BuiltIn(BuiltinDriver::Text), + attributes::StateRef::Unset => DriverChoice::BuiltIn(BuiltinDriver::Binary), + attributes::StateRef::Value(_) | attributes::StateRef::Unspecified => { + let name = match attr.assignment.state { + attributes::StateRef::Value(name) => Some(name.as_bstr()), + attributes::StateRef::Unspecified => { + self.options.default_driver.as_ref().map(|name| name.as_bstr()) + } + _ => unreachable!("only value and unspecified are possible here"), + }; + name.and_then(|name| { + self.drivers + .binary_search_by(|d| d.name.as_bstr().cmp(name)) + .ok() + .map(DriverChoice::Index) + .or_else(|| { + name.to_str() + .ok() + .and_then(BuiltinDriver::by_name) + .map(DriverChoice::BuiltIn) + }) + }) + .unwrap_or_default() + } + }; + let out = merge::State { parent: self, + driver_choice: driver, current: ResourceRef::new(current), ancestor: ResourceRef::new(ancestor), other: ResourceRef::new(other), }; - match (current.conversion.data, other.conversion.data) { - (None, None) => Err(prepare_merge::Error::CurrentOrOtherRemoved), - (_, _) => Ok(out), - } + Ok(out) } } @@ -430,7 +530,7 @@ impl Platform { *storage = Some(Resource { id, rela_path: rela_path.to_owned(), - conversion: out, + data: out, mode, buffer: buf_storage, }); @@ -438,7 +538,7 @@ impl Platform { Some(storage) => { storage.id = id; storage.rela_path = rela_path.to_owned(); - storage.conversion = out; + storage.data = out; storage.mode = mode; } }; diff --git a/gix-merge/tests/fixtures/generated-archives/make_blob_repo.tar b/gix-merge/tests/fixtures/generated-archives/make_blob_repo.tar new file mode 100644 index 00000000000..ee7859571ca Binary files /dev/null and b/gix-merge/tests/fixtures/generated-archives/make_blob_repo.tar differ diff --git a/gix-merge/tests/fixtures/make_blob_repo.sh b/gix-merge/tests/fixtures/make_blob_repo.sh new file mode 100644 index 00000000000..13af2c5c6bb --- /dev/null +++ b/gix-merge/tests/fixtures/make_blob_repo.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q + +echo a > a +echo b > b +echo union > union +echo e > e-no-attr +echo unset > unset +echo unspecified > unspecified + +cat <.gitattributes +a merge=a +b merge=b +union merge=union +missing merge=missing +unset -merge +unspecified !merge +EOF + +git add . && git commit -m "init" diff --git a/gix-merge/tests/merge/blob/builtin_driver.rs b/gix-merge/tests/merge/blob/builtin_driver.rs index 073280e0afd..d42ec7aa823 100644 --- a/gix-merge/tests/merge/blob/builtin_driver.rs +++ b/gix-merge/tests/merge/blob/builtin_driver.rs @@ -123,8 +123,8 @@ mod text { "Number of expected diverging cases must match the actual one - probably the implementation improved" ); assert_eq!( - (num_diverging as f32 / num_cases as f32) * 100.0, - 12.053572, + ((num_diverging as f32 / num_cases as f32) * 100.0) as usize, + 12, "Just to show the percentage of skipped tests - this should get better" ); Ok(()) diff --git a/gix-merge/tests/merge/blob/mod.rs b/gix-merge/tests/merge/blob/mod.rs index f781f63e485..57d9205d79a 100644 --- a/gix-merge/tests/merge/blob/mod.rs +++ b/gix-merge/tests/merge/blob/mod.rs @@ -1 +1,52 @@ mod builtin_driver; +mod pipeline; +mod platform; + +mod util { + use std::collections::HashMap; + + use gix_hash::oid; + use gix_object::{bstr::BString, find::Error}; + + #[derive(Default)] + pub struct ObjectDb { + data_by_id: HashMap, + } + + impl gix_object::FindHeader for ObjectDb { + fn try_header(&self, id: &oid) -> Result, Error> { + match self.data_by_id.get(&id.to_owned()) { + Some(data) => Ok(Some(gix_object::Header { + kind: gix_object::Kind::Blob, + size: data.len() as u64, + })), + None => Ok(None), + } + } + } + + impl gix_object::Find for ObjectDb { + fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec) -> Result>, Error> { + match self.data_by_id.get(&id.to_owned()) { + Some(data) => { + buffer.clear(); + buffer.extend_from_slice(data); + Ok(Some(gix_object::Data { + kind: gix_object::Kind::Blob, + data: buffer.as_slice(), + })) + } + None => Ok(None), + } + } + } + + impl ObjectDb { + /// Insert `data` and return its hash. That can be used to find it again. + pub fn insert(&mut self, data: &str) -> gix_hash::ObjectId { + let id = gix_object::compute_hash(gix_hash::Kind::Sha1, gix_object::Kind::Blob, data.as_bytes()); + self.data_by_id.insert(id, data.into()); + id + } + } +} diff --git a/gix-merge/tests/merge/blob/pipeline.rs b/gix-merge/tests/merge/blob/pipeline.rs new file mode 100644 index 00000000000..080a9d601f6 --- /dev/null +++ b/gix-merge/tests/merge/blob/pipeline.rs @@ -0,0 +1,433 @@ +use crate::blob::util::ObjectDb; +use bstr::ByteSlice; +use gix_filter::eol; +use gix_filter::eol::AutoCrlf; +use gix_merge::blob::pipeline::{self, Mode, WorktreeRoots}; +use gix_merge::blob::{Pipeline, ResourceKind}; +use gix_object::tree::EntryKind; + +const ALL_MODES: [pipeline::Mode; 2] = [pipeline::Mode::ToGit, pipeline::Mode::Renormalize]; + +#[test] +fn without_transformation() -> crate::Result { + for mode in ALL_MODES { + let tmp = gix_testtools::tempfile::TempDir::new()?; + let mut filter = Pipeline::new( + WorktreeRoots { + common_ancestor_root: Some(tmp.path().to_owned()), + ..Default::default() + }, + gix_filter::Pipeline::default(), + default_options(), + ); + + let does_not_matter = gix_hash::Kind::Sha1.null(); + let mut buf = Vec::new(); + let a_name = "a"; + let a_content = "a-content"; + std::fs::write(tmp.path().join(a_name), a_content.as_bytes())?; + let out = filter.convert_to_mergeable( + &does_not_matter, + EntryKind::Blob, + a_name.into(), + ResourceKind::CommonAncestorOrBase, + &mut |_, _| {}, + &gix_object::find::Never, + mode, + &mut buf, + )?; + assert_eq!(out, Some(pipeline::Data::Buffer)); + assert_eq!(buf.as_bstr(), a_content, "there is no transformations configured"); + + let link_name = "link"; + gix_fs::symlink::create(a_name.as_ref(), &tmp.path().join(link_name))?; + let err = filter + .convert_to_mergeable( + &does_not_matter, + EntryKind::Link, + link_name.into(), + ResourceKind::CommonAncestorOrBase, + &mut |_, _| {}, + &gix_object::find::Never, + mode, + &mut buf, + ) + .unwrap_err(); + + assert!( + matches!(err, pipeline::convert_to_mergeable::Error::InvalidEntryKind {rela_path,actual} + if rela_path == link_name && actual == EntryKind::Link) + ); + assert_eq!( + buf.len(), + 9, + "input buffers are cleared only if we think they are going to be used" + ); + drop(tmp); + + let mut db = ObjectDb::default(); + let b_content = "b-content"; + let id = db.insert(b_content); + + let out = filter.convert_to_mergeable( + &id, + EntryKind::Blob, + a_name.into(), + ResourceKind::CurrentOrOurs, + &mut |_, _| {}, + &db, + mode, + &mut buf, + )?; + + assert_eq!(out, Some(pipeline::Data::Buffer)); + assert_eq!( + buf.as_bstr(), + b_content, + "there is no transformations configured, it fetched the data from the ODB" + ); + + let out = filter.convert_to_mergeable( + &does_not_matter, + EntryKind::Blob, + a_name.into(), + ResourceKind::OtherOrTheirs, + &mut |_, _| {}, + &gix_object::find::Never, + mode, + &mut buf, + )?; + assert_eq!(out, None, "the lack of object in the database isn't a problem"); + + let out = filter.convert_to_mergeable( + &does_not_matter, + EntryKind::Blob, + "does not exist on disk".into(), + ResourceKind::CommonAncestorOrBase, + &mut |_, _| {}, + &gix_object::find::Never, + mode, + &mut buf, + )?; + assert_eq!(out, None, "the lack of file on disk is fine as well"); + } + + Ok(()) +} + +#[test] +fn binary_below_large_file_threshold() -> crate::Result { + let tmp = gix_testtools::tempfile::TempDir::new()?; + let mut filter = Pipeline::new( + WorktreeRoots { + current_root: Some(tmp.path().to_owned()), + ..Default::default() + }, + gix_filter::Pipeline::default(), + pipeline::Options { + large_file_threshold_bytes: 5, + }, + ); + + let does_not_matter = gix_hash::Kind::Sha1.null(); + let mut buf = Vec::new(); + let a_name = "a"; + let binary_content = "a\0b"; + std::fs::write(tmp.path().join(a_name), binary_content.as_bytes())?; + let out = filter.convert_to_mergeable( + &does_not_matter, + EntryKind::BlobExecutable, + a_name.into(), + ResourceKind::CurrentOrOurs, + &mut |_, _| {}, + &gix_object::find::Never, + pipeline::Mode::ToGit, + &mut buf, + )?; + assert_eq!(out, Some(pipeline::Data::Buffer), "binary data can still be merged"); + assert_eq!(buf.as_bstr(), binary_content); + + let mut db = ObjectDb::default(); + let id = db.insert(binary_content); + let out = filter.convert_to_mergeable( + &id, + EntryKind::Blob, + a_name.into(), + ResourceKind::OtherOrTheirs, + &mut |_, _| {}, + &db, + pipeline::Mode::ToGit, + &mut buf, + )?; + assert_eq!(out, Some(pipeline::Data::Buffer)); + assert_eq!(buf.as_bstr(), binary_content); + + Ok(()) +} + +#[test] +fn above_large_file_threshold() -> crate::Result { + let tmp = gix_testtools::tempfile::TempDir::new()?; + let mut filter = gix_merge::blob::Pipeline::new( + WorktreeRoots { + current_root: Some(tmp.path().to_owned()), + ..Default::default() + }, + gix_filter::Pipeline::default(), + pipeline::Options { + large_file_threshold_bytes: 4, + }, + ); + + let does_not_matter = gix_hash::Kind::Sha1.null(); + let mut buf = Vec::new(); + let a_name = "a"; + let large_content = "hello"; + std::fs::write(tmp.path().join(a_name), large_content.as_bytes())?; + let out = filter.convert_to_mergeable( + &does_not_matter, + EntryKind::BlobExecutable, + a_name.into(), + ResourceKind::CurrentOrOurs, + &mut |_, _| {}, + &gix_object::find::Never, + pipeline::Mode::ToGit, + &mut buf, + )?; + assert_eq!( + out, + Some(pipeline::Data::TooLarge { size: 5 }), + "it indicates that the file is too large" + ); + assert_eq!(buf.len(), 0, "it should avoid querying that data in the first place"); + + drop(tmp); + let mut db = ObjectDb::default(); + let id = db.insert(large_content); + + let out = filter.convert_to_mergeable( + &id, + EntryKind::Blob, + a_name.into(), + ResourceKind::CommonAncestorOrBase, + &mut |_, _| {}, + &db, + pipeline::Mode::ToGit, + &mut buf, + )?; + + assert_eq!(out, Some(pipeline::Data::TooLarge { size: 5 })); + assert_eq!( + buf.len(), + 0, + "it won't have queried the blob, first it checks the header" + ); + + Ok(()) +} + +#[test] +fn non_existing() -> crate::Result { + let tmp = gix_testtools::tempfile::TempDir::new()?; + let mut filter = Pipeline::new( + WorktreeRoots { + common_ancestor_root: Some(tmp.path().to_owned()), + ..Default::default() + }, + gix_filter::Pipeline::default(), + default_options(), + ); + + let null = gix_hash::Kind::Sha1.null(); + let mut buf = vec![1]; + let a_name = "a"; + assert!( + !tmp.path().join(a_name).exists(), + "precondition: worktree file doesn't exist" + ); + let out = filter.convert_to_mergeable( + &null, + EntryKind::Blob, + a_name.into(), + ResourceKind::CommonAncestorOrBase, + &mut |_, _| {}, + &gix_object::find::Never, + pipeline::Mode::ToGit, + &mut buf, + )?; + assert_eq!( + out, None, + "it's OK for a resource to not exist on disk - they'd then count as deleted" + ); + assert_eq!(buf.len(), 0, "always cleared"); + + drop(tmp); + + buf.push(1); + let out = filter.convert_to_mergeable( + &null, + EntryKind::Blob, + a_name.into(), + ResourceKind::OtherOrTheirs, + &mut |_, _| {}, + &gix_object::find::Never, + pipeline::Mode::ToGit, + &mut buf, + )?; + + assert_eq!( + out, None, + "the root path isn't configured and the object database returns nothing" + ); + assert_eq!(buf.len(), 0, "it's always cleared before any potential use"); + + let some_id = gix_hash::ObjectId::from_hex(b"45c160c35c17ad264b96431cceb9793160396e99")?; + let err = filter + .convert_to_mergeable( + &some_id, + EntryKind::Blob, + a_name.into(), + ResourceKind::OtherOrTheirs, + &mut |_, _| {}, + &gix_object::find::Never, + pipeline::Mode::ToGit, + &mut buf, + ) + .unwrap_err(); + assert!( + matches!( + err, + gix_merge::blob::pipeline::convert_to_mergeable::Error::FindObject( + gix_object::find::existing_object::Error::NotFound { .. } + ), + ), + "missing object database ids are always an error (even though missing objects on disk are allowed)" + ); + Ok(()) +} + +#[test] +fn worktree_filter() -> crate::Result { + let tmp = gix_testtools::tempfile::TempDir::new()?; + let filter = gix_filter::Pipeline::new( + Default::default(), + gix_filter::pipeline::Options { + eol_config: eol::Configuration { + auto_crlf: AutoCrlf::Enabled, + ..Default::default() + }, + ..Default::default() + }, + ); + let mut filter = gix_merge::blob::Pipeline::new( + WorktreeRoots { + common_ancestor_root: Some(tmp.path().to_owned()), + ..Default::default() + }, + filter, + default_options(), + ); + + let mut db = ObjectDb::default(); + let a_name = "a"; + let mut buf = Vec::new(); + let a_content = "a-content\r\n"; + std::fs::write(tmp.path().join(a_name), a_content.as_bytes())?; + for mode in ALL_MODES { + let does_not_matter = gix_hash::Kind::Sha1.null(); + let out = filter.convert_to_mergeable( + &does_not_matter, + EntryKind::Blob, + a_name.into(), + ResourceKind::CommonAncestorOrBase, + &mut |_, _| {}, + &gix_object::find::Never, + mode, + &mut buf, + )?; + assert_eq!(out, Some(pipeline::Data::Buffer)); + assert_eq!( + buf.as_bstr(), + "a-content\n", + "worktree files need to be converted back to what's stored in Git" + ); + + let id = db.insert(a_content); + let out = filter.convert_to_mergeable( + &id, + EntryKind::Blob, + a_name.into(), + ResourceKind::CommonAncestorOrBase, + &mut |_, _| {}, + &db, + mode, + &mut buf, + )?; + assert_eq!(out, Some(pipeline::Data::Buffer)); + match mode { + Mode::ToGit => { + assert_eq!( + buf.as_bstr(), + "a-content\r\n", + "if an object with CRLF already exists, we don't 'renormalize' it, it's a feature" + ); + } + Mode::Renormalize => { + assert_eq!( + buf.as_bstr(), + "a-content\n", + "we can also do it if the file exists both on disk and is known to the ODB" + ); + } + } + } + + drop(tmp); + + let b_content = "b-content\n"; + let id = db.insert(b_content); + + let out = filter.convert_to_mergeable( + &id, + EntryKind::Blob, + a_name.into(), + ResourceKind::CurrentOrOurs, + &mut |_, _| {}, + &db, + pipeline::Mode::ToGit, + &mut buf, + )?; + + assert_eq!(out, Some(pipeline::Data::Buffer)); + assert_eq!(buf.as_bstr(), b_content, "no work is done for what's already in Git"); + + let mut db = ObjectDb::default(); + let b_content = "b-content\r\n"; + let id = db.insert(b_content); + let out = filter.convert_to_mergeable( + &id, + EntryKind::Blob, + a_name.into(), + ResourceKind::OtherOrTheirs, + &mut |_, _| {}, + &db, + pipeline::Mode::Renormalize, + &mut buf, + )?; + + assert_eq!(out, Some(pipeline::Data::Buffer)); + assert_eq!( + buf.as_bstr(), + "b-content\n", + "we see what would have been stored if the file was checked out and checked in again.\ + It explicitly ignores what's in Git already (or it wouldn't do anyting)" + ); + + Ok(()) +} + +fn default_options() -> pipeline::Options { + pipeline::Options { + large_file_threshold_bytes: 0, + } +} diff --git a/gix-merge/tests/merge/blob/platform.rs b/gix-merge/tests/merge/blob/platform.rs new file mode 100644 index 00000000000..6865e097f4d --- /dev/null +++ b/gix-merge/tests/merge/blob/platform.rs @@ -0,0 +1,584 @@ +use gix_merge::blob::{pipeline, ResourceKind}; +use gix_object::tree::EntryKind; +use gix_worktree::stack::state::attributes; + +use gix_merge::blob::Platform; + +#[test] +fn ancestor_and_current_and_other_do_not_exist() -> crate::Result { + let mut platform = new_platform(None, pipeline::Mode::default()); + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "also-missing".into(), + ResourceKind::CommonAncestorOrBase, + &gix_object::find::Never, + )?; + + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "can't-be-found-in-odb".into(), + ResourceKind::CurrentOrOurs, + &gix_object::find::Never, + )?; + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::BlobExecutable, + "can't-be-found-in-odb".into(), + ResourceKind::OtherOrTheirs, + &gix_object::find::Never, + )?; + + let state = platform + .prepare_merge_state(&gix_object::find::Never) + .expect("no validation is done here, let the caller inspect"); + assert_eq!(state.ancestor.data.as_slice(), None); + assert_eq!(state.current.data.as_slice(), None); + assert_eq!(state.other.data.as_slice(), None); + Ok(()) +} + +mod set_resource { + use crate::blob::platform::new_platform; + use gix_merge::blob::{pipeline, ResourceKind}; + use gix_object::tree::EntryKind; + + #[test] + fn invalid_resource_types() { + let mut platform = new_platform(None, pipeline::Mode::ToGit); + for (mode, name) in [(EntryKind::Commit, "Commit"), (EntryKind::Tree, "Tree")] { + assert_eq!( + platform + .set_resource( + gix_hash::Kind::Sha1.null(), + mode, + "a".into(), + ResourceKind::OtherOrTheirs, + &gix_object::find::Never, + ) + .unwrap_err() + .to_string(), + format!("Can only diff blobs, not {name}") + ); + } + } +} + +fn new_platform( + drivers: impl IntoIterator, + filter_mode: gix_merge::blob::pipeline::Mode, +) -> Platform { + let root = gix_testtools::scripted_fixture_read_only("make_blob_repo.sh").expect("valid fixture"); + let attributes = gix_worktree::Stack::new( + &root, + gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::new( + Default::default(), + None, + attributes::Source::WorktreeThenIdMapping, + Default::default(), + )), + gix_worktree::glob::pattern::Case::Sensitive, + Vec::new(), + Vec::new(), + ); + let filter = gix_merge::blob::Pipeline::new( + gix_merge::blob::pipeline::WorktreeRoots { + common_ancestor_root: Some(root.clone()), + ..Default::default() + }, + gix_filter::Pipeline::default(), + Default::default(), + ); + Platform::new( + filter, + filter_mode, + attributes, + drivers.into_iter().collect(), + Default::default(), + ) +} + +// +// #[test] +// fn with_driver() -> crate::Result { +// let root = gix_testtools::scripted_fixture_read_only("make_blob_repo.sh")?; +// let print_all = "echo $@ %O %A %B %L %P %S %X %Y"; +// let print_script_args = "echo $@"; +// let mut attributes = gix_worktree::Stack::new( +// &root, +// gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::new( +// Default::default(), +// None, +// attributes::Source::WorktreeThenIdMapping, +// Default::default(), +// )), +// gix_worktree::glob::pattern::Case::Sensitive, +// Vec::new(), +// Vec::new(), +// ); +// let mut filter = gix_merge::blob::Pipeline::new( +// WorktreeRoots { +// common_ancestor_root: Some(root.clone()), +// ..Default::default() +// }, +// gix_filter::Pipeline::default(), +// vec![ +// gix_merge::blob::Driver { +// name: "a".into(), +// command: print_all.into(), +// ..Default::default() +// }, +// gix_merge::blob::Driver { +// name: "b".into(), +// command: print_script_args.into(), +// ..Default::default() +// }, +// gix_merge::blob::Driver { +// name: "union".into(), +// ..Default::default() +// }, +// gix_merge::blob::Driver { +// name: "missing".into(), +// ..Default::default() +// }, +// ], +// pipeline::Options { +// default_driver: Some("binary".into()), +// ..crate::blob::pipeline::default_options() +// }, +// ); +// +// let mut buf = Vec::new(); +// let does_not_matter = gix_hash::Kind::Sha1.null(); +// let path = "unspecified"; +// let platform = attributes.at_entry(path, None, &gix_object::find::Never)?; +// let out = filter.convert_to_mergeable( +// &does_not_matter, +// EntryKind::Blob, +// path.into(), +// ResourceKind::CommonAncestorOrBase, +// &mut |_, out| { +// let _ = platform.matching_attributes(out); +// }, +// &gix_object::find::Never, +// pipeline::Mode::ToGit, +// &mut buf, +// )?; +// assert_eq!( +// out.driver, +// DriverChoice::BuiltIn(BuiltinDriver::Binary), +// "fall through to what's set in options" +// ); +// assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// assert_eq!(buf.as_bstr(), "unspecified\n"); +// +// let path = "union"; +// let platform = attributes.at_entry(path, None, &gix_object::find::Never)?; +// let out = filter.convert_to_mergeable( +// &does_not_matter, +// EntryKind::Blob, +// path.into(), +// ResourceKind::CommonAncestorOrBase, +// &mut |_, out| { +// let _ = platform.matching_attributes(out); +// }, +// &gix_object::find::Never, +// pipeline::Mode::ToGit, +// &mut buf, +// )?; +// let driver_idx = 3; +// assert_eq!( +// out.driver, +// DriverChoice::Index(driver_idx), +// "it finds explicit drivers first before it searches built-in ones" +// ); +// assert_eq!( +// filter.drivers()[driver_idx].name, +// "union", +// "it has re-sorted the drivers internally, which is why it's read-only" +// ); +// assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// assert_eq!(buf.as_bstr(), "union\n"); +// // +// // let mut db = ObjectDb::default(); +// // let null = gix_hash::Kind::Sha1.null(); +// // let mut buf = Vec::new(); +// // let platform = attributes.at_entry("a", None, &gix_object::find::Never)?; +// // let worktree_modes = [ +// // pipeline::Mode::ToWorktreeAndBinaryToText, +// // pipeline::Mode::ToGitUnlessBinaryToTextIsPresent, +// // ]; +// // let all_modes = [ +// // pipeline::Mode::ToGit, +// // pipeline::Mode::ToWorktreeAndBinaryToText, +// // pipeline::Mode::ToGitUnlessBinaryToTextIsPresent, +// // ]; +// // for mode in worktree_modes { +// // let out = filter.convert_to_diffable( +// // &null, +// // EntryKind::Blob, +// // "a".into(), +// // ResourceKind::OldOrSource, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &gix_object::find::Never, +// // mode, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, Some(0)); +// // assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// // assert_eq!(buf.as_bstr(), "to-text\na\n", "filter was applied"); +// // } +// // +// // let out = filter.convert_to_diffable( +// // &null, +// // EntryKind::Blob, +// // "a".into(), +// // ResourceKind::OldOrSource, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &gix_object::find::Never, +// // pipeline::Mode::ToGit, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, Some(0)); +// // assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// // assert_eq!(buf.as_bstr(), "a\n", "unconditionally use git according to mode"); +// // +// // let id = db.insert("a\n"); +// // for mode in worktree_modes { +// // let out = filter.convert_to_diffable( +// // &id, +// // EntryKind::Blob, +// // "a".into(), +// // ResourceKind::NewOrDestination, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &db, +// // mode, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, Some(0)); +// // assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// // assert_eq!(buf.as_bstr(), "to-text\na\n", "filter was applied"); +// // } +// // +// // let out = filter.convert_to_diffable( +// // &id, +// // EntryKind::Blob, +// // "a".into(), +// // ResourceKind::NewOrDestination, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &db, +// // pipeline::Mode::ToGit, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, Some(0)); +// // assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// // assert_eq!( +// // buf.as_bstr(), +// // "a\n", +// // "no filter was applied in this mode, also when using the ODB" +// // ); +// // +// // let platform = attributes.at_entry("missing", None, &gix_object::find::Never)?; +// // for mode in all_modes { +// // buf.push(1); +// // let out = filter.convert_to_diffable( +// // &null, +// // EntryKind::Link, +// // "missing".into(), /* does not actually exist */ +// // ResourceKind::OldOrSource, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &gix_object::find::Never, +// // mode, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, Some(4), "despite missing, we get driver information"); +// // assert_eq!(out.data, None); +// // assert_eq!(buf.len(), 0, "always cleared"); +// // +// // buf.push(1); +// // let out = filter.convert_to_diffable( +// // &null, +// // EntryKind::Link, +// // "missing".into(), /* does not actually exist */ +// // ResourceKind::NewOrDestination, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &gix_object::find::Never, +// // mode, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, Some(4), "despite missing, we get driver information"); +// // assert_eq!(out.data, None); +// // assert_eq!(buf.len(), 0, "always cleared"); +// // +// // buf.push(1); +// // let id = db.insert("link-target"); +// // let out = filter.convert_to_diffable( +// // &id, +// // EntryKind::Link, +// // "missing".into(), +// // ResourceKind::NewOrDestination, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &db, +// // mode, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, Some(4), "despite missing, we get driver information"); +// // assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// // assert_eq!( +// // buf.as_bstr(), +// // "link-target", +// // "no matter what, links always look the same." +// // ); +// // } +// +// // let platform = attributes.at_entry("b", None, &gix_object::find::Never)?; +// // for mode in all_modes { +// // buf.push(1); +// // let out = filter.convert_to_diffable( +// // &null, +// // EntryKind::Blob, +// // "b".into(), +// // ResourceKind::OldOrSource, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &gix_object::find::Never, +// // mode, +// // &mut buf, +// // )?; +// // +// // assert_eq!(out.driver_index, Some(1)); +// // assert_eq!( +// // out.data, +// // Some(pipeline::Data::Binary { size: 2 }), +// // "binary value comes from driver, and it's always respected with worktree source" +// // ); +// // assert_eq!(buf.len(), 0, "it's always cleared before any potential use"); +// // } +// // +// // let id = db.insert("b\n"); +// // for mode in all_modes { +// // buf.push(1); +// // let out = filter.convert_to_diffable( +// // &id, +// // EntryKind::Blob, +// // "b".into(), +// // ResourceKind::NewOrDestination, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &db, +// // mode, +// // &mut buf, +// // )?; +// // +// // assert_eq!(out.driver_index, Some(1)); +// // assert_eq!( +// // out.data, +// // Some(pipeline::Data::Binary { size: 2 }), +// // "binary value comes from driver, and it's always respected with DB source" +// // ); +// // assert_eq!(buf.len(), 0, "it's always cleared before any potential use"); +// // } +// // +// // let platform = attributes.at_entry("c", None, &gix_object::find::Never)?; +// // for mode in worktree_modes { +// // let out = filter.convert_to_diffable( +// // &null, +// // EntryKind::Blob, +// // "c".into(), +// // ResourceKind::OldOrSource, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &gix_object::find::Never, +// // mode, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, Some(2)); +// // assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// // assert_eq!( +// // buf.as_bstr(), +// // "to-text\nc\n", +// // "filter was applied, it overrides binary=true" +// // ); +// // } +// // +// // let id = db.insert("c\n"); +// // for mode in worktree_modes { +// // let out = filter.convert_to_diffable( +// // &id, +// // EntryKind::Blob, +// // "c".into(), +// // ResourceKind::NewOrDestination, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &db, +// // mode, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, Some(2)); +// // assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// // assert_eq!( +// // buf.as_bstr(), +// // "to-text\nc\n", +// // "filter was applied, it overrides binary=true" +// // ); +// // } +// // +// // let platform = attributes.at_entry("unset", None, &gix_object::find::Never)?; +// // for mode in all_modes { +// // let out = filter.convert_to_diffable( +// // &null, +// // EntryKind::Blob, +// // "unset".into(), +// // ResourceKind::OldOrSource, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &gix_object::find::Never, +// // mode, +// // &mut buf, +// // )?; +// // assert_eq!( +// // out.driver_index, None, +// // "no driver is associated, as `diff` is explicitly unset" +// // ); +// // assert_eq!( +// // out.data, +// // Some(pipeline::Data::Binary { size: 6 }), +// // "unset counts as binary" +// // ); +// // assert_eq!(buf.len(), 0); +// // } +// // +// // let id = db.insert("unset\n"); +// // for mode in all_modes { +// // let out = filter.convert_to_diffable( +// // &id, +// // EntryKind::Blob, +// // "unset".into(), +// // ResourceKind::NewOrDestination, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &db, +// // mode, +// // &mut buf, +// // )?; +// // assert_eq!( +// // out.driver_index, None, +// // "no driver is associated, as `diff` is explicitly unset" +// // ); +// // assert_eq!( +// // out.data, +// // Some(pipeline::Data::Binary { size: 6 }), +// // "unset counts as binary" +// // ); +// // assert_eq!(buf.len(), 0); +// // } +// // +// // let platform = attributes.at_entry("d", None, &gix_object::find::Never)?; +// // let id = db.insert("d-in-db"); +// // for mode in worktree_modes { +// // let out = filter.convert_to_diffable( +// // &null, +// // EntryKind::Blob, +// // "d".into(), +// // ResourceKind::OldOrSource, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &gix_object::find::Never, +// // mode, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, Some(3)); +// // assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// // assert_eq!( +// // buf.as_bstr(), +// // "to-text\nd\n", +// // "the worktree + text conversion was triggered for worktree source" +// // ); +// // +// // let out = filter.convert_to_diffable( +// // &id, +// // EntryKind::Blob, +// // "d".into(), +// // ResourceKind::NewOrDestination, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &db, +// // mode, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, Some(3)); +// // assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// // assert_eq!( +// // buf.as_bstr(), +// // "to-text\nd-in-db", +// // "the worktree + text conversion was triggered for db source" +// // ); +// // } +// // +// // let platform = attributes.at_entry("e-no-attr", None, &gix_object::find::Never)?; +// // let out = filter.convert_to_diffable( +// // &null, +// // EntryKind::Blob, +// // "e-no-attr".into(), +// // ResourceKind::OldOrSource, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &gix_object::find::Never, +// // pipeline::Mode::ToGitUnlessBinaryToTextIsPresent, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, None); +// // assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// // assert_eq!( +// // buf.as_bstr(), +// // "e\n", +// // "no text filter, so git conversion was applied for worktree source" +// // ); +// // +// // let id = db.insert("e-in-db"); +// // let out = filter.convert_to_diffable( +// // &id, +// // EntryKind::Blob, +// // "e-no-attr".into(), +// // ResourceKind::NewOrDestination, +// // &mut |_, out| { +// // let _ = platform.matching_attributes(out); +// // }, +// // &db, +// // pipeline::Mode::ToGitUnlessBinaryToTextIsPresent, +// // &mut buf, +// // )?; +// // assert_eq!(out.driver_index, None); +// // assert_eq!(out.data, Some(pipeline::Data::Buffer)); +// // assert_eq!( +// // buf.as_bstr(), +// // "e-in-db", +// // "no text filter, so git conversion was applied for ODB source" +// // ); +// +// Ok(()) +// }