diff --git a/Cargo.lock b/Cargo.lock index 5912a30127d..13e98214f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2052,6 +2052,7 @@ dependencies = [ "gix-hash 0.14.2", "gix-object 0.44.0", "gix-path 0.10.11", + "gix-quote 0.4.12", "gix-tempfile 14.0.2", "gix-testtools", "gix-trace 0.1.10", diff --git a/gix-merge/Cargo.toml b/gix-merge/Cargo.toml index 6d8da010147..93a5ae5b664 100644 --- a/gix-merge/Cargo.toml +++ b/gix-merge/Cargo.toml @@ -17,7 +17,7 @@ doctest = false [features] default = ["blob"] ## Enable diffing of blobs using imara-diff, which also allows for a generic rewrite tracking implementation. -blob = ["dep:imara-diff", "dep:gix-filter", "dep:gix-worktree", "dep:gix-path", "dep:gix-fs", "dep:gix-command", "dep:gix-tempfile", "dep:gix-trace"] +blob = ["dep:imara-diff", "dep:gix-filter", "dep:gix-worktree", "dep:gix-path", "dep:gix-fs", "dep:gix-command", "dep:gix-tempfile", "dep:gix-trace", "dep:gix-quote"] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = ["dep:serde", "gix-hash/serde", "gix-object/serde"] @@ -31,6 +31,7 @@ gix-path = { version = "^0.10.11", path = "../gix-path", optional = true } gix-fs = { version = "^0.11.3", path = "../gix-fs", optional = true } gix-tempfile = { version = "^14.0.0", path = "../gix-tempfile", optional = true } gix-trace = { version = "^0.1.10", path = "../gix-trace", optional = true } +gix-quote = { version = "^0.4.12", path = "../gix-quote", optional = true } thiserror = "1.0.63" imara-diff = { version = "0.1.7", optional = true } diff --git a/gix-merge/src/blob/builtin_driver/text/function.rs b/gix-merge/src/blob/builtin_driver/text/function.rs index a69b9a1a58d..bb800ce47e2 100644 --- a/gix-merge/src/blob/builtin_driver/text/function.rs +++ b/gix-merge/src/blob/builtin_driver/text/function.rs @@ -3,13 +3,12 @@ use crate::blob::builtin_driver::text::utils::{ hunks_differ_in_diff3, take_intersecting, tokens, write_ancestor, write_conflict_marker, write_hunks, zealously_contract_hunks, CollectHunks, Hunk, Side, }; -use crate::blob::builtin_driver::text::{ConflictStyle, Options, ResolveWith}; +use crate::blob::builtin_driver::text::{Conflict, ConflictStyle, Labels, Options}; use crate::blob::Resolution; -use bstr::BStr; /// Merge `current` and `other` with `ancestor` as base according to `opts`. /// -/// Use `current_label`, `other_label` and `ancestor_label` to annotate conflict sections. +/// Use `labels` to annotate conflict sections. /// /// `input` is for reusing memory for lists of tokens, but note that it grows indefinitely /// while tokens for `current`, `ancestor` and `other` are added. @@ -23,12 +22,14 @@ use bstr::BStr; pub fn merge<'a>( out: &mut Vec, input: &mut imara_diff::intern::InternedInput<&'a [u8]>, + Labels { + ancestor: ancestor_label, + current: current_label, + other: other_label, + }: Labels<'_>, current: &'a [u8], - current_label: Option<&BStr>, ancestor: &'a [u8], - ancestor_label: Option<&BStr>, other: &'a [u8], - other_label: Option<&BStr>, opts: Options, ) -> Resolution { out.clear(); @@ -77,9 +78,9 @@ pub fn merge<'a>( .expect("at least one entry"), &mut filled_hunks, ); - match opts.on_conflict { - None => { - let (hunks_front_and_back, num_hunks_front) = match opts.conflict_style { + match opts.conflict { + Conflict::Keep { style, marker_size } => { + let (hunks_front_and_back, num_hunks_front) = match style { ConflictStyle::Merge | ConflictStyle::ZealousDiff3 => { zealously_contract_hunks(&mut filled_hunks, &mut intersecting, input, ¤t_tokens) } @@ -130,28 +131,22 @@ pub fn merge<'a>( ) .or_else(|| detect_line_ending(our_hunks, input, ¤t_tokens)) .unwrap_or(b"\n".into()); - match opts.conflict_style { + match style { ConflictStyle::Merge => { if contains_lines(our_hunks) || contains_lines(their_hunks) { resolution = Resolution::Conflict; - write_conflict_marker(out, b'<', current_label, opts.marker_size, nl); + write_conflict_marker(out, b'<', current_label, marker_size, nl); write_hunks(our_hunks, input, ¤t_tokens, out); - write_conflict_marker(out, b'=', None, opts.marker_size, nl); + write_conflict_marker(out, b'=', None, marker_size, nl); write_hunks(their_hunks, input, ¤t_tokens, out); - write_conflict_marker(out, b'>', other_label, opts.marker_size, nl); + write_conflict_marker(out, b'>', other_label, marker_size, nl); } } ConflictStyle::Diff3 | ConflictStyle::ZealousDiff3 => { if contains_lines(our_hunks) || contains_lines(their_hunks) { - if hunks_differ_in_diff3( - opts.conflict_style, - our_hunks, - their_hunks, - input, - ¤t_tokens, - ) { + if hunks_differ_in_diff3(style, our_hunks, their_hunks, input, ¤t_tokens) { resolution = Resolution::Conflict; - write_conflict_marker(out, b'<', current_label, opts.marker_size, nl); + write_conflict_marker(out, b'<', current_label, marker_size, nl); write_hunks(our_hunks, input, ¤t_tokens, out); let ancestor_hunk = Hunk { before: first_hunk.before.start..last_hunk.before.end, @@ -161,11 +156,11 @@ pub fn merge<'a>( let ancestor_hunk = std::slice::from_ref(&ancestor_hunk); let ancestor_nl = detect_line_ending_or_nl(ancestor_hunk, input, ¤t_tokens); - write_conflict_marker(out, b'|', ancestor_label, opts.marker_size, ancestor_nl); + write_conflict_marker(out, b'|', ancestor_label, marker_size, ancestor_nl); write_hunks(ancestor_hunk, input, ¤t_tokens, out); - write_conflict_marker(out, b'=', None, opts.marker_size, nl); + write_conflict_marker(out, b'=', None, marker_size, nl); write_hunks(their_hunks, input, ¤t_tokens, out); - write_conflict_marker(out, b'>', other_label, opts.marker_size, nl); + write_conflict_marker(out, b'>', other_label, marker_size, nl); } else { write_hunks(our_hunks, input, ¤t_tokens, out); } @@ -176,64 +171,60 @@ pub fn merge<'a>( write_hunks(back_hunks, input, ¤t_tokens, out); ancestor_integrated_until = last_hunk.before.end; } - Some(resolve) => { - match resolve { - ResolveWith::Ours | ResolveWith::Theirs => { - let (our_hunks, their_hunks) = match filled_hunks_side { - Side::Current => (&filled_hunks, &intersecting), - Side::Other => (&intersecting, &filled_hunks), - Side::Ancestor => { - unreachable!("initial hunks are never ancestors") - } - }; - let hunks_to_write = if resolve == ResolveWith::Ours { - our_hunks - } else { - their_hunks - }; - if let Some(first_hunk) = hunks_to_write.first() { - write_ancestor(input, ancestor_integrated_until, first_hunk.before.start as usize, out); - } - write_hunks(hunks_to_write, input, ¤t_tokens, out); - if let Some(last_hunk) = hunks_to_write.last() { - ancestor_integrated_until = last_hunk.before.end; - } + Conflict::ResolveWithOurs | Conflict::ResolveWithTheirs => { + let (our_hunks, their_hunks) = match filled_hunks_side { + Side::Current => (&filled_hunks, &intersecting), + Side::Other => (&intersecting, &filled_hunks), + Side::Ancestor => { + unreachable!("initial hunks are never ancestors") } - ResolveWith::Union => { - let (hunks_front_and_back, num_hunks_front) = - zealously_contract_hunks(&mut filled_hunks, &mut intersecting, input, ¤t_tokens); + }; + let hunks_to_write = if opts.conflict == Conflict::ResolveWithOurs { + our_hunks + } else { + their_hunks + }; + if let Some(first_hunk) = hunks_to_write.first() { + write_ancestor(input, ancestor_integrated_until, first_hunk.before.start as usize, out); + } + write_hunks(hunks_to_write, input, ¤t_tokens, out); + if let Some(last_hunk) = hunks_to_write.last() { + ancestor_integrated_until = last_hunk.before.end; + } + } + Conflict::ResolveWithUnion => { + let (hunks_front_and_back, num_hunks_front) = + zealously_contract_hunks(&mut filled_hunks, &mut intersecting, input, ¤t_tokens); - let (our_hunks, their_hunks) = match filled_hunks_side { - Side::Current => (&filled_hunks, &intersecting), - Side::Other => (&intersecting, &filled_hunks), - Side::Ancestor => { - unreachable!("initial hunks are never ancestors") - } - }; - let (front_hunks, back_hunks) = hunks_front_and_back.split_at(num_hunks_front); - let first_hunk = front_hunks - .first() - .or(our_hunks.first()) - .expect("at least one hunk to write"); - write_ancestor(input, ancestor_integrated_until, first_hunk.before.start as usize, out); - write_hunks(front_hunks, input, ¤t_tokens, out); - assure_ends_with_nl(out, detect_line_ending_or_nl(front_hunks, input, ¤t_tokens)); - write_hunks(our_hunks, input, ¤t_tokens, out); - assure_ends_with_nl(out, detect_line_ending_or_nl(our_hunks, input, ¤t_tokens)); - write_hunks(their_hunks, input, ¤t_tokens, out); - if !back_hunks.is_empty() { - assure_ends_with_nl(out, detect_line_ending_or_nl(their_hunks, input, ¤t_tokens)); - } - write_hunks(back_hunks, input, ¤t_tokens, out); - let last_hunk = back_hunks - .last() - .or(their_hunks.last()) - .or(our_hunks.last()) - .or(front_hunks.last()) - .expect("at least one hunk"); - ancestor_integrated_until = last_hunk.before.end; + let (our_hunks, their_hunks) = match filled_hunks_side { + Side::Current => (&filled_hunks, &intersecting), + Side::Other => (&intersecting, &filled_hunks), + Side::Ancestor => { + unreachable!("initial hunks are never ancestors") } }; + let (front_hunks, back_hunks) = hunks_front_and_back.split_at(num_hunks_front); + let first_hunk = front_hunks + .first() + .or(our_hunks.first()) + .expect("at least one hunk to write"); + write_ancestor(input, ancestor_integrated_until, first_hunk.before.start as usize, out); + write_hunks(front_hunks, input, ¤t_tokens, out); + assure_ends_with_nl(out, detect_line_ending_or_nl(front_hunks, input, ¤t_tokens)); + write_hunks(our_hunks, input, ¤t_tokens, out); + assure_ends_with_nl(out, detect_line_ending_or_nl(our_hunks, input, ¤t_tokens)); + write_hunks(their_hunks, input, ¤t_tokens, out); + if !back_hunks.is_empty() { + assure_ends_with_nl(out, detect_line_ending_or_nl(their_hunks, input, ¤t_tokens)); + } + write_hunks(back_hunks, input, ¤t_tokens, out); + let last_hunk = back_hunks + .last() + .or(their_hunks.last()) + .or(our_hunks.last()) + .or(front_hunks.last()) + .expect("at least one hunk"); + ancestor_integrated_until = last_hunk.before.end; } } } else { diff --git a/gix-merge/src/blob/builtin_driver/text/mod.rs b/gix-merge/src/blob/builtin_driver/text/mod.rs index 73d3f123cc6..1c4287dc7be 100644 --- a/gix-merge/src/blob/builtin_driver/text/mod.rs +++ b/gix-merge/src/blob/builtin_driver/text/mod.rs @@ -1,3 +1,5 @@ +use bstr::BStr; + /// The way the built-in [text driver](crate::blob::BuiltinDriver::Text) will express /// merge conflicts in the resulting file. #[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -48,27 +50,31 @@ pub enum ConflictStyle { ZealousDiff3, } +/// The set of labels to annotate conflict markers with. +/// +/// That way it becomes clearer where the content of conflicts are originating from. +#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] +pub struct Labels<'a> { + pub ancestor: Option<&'a BStr>, + pub current: Option<&'a BStr>, + pub other: Option<&'a BStr>, +} + /// Options for the builtin [text driver](crate::blob::BuiltinDriver::Text). #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Options { /// Determine of the diff will be performed. /// Defaults to [`imara_diff::Algorithm::Myers`]. pub diff_algorithm: imara_diff::Algorithm, - /// How to visualize conflicts in merged files. - pub conflict_style: ConflictStyle, - /// The amount of markers to draw, defaults to 7, i.e. `<<<<<<<` - pub marker_size: usize, - /// Decide what to do to automatically resolve conflicts. + /// Decide what to do to automatically resolve conflicts, or to keep them /// If `None`, add conflict markers according to `conflict_style` and `marker_size`. - pub on_conflict: Option, + pub conflict: Conflict, } impl Default for Options { fn default() -> Self { Options { - conflict_style: Default::default(), - marker_size: 7, - on_conflict: None, + conflict: Default::default(), diff_algorithm: imara_diff::Algorithm::Myers, } } @@ -76,13 +82,42 @@ impl Default for Options { /// What to do to resolve a conflict. #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum ResolveWith { +pub enum Conflict { + /// Keep the conflict by marking it in the source file. + Keep { + /// How to visualize conflicts in merged files. + style: ConflictStyle, + /// The amount of markers to draw, defaults to 7, i.e. `<<<<<<<` + marker_size: usize, + }, /// Chose our side to resolve a conflict. - Ours, + ResolveWithOurs, /// Chose their side to resolve a conflict. - Theirs, + ResolveWithTheirs, /// Place our and their lines one after another, in any order - Union, + ResolveWithUnion, +} + +impl Conflict { + /// The amount of conflict marker characters to print by default. + pub const DEFAULT_MARKER_SIZE: usize = 7; + + /// The amount of conflict markers to print if this instance contains them, or `None` otherwise + pub fn marker_size(&self) -> Option { + match self { + Conflict::Keep { marker_size, .. } => Some(*marker_size), + Conflict::ResolveWithOurs | Conflict::ResolveWithTheirs | Conflict::ResolveWithUnion => None, + } + } +} + +impl Default for Conflict { + fn default() -> Self { + Conflict::Keep { + style: Default::default(), + marker_size: Conflict::DEFAULT_MARKER_SIZE, + } + } } pub(super) mod function; diff --git a/gix-merge/src/blob/builtin_driver/text/utils.rs b/gix-merge/src/blob/builtin_driver/text/utils.rs index 9d3db8d5599..1aab3e47f08 100644 --- a/gix-merge/src/blob/builtin_driver/text/utils.rs +++ b/gix-merge/src/blob/builtin_driver/text/utils.rs @@ -168,7 +168,6 @@ fn ancestor_hunk(start: u32, num_lines: u32) -> Hunk { /// /// Return a new vector of all the hunks that were removed from front and back, with partial hunks inserted, /// along with the amount of hunks that go front, with the remaining going towards the back. -// TODO: refactor so hunks and their associated data can go into an array for easier handling. #[must_use] pub fn zealously_contract_hunks( a_hunks: &mut Vec, diff --git a/gix-merge/src/blob/mod.rs b/gix-merge/src/blob/mod.rs index ab4094e9eaf..07f544a2e23 100644 --- a/gix-merge/src/blob/mod.rs +++ b/gix-merge/src/blob/mod.rs @@ -1,6 +1,7 @@ // TODO: remove this - only needed while &mut Vec isn't used. #![allow(clippy::ptr_arg)] +use crate::blob::platform::{DriverChoice, ResourceRef}; use bstr::BString; use std::path::PathBuf; @@ -83,7 +84,7 @@ pub struct Driver { /// * **%L** /// - The conflict-marker size as positive number. /// * **%P** - /// - The path in which the merged result will be stored. + /// - The path in which the merged result would be stored, as workspace-relative path, of the current/ours side. /// * **%S** /// - The conflict-label for the common ancestor or *base*. /// * **%X** @@ -98,6 +99,8 @@ pub struct Driver { /// ``` /// .merge_file_nR2Qs1 .merge_file_WYXCJe .merge_file_UWbzrm 7 file e2a2970 HEAD feature /// ``` + /// + /// The driver is expected to leave its version in the file at `%A`, by overwriting it. pub command: BString, /// If `true`, this is the `name` of the driver to use when a virtual-merge-base is created, as a merge of all /// available merge-bases if there are more than one. @@ -157,3 +160,24 @@ pub struct Platform { /// The way we convert resources into mergeable states. filter_mode: pipeline::Mode, } + +/// The product of a [`prepare_merge()`](Platform::prepare_merge()) call to finally +/// perform the merge and retrieve the merge results. +#[derive(Copy, Clone)] +pub struct PlatformRef<'parent> { + /// The platform that hosts the resources, used to access drivers. + pub(super) parent: &'parent Platform, + /// The current or our side of the merge operation. + pub current: ResourceRef<'parent>, + /// The ancestor or base of the merge operation. + 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: DriverChoice, + /// Possibly processed options for use when performing the actual merge. + /// + /// They may be inspected before the merge, or altered at will. + pub options: platform::merge::Options, +} diff --git a/gix-merge/src/blob/platform.rs b/gix-merge/src/blob/platform.rs deleted file mode 100644 index f749e03c3ec..00000000000 --- a/gix-merge/src/blob/platform.rs +++ /dev/null @@ -1,547 +0,0 @@ -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)] -pub(super) struct Resource { - /// The `id` of the value, or `null` if it's only living in a worktree. - 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 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 - /// or the resource doesn't exist. - buffer: Vec, -} - -/// A blob or executable ready to be merged in one way or another. -#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub struct ResourceRef<'a> { - /// The data itself, suitable for merging, and if the object or worktree item is present at all. - pub data: resource::Data<'a>, - /// The location of the resource, relative to the working tree. - pub rela_path: &'a BStr, - /// 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::{ - pipeline, - platform::{Resource, ResourceRef}, - }; - - impl<'a> ResourceRef<'a> { - pub(super) fn new(cache: &'a Resource) -> Self { - ResourceRef { - data: cache.data.map_or(Data::Missing, |data| match data { - pipeline::Data::Buffer => Data::Buffer(&cache.buffer), - pipeline::Data::TooLarge { size } => Data::Binary { size }, - }), - rela_path: cache.rela_path.as_ref(), - id: &cache.id, - } - } - } - - /// The data of a mergeable resource, as it could be determined and computed previously. - #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] - pub enum Data<'a> { - /// The object is missing, either because it didn't exist in the working tree or because its `id` was null. - Missing, - /// The textual data as processed and ready for merging, i.e. suitable for storage in Git. - Buffer(&'a [u8]), - /// 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. - /// - /// In this state, the binary file cannot be merged. - Binary { - /// 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 - /// content, as once it can be the size of the blob in Git, and once it's the size of file - /// in the worktree. - size: u64, - }, - } - - impl<'a> Data<'a> { - /// Return ourselves as slice of bytes if this instance stores data. - pub fn as_slice(&self) -> Option<&'a [u8]> { - match self { - Data::Buffer(d) => Some(d), - Data::Binary { .. } | Data::Missing => None, - } - } - } -} - -/// -pub mod set_resource { - use bstr::BString; - - use crate::blob::{pipeline, ResourceKind}; - - /// The error returned by [Platform::set_resource](super::Platform::set_resource). - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error("Can only diff blobs, not {mode:?}")] - InvalidMode { mode: gix_object::tree::EntryKind }, - #[error("Failed to read {kind:?} worktree data from '{rela_path}'")] - Io { - rela_path: BString, - kind: ResourceKind, - source: std::io::Error, - }, - #[error("Failed to obtain attributes for {kind:?} resource at '{rela_path}'")] - Attributes { - rela_path: BString, - kind: ResourceKind, - source: std::io::Error, - }, - #[error(transparent)] - ConvertToMergeable(#[from] pipeline::convert_to_mergeable::Error), - } -} - -/// -pub mod merge { - use crate::blob::platform::DriverChoice; - use crate::blob::platform::ResourceRef; - use crate::blob::{builtin_driver, BuiltinDriver, Driver, Resolution}; - use bstr::BString; - - /// The product of a [`prepare_merge()`](crate::blob::Platform::prepare_merge_state()) call to finally - /// perform the merge and retrieve the merge results. - #[derive(Copy, Clone)] - pub struct State<'parent> { - /// The platform that hosts the resources, used to access drivers. - pub(super) parent: &'parent super::Platform, - /// The current or our side of the merge operation. - pub current: ResourceRef<'parent>, - /// The ancestor or base of the merge operation. - 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)] - pub struct Options { - /// If `true`, the resources being merged are contained in a virtual ancestor, - /// which is the case when merge bases are merged into one. - pub is_virtual_ancestor: bool, - /// Determine how to resolve conflicts. If `None`, no conflict resolution is possible and it picks a side. - pub resolve_binary_with: Option, - /// Options for the builtin [text driver](BuiltinDriver::Text). - pub text: builtin_driver::text::Options, - } - - /// - pub mod prepare_external_driver { - use std::ops::{Deref, DerefMut}; - - use crate::blob::ResourceKind; - use bstr::BString; - - /// The error returned by [State::prepare_merge_command()](super::State::prepare_external_driver()). - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error("Binary resources can't be diffed with an external command (as we don't have the data anymore)")] - SourceOrDestinationAreBinary, - #[error( - "Tempfile to store content of '{rela_path}' ({kind:?}) for passing to external merge command could not be created" - )] - CreateTempfile { - rela_path: BString, - kind: ResourceKind, - source: std::io::Error, - }, - #[error( - "Could not write content of '{rela_path}' ({kind:?}) to tempfile for passing to external merge command" - )] - WriteTempfile { - rela_path: BString, - kind: ResourceKind, - source: std::io::Error, - }, - } - - /// The product of a [`prepare_external_driver`](super::State::prepare_external_driver()) operation. - /// - /// This type acts like [`std::process::Command`], ready to run, with `stderr` set to *inherit*, - /// but `stdin` closed and `stdout` setup to be captured. - // TODO: remove this - #[allow(dead_code)] - pub struct Command { - /// The pre-configured command - cmd: std::process::Command, - /// A tempfile holding the *current* (ours) state of the resource. - current: gix_tempfile::Handle, - /// A tempfile holding the *ancestor* (base) state of the resource. - ancestor: gix_tempfile::Handle, - /// A tempfile holding the *other* (their) state of the resource. - other: gix_tempfile::Handle, - } - - impl Deref for Command { - type Target = std::process::Command; - - fn deref(&self) -> &Self::Target { - &self.cmd - } - } - - impl DerefMut for Command { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cmd - } - } - } - - /// - pub mod builtin_merge { - /// An identifier to tell us how a merge conflict was resolved by [builtin_merge](super::State::builtin_merge). - pub enum Pick { - /// Chose the ancestor. - Ancestor, - /// Chose our side. - Ours, - /// Chose their side. - Theirs, - /// New data was produced with the result of the merge, to be found in the buffer that was passed to - /// [builtin_merge()](super::State::builtin_merge). - Buffer, - } - } - - /// The error returned by [State::merge()]. - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error(transparent)] - PrepareExternalDriver(#[from] prepare_external_driver::Error), - } - - /// Plumbing - impl<'parent> State<'parent> { - /// Given `merge_command` and `context`, typically obtained from git-configuration, and the currently set merge-resources, - /// prepare the invocation and temporary files needed to launch it according to protocol. - /// - /// Please note that this is an expensive operation this will always create three temporary files to hold all sides of the merge. - /// - /// ### Deviation - /// - /// We allow passing more context than Git would by taking a whole `context`, it's up to the caller to decide how much is filled. - pub fn prepare_external_driver( - &self, - _merge_command: BString, - _context: gix_command::Context, - ) -> Result { - todo!("prepare command") - } - - /// Perform the merge according to our resources and - /// Note that if the *pick* wasn't [`Buffer`](builtin_merge::Pick::Buffer), then `out` will not have been cleared. - pub fn builtin_merge( - &self, - _out: &mut Vec, - _driver: BuiltinDriver, - _opts: Options, - ) -> (builtin_merge::Pick, Resolution) { - todo!("do full 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.driver_choice { - DriverChoice::BuiltIn(builtin) => Err(builtin), - DriverChoice::Index(idx) => self.parent.drivers.get(idx).ok_or(BuiltinDriver::default()), - } - } - } - - /// Convenience - impl<'parent> State<'parent> { - /// Perform the merge, possibly invoking an external merge command, and store the result in `out`. - /// The merge is configured by `opts` and possible merge driver command executions are affected by `context`. - pub fn merge( - &self, - _out: &mut Vec, - _opts: Options, - _context: gix_command::Context, - ) -> Result { - match self.configured_driver() { - Ok(driver) => { - let _cmd = self.prepare_external_driver(driver.command.clone(), _context)?; - todo!("invoke command and copy result") - } - Err(_builtin) => { - todo!("call builtins and copy results") - } - } - } - } -} - -/// -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("Failed to obtain attributes for {kind:?} resource at '{rela_path}'")] - Attributes { - rela_path: BString, - kind: ResourceKind, - source: std::io::Error, - }, - } -} - -/// Lifecycle -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. - /// `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… - /// - /// * `id` is the hash of the resource. If it [is null](gix_hash::ObjectId::is_null()), it should either - /// be a resource in the worktree, or it's considered a non-existing, deleted object. - /// If an `id` is known, as the hash of the object as (would) be stored in `git`, then it should be provided - /// for completeness. Note that it's not expected to be in `objects` if `rela_path` is set and a worktree-root - /// is available for `kind`. - /// * `mode` is the kind of object (only blobs and links are allowed) - /// * `rela_path` is the relative path as seen from the (work)tree root. - /// * `kind` identifies the side of the merge this resource will be used for. - /// * `objects` provides access to the object database in case the resource can't be read from a worktree. - pub fn set_resource( - &mut self, - id: gix_hash::ObjectId, - mode: gix_object::tree::EntryKind, - rela_path: &BStr, - kind: ResourceKind, - objects: &impl gix_object::FindObjectOrHeader, - ) -> Result<(), set_resource::Error> { - self.set_resource_inner(id, mode, rela_path, kind, objects) - } - - /// Prepare all state needed for performing a merge, using all [previously set](Self::set_resource()) resources. - /// 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), - }; - - Ok(out) - } -} - -impl Platform { - fn set_resource_inner( - &mut self, - id: gix_hash::ObjectId, - mode: gix_object::tree::EntryKind, - rela_path: &BStr, - kind: ResourceKind, - objects: &impl gix_object::FindObjectOrHeader, - ) -> Result<(), set_resource::Error> { - if !matches!( - mode, - gix_object::tree::EntryKind::Blob | gix_object::tree::EntryKind::BlobExecutable - ) { - return Err(set_resource::Error::InvalidMode { mode }); - } - let entry = - self.attr_stack - .at_entry(rela_path, None, objects) - .map_err(|err| set_resource::Error::Attributes { - source: err, - kind, - rela_path: rela_path.to_owned(), - })?; - - let storage = match kind { - ResourceKind::OtherOrTheirs => &mut self.other, - ResourceKind::CommonAncestorOrBase => &mut self.ancestor, - ResourceKind::CurrentOrOurs => &mut self.current, - }; - - let mut buf_storage = Vec::new(); - let out = self.filter.convert_to_mergeable( - &id, - mode, - rela_path, - kind, - &mut |_, out| { - let _ = entry.matching_attributes(out); - }, - objects, - self.filter_mode, - storage.as_mut().map_or(&mut buf_storage, |s| &mut s.buffer), - )?; - - match storage { - None => { - *storage = Some(Resource { - id, - rela_path: rela_path.to_owned(), - data: out, - mode, - buffer: buf_storage, - }); - } - Some(storage) => { - storage.id = id; - storage.rela_path = rela_path.to_owned(); - storage.data = out; - storage.mode = mode; - } - }; - Ok(()) - } -} diff --git a/gix-merge/src/blob/platform/merge.rs b/gix-merge/src/blob/platform/merge.rs new file mode 100644 index 00000000000..c7e47555670 --- /dev/null +++ b/gix-merge/src/blob/platform/merge.rs @@ -0,0 +1,405 @@ +use crate::blob::{builtin_driver, PlatformRef, Resolution}; +use std::io::Read; +use std::path::PathBuf; + +/// Options for the use in the [`PlatformRef::merge()`] call. +#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] +pub struct Options { + /// If `true`, the resources being merged are contained in a virtual ancestor, + /// which is the case when merge bases are merged into one. + pub is_virtual_ancestor: bool, + /// Determine how to resolve conflicts. If `None`, no conflict resolution is possible, and it picks a side. + pub resolve_binary_with: Option, + /// Options for the builtin [text driver](crate::blob::BuiltinDriver::Text). + pub text: builtin_driver::text::Options, +} + +/// The error returned by [`PlatformRef::merge()`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("At least one resource was too large to be processed")] + ResourceTooLarge, + #[error(transparent)] + PrepareExternalDriver(#[from] inner::prepare_external_driver::Error), + #[error("Failed to launch external merge driver: {cmd}")] + SpawnExternalDriver { cmd: String, source: std::io::Error }, + #[error("External merge driver failed with non-zero exit status {status:?}: {cmd}")] + ExternalDriverFailure { + status: std::process::ExitStatus, + cmd: String, + }, + #[error("IO failed when dealing with merge-driver output")] + ExternalDriverIO(#[from] std::io::Error), +} + +/// The product of a [`PlatformRef::prepare_external_driver()`] operation. +/// +/// This type allows to creation of [`std::process::Command`], ready to run, with `stderr` and `stdout` set to *inherit*, +/// but `stdin` closed. +/// It's expected to leave its result in the file substituted at `current` which is then supposed to be read back from there. +// TODO: remove dead-code annotation +#[allow(dead_code)] +pub struct Command { + /// The pre-configured command + cmd: std::process::Command, + /// A tempfile holding the *current* (ours) state of the resource. + current: gix_tempfile::Handle, + /// The path at which `current` is located, for reading the result back from later. + current_path: PathBuf, + /// A tempfile holding the *ancestor* (base) state of the resource. + ancestor: gix_tempfile::Handle, + /// A tempfile holding the *other* (their) state of the resource. + other: gix_tempfile::Handle, +} + +// Just to keep things here but move them a level up later. +pub(super) mod inner { + /// + pub mod prepare_external_driver { + use crate::blob::builtin_driver::text::Conflict; + use crate::blob::platform::{merge, DriverChoice}; + use crate::blob::{builtin_driver, BuiltinDriver, Driver, PlatformRef, ResourceKind}; + use bstr::{BString, ByteVec}; + use gix_tempfile::{AutoRemove, ContainingDirectory}; + use std::io::Write; + use std::ops::{Deref, DerefMut}; + use std::path::{Path, PathBuf}; + use std::process::Stdio; + + /// The error returned by [PlatformRef::prepare_external_driver()](PlatformRef::prepare_external_driver()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("The resource of kind {kind:?} was too large to be processed")] + ResourceTooLarge { kind: ResourceKind }, + #[error( + "Tempfile to store content of '{rela_path}' ({kind:?}) for passing to external merge command could not be created" + )] + CreateTempfile { + rela_path: BString, + kind: ResourceKind, + source: std::io::Error, + }, + #[error( + "Could not write content of '{rela_path}' ({kind:?}) to tempfile for passing to external merge command" + )] + WriteTempfile { + rela_path: BString, + kind: ResourceKind, + source: std::io::Error, + }, + } + + /// Plumbing + impl<'parent> PlatformRef<'parent> { + /// Given `merge_command` and `context`, typically obtained from git-configuration, and the currently set merge-resources, + /// prepare the invocation and temporary files needed to launch it according to protocol. + /// See the documentation of [`Driver::command`] for possible substitutions. + /// + /// Please note that this is an expensive operation this will always create three temporary files to hold all sides of the merge. + /// + /// The resulting command should be spawned, and when successful, [the result file can be opened](merge::Command::open_result_file) + /// to read back the result into a suitable buffer. + /// + /// ### Deviation + /// + /// * We allow passing more context than Git would by taking a whole `context`, + /// it's up to the caller to decide how much is filled. + /// * Our tempfiles aren't suffixed `.merge_file_XXXXXX` with `X` replaced with characters for uniqueness. + pub fn prepare_external_driver( + &self, + merge_command: BString, + builtin_driver::text::Labels { + ancestor, + current, + other, + }: builtin_driver::text::Labels<'_>, + context: gix_command::Context, + ) -> Result { + fn write_data( + data: &[u8], + ) -> std::io::Result<(gix_tempfile::Handle, PathBuf)> { + let mut file = gix_tempfile::new(Path::new(""), ContainingDirectory::Exists, AutoRemove::Tempfile)?; + file.write_all(data)?; + let mut path = Default::default(); + file.with_mut(|f| { + f.path().clone_into(&mut path); + })?; + let file = file.close()?; + Ok((file, path)) + } + + let base = self.ancestor.data.as_slice().ok_or(Error::ResourceTooLarge { + kind: ResourceKind::CommonAncestorOrBase, + })?; + let ours = self.current.data.as_slice().ok_or(Error::ResourceTooLarge { + kind: ResourceKind::CurrentOrOurs, + })?; + let theirs = self.other.data.as_slice().ok_or(Error::ResourceTooLarge { + kind: ResourceKind::OtherOrTheirs, + })?; + + let (base_tmp, base_path) = write_data(base).map_err(|err| Error::CreateTempfile { + rela_path: self.ancestor.rela_path.into(), + kind: ResourceKind::CommonAncestorOrBase, + source: err, + })?; + let (ours_tmp, ours_path) = write_data(ours).map_err(|err| Error::CreateTempfile { + rela_path: self.current.rela_path.into(), + kind: ResourceKind::CurrentOrOurs, + source: err, + })?; + let (theirs_tmp, theirs_path) = write_data(theirs).map_err(|err| Error::CreateTempfile { + rela_path: self.other.rela_path.into(), + kind: ResourceKind::OtherOrTheirs, + source: err, + })?; + + let mut cmd = BString::from(Vec::with_capacity(merge_command.len())); + let mut count = 0; + for token in merge_command.split(|b| *b == b'%') { + count += 1; + let token = if count > 1 { + match token.first() { + Some(&b'O') => { + cmd.push_str(gix_path::into_bstr(&base_path).as_ref()); + &token[1..] + } + Some(&b'A') => { + cmd.push_str(gix_path::into_bstr(&ours_path).as_ref()); + &token[1..] + } + Some(&b'B') => { + cmd.push_str(gix_path::into_bstr(&theirs_path).as_ref()); + &token[1..] + } + Some(&b'L') => { + let marker_size = self + .options + .text + .conflict + .marker_size() + .unwrap_or(Conflict::DEFAULT_MARKER_SIZE); + cmd.push_str(format!("{marker_size}")); + &token[1..] + } + Some(&b'P') => { + cmd.push_str(gix_quote::single(self.current.rela_path)); + &token[1..] + } + Some(&b'S') => { + cmd.push_str(gix_quote::single(ancestor.unwrap_or_default())); + &token[1..] + } + Some(&b'X') => { + cmd.push_str(gix_quote::single(current.unwrap_or_default())); + &token[1..] + } + Some(&b'Y') => { + cmd.push_str(gix_quote::single(other.unwrap_or_default())); + &token[1..] + } + Some(_other) => { + cmd.push(b'%'); + token + } + None => b"%", + } + } else { + token + }; + cmd.extend_from_slice(token); + } + + Ok(merge::Command { + cmd: gix_command::prepare(gix_path::from_bstring(cmd)) + .with_context(context) + .with_shell() + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .into(), + current: ours_tmp, + current_path: ours_path, + ancestor: base_tmp, + other: theirs_tmp, + }) + } + + /// 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.driver { + DriverChoice::BuiltIn(builtin) => Err(builtin), + DriverChoice::Index(idx) => self.parent.drivers.get(idx).ok_or(BuiltinDriver::default()), + } + } + } + + impl std::fmt::Debug for merge::Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.cmd.fmt(f) + } + } + + impl Deref for merge::Command { + type Target = std::process::Command; + + fn deref(&self) -> &Self::Target { + &self.cmd + } + } + + impl DerefMut for merge::Command { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.cmd + } + } + + impl merge::Command { + /// Open the file which should have been written to the location of `ours`, to yield the result of the merge operation. + /// Calling this makes sense only after the merge command has finished successfully. + pub fn open_result_file(&self) -> std::io::Result { + std::fs::File::open(&self.current_path) + } + } + } + + /// + pub mod builtin_merge { + use crate::blob::{builtin_driver, BuiltinDriver, PlatformRef, Resolution}; + + /// An identifier to tell us how a merge conflict was resolved by [builtin_merge](PlatformRef::builtin_merge). + #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] + pub enum Pick { + /// In a binary merge, chose the ancestor. + /// + /// Use [`PlatformRef::buffer_by_pick()`] to retrieve it. + Ancestor, + /// In a binary merge, chose our side. + /// + /// Use [`PlatformRef::buffer_by_pick()`] to retrieve it. + Ours, + /// In a binary merge, chose their side. + /// + /// Use [`PlatformRef::buffer_by_pick()`] to retrieve it. + Theirs, + /// New data was produced with the result of the merge, to be found in the buffer that was passed to + /// [builtin_merge()](PlatformRef::builtin_merge). + /// This happens for any merge that isn't a binary merge. + Buffer, + } + + /// Plumbing + impl<'parent> PlatformRef<'parent> { + /// Perform the merge using the given `driver`, possibly placing the output in `out`. + /// `input` can be used to keep tokens between runs, but note it will only grow in size unless cleared manually. + /// Use `labels` to annotate conflict sections in case of a text-merge. + /// Returns `None` if one of the buffers is too large, making a merge impossible. + /// Note that if the *pick* wasn't [`Pick::Buffer`], then `out` will not have been cleared, + /// and one has to take the data from the respective resource. + pub fn builtin_merge( + &self, + driver: BuiltinDriver, + out: &mut Vec, + input: &mut imara_diff::intern::InternedInput<&'parent [u8]>, + labels: builtin_driver::text::Labels<'_>, + ) -> Option<(Pick, Resolution)> { + let base = self.ancestor.data.as_slice()?; + let ours = self.current.data.as_slice()?; + let theirs = self.other.data.as_slice()?; + Some(match driver { + BuiltinDriver::Text => { + let resolution = + builtin_driver::text(out, input, labels, ours, base, theirs, self.options.text); + (Pick::Buffer, resolution) + } + BuiltinDriver::Binary => { + let (pick, resolution) = builtin_driver::binary(self.options.resolve_binary_with); + let pick = match pick { + builtin_driver::binary::Pick::Ours => Pick::Ours, + builtin_driver::binary::Pick::Theirs => Pick::Theirs, + builtin_driver::binary::Pick::Ancestor => Pick::Ancestor, + }; + (pick, resolution) + } + BuiltinDriver::Union => { + let resolution = builtin_driver::text( + out, + input, + labels, + ours, + base, + theirs, + builtin_driver::text::Options { + conflict: builtin_driver::text::Conflict::ResolveWithUnion, + ..self.options.text + }, + ); + (Pick::Buffer, resolution) + } + }) + } + } + } +} + +/// Convenience +impl<'parent> PlatformRef<'parent> { + /// Perform the merge, possibly invoking an external merge command, and store the result in `out`, returning `(pick, resolution)`. + /// Note that `pick` indicates which resource the buffer should be taken from, unless it's [`Pick::Buffer`](inner::builtin_merge::Pick::Buffer) + /// to indicate it's `out`. + /// Use `labels` to annotate conflict sections in case of a text-merge. + /// The merge is configured by `opts` and possible merge driver command executions are affected by `context`. + /// + /// Note that at this stage, none-existing input data will simply default to an empty buffer when running the actual merge algorithm. + /// Too-large resources will result in an error. + /// + /// Generally, it is assumed that standard logic, like deletions of files, is handled before any of this is called, so we are lenient + /// in terms of buffer handling to make it more useful in the face of missing local files. + pub fn merge( + &self, + out: &mut Vec, + labels: builtin_driver::text::Labels<'_>, + context: gix_command::Context, + ) -> Result<(inner::builtin_merge::Pick, Resolution), Error> { + match self.configured_driver() { + Ok(driver) => { + let mut cmd = self.prepare_external_driver(driver.command.clone(), labels, context)?; + let status = cmd.status().map_err(|err| Error::SpawnExternalDriver { + cmd: format!("{:?}", cmd.cmd), + source: err, + })?; + if !status.success() { + return Err(Error::ExternalDriverFailure { + cmd: format!("{:?}", cmd.cmd), + status, + }); + } + out.clear(); + cmd.open_result_file()?.read_to_end(out)?; + Ok((inner::builtin_merge::Pick::Buffer, Resolution::Complete)) + } + Err(builtin) => { + let mut input = imara_diff::intern::InternedInput::new(&[][..], &[]); + out.clear(); + let (pick, resolution) = self + .builtin_merge(builtin, out, &mut input, labels) + .ok_or(Error::ResourceTooLarge)?; + Ok((pick, resolution)) + } + } + } + + /// Using a `pick` obtained from [`merge()`](Self::merge), obtain the respective buffer suitable for reading or copying. + /// Return `None` if the buffer is too large, or if the `pick` corresponds to a buffer (that was written separately). + pub fn buffer_by_pick(&self, pick: inner::builtin_merge::Pick) -> Option<&'parent [u8]> { + match pick { + inner::builtin_merge::Pick::Ancestor => self.ancestor.data.as_slice(), + inner::builtin_merge::Pick::Ours => self.current.data.as_slice(), + inner::builtin_merge::Pick::Theirs => self.other.data.as_slice(), + inner::builtin_merge::Pick::Buffer => None, + } + } +} diff --git a/gix-merge/src/blob/platform/mod.rs b/gix-merge/src/blob/platform/mod.rs new file mode 100644 index 00000000000..14b33d03fd5 --- /dev/null +++ b/gix-merge/src/blob/platform/mod.rs @@ -0,0 +1,127 @@ +use crate::blob::{pipeline, BuiltinDriver, Pipeline, Platform}; +use bstr::{BStr, BString}; +use gix_filter::attributes; + +/// A stored value representing a resource that participates in a merge. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +pub(super) struct Resource { + /// The `id` of the value, or `null` if it's only living in a worktree. + 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 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 + /// or the resource doesn't exist. + buffer: Vec, +} + +/// A blob or executable ready to be merged in one way or another. +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct ResourceRef<'a> { + /// The data itself, suitable for merging, and if the object or worktree item is present at all. + pub data: resource::Data<'a>, + /// The location of the resource, relative to the working tree. + pub rela_path: &'a BStr, + /// 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 [`Platform::new()`]. +#[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 [`Platform::prepare_merge()`]. +/// +/// 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 platform drivers array](Platform::drivers()). + Index(usize), +} + +impl Default for DriverChoice { + fn default() -> Self { + DriverChoice::BuiltIn(Default::default()) + } +} + +/// Lifecycle +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. + /// `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 + } +} + +/// +pub mod set_resource; + +/// +pub mod resource; + +/// +pub mod merge; +pub use merge::inner::{builtin_merge, prepare_external_driver}; + +/// +pub mod prepare_merge; diff --git a/gix-merge/src/blob/platform/prepare_merge.rs b/gix-merge/src/blob/platform/prepare_merge.rs new file mode 100644 index 00000000000..83eb8f5b4cd --- /dev/null +++ b/gix-merge/src/blob/platform/prepare_merge.rs @@ -0,0 +1,99 @@ +use crate::blob::platform::{merge, DriverChoice, ResourceRef}; +use crate::blob::{BuiltinDriver, Platform, PlatformRef, ResourceKind}; +use bstr::{BStr, BString, ByteSlice}; +use gix_filter::attributes; + +/// The error returned by [Platform::prepare_merge_state()](Platform::prepare_merge()). +#[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("Failed to obtain attributes for {kind:?} resource at '{rela_path}'")] + Attributes { + rela_path: BString, + kind: ResourceKind, + source: std::io::Error, + }, +} + +impl Platform { + /// Prepare all state needed for performing a merge, using all [previously set](Self::set_resource()) resources. + /// `objects` is used to possibly lookup attribute files when obtaining merge-related attributes. + /// + /// `options` are to be used when merging later, and they may be altered to implement correct binary merges + /// in the present of [virtual merge bases](merge::Options::is_virtual_ancestor). + /// + /// Note that no additional validation is performed here to facilitate inspection, which means that + /// resource buffers might still be too large to be merged, preventing a successful merge at a later time. + pub fn prepare_merge( + &mut self, + objects: &impl gix_object::Find, + mut options: merge::Options, + ) -> Result, Error> { + let current = self.current.as_ref().ok_or(Error::UnsetResource)?; + let ancestor = self.ancestor.as_ref().ok_or(Error::UnsetResource)?; + let other = self.other.as_ref().ok_or(Error::UnsetResource)?; + + let entry = self + .attr_stack + .at_entry(current.rela_path.as_bstr(), None, objects) + .map_err(|err| 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 mut 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"), + }; + self.find_driver_by_name(name) + } + }; + if let Some(recursive_driver_name) = match driver { + DriverChoice::Index(idx) => self.drivers.get(idx), + _ => None, + } + .and_then(|driver| driver.recursive.as_deref()) + .filter(|_| options.is_virtual_ancestor) + { + driver = self.find_driver_by_name(Some(recursive_driver_name.as_bstr())); + options.resolve_binary_with = Some(crate::blob::builtin_driver::binary::ResolveWith::Ours); + } + + let out = PlatformRef { + parent: self, + driver, + current: ResourceRef::new(current), + ancestor: ResourceRef::new(ancestor), + other: ResourceRef::new(other), + options, + }; + Ok(out) + } + + fn find_driver_by_name(&self, name: Option<&BStr>) -> DriverChoice { + 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() + } +} diff --git a/gix-merge/src/blob/platform/resource.rs b/gix-merge/src/blob/platform/resource.rs new file mode 100644 index 00000000000..ed646c94233 --- /dev/null +++ b/gix-merge/src/blob/platform/resource.rs @@ -0,0 +1,50 @@ +use crate::blob::{ + pipeline, + platform::{Resource, ResourceRef}, +}; + +impl<'a> ResourceRef<'a> { + pub(super) fn new(cache: &'a Resource) -> Self { + ResourceRef { + data: cache.data.map_or(Data::Missing, |data| match data { + pipeline::Data::Buffer => Data::Buffer(&cache.buffer), + pipeline::Data::TooLarge { size } => Data::TooLarge { size }, + }), + rela_path: cache.rela_path.as_ref(), + id: &cache.id, + } + } +} + +/// The data of a mergeable resource, as it could be determined and computed previously. +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub enum Data<'a> { + /// The object is missing, either because it didn't exist in the working tree or because its `id` was null. + /// Such data equals an empty buffer. + Missing, + /// The textual data as processed and ready for merging, i.e. suitable for storage in Git. + Buffer(&'a [u8]), + /// The file or blob is above the big-file threshold and cannot be processed. + /// + /// 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 + /// content, as once it can be the size of the blob in Git, and once it's the size of file + /// in the worktree. + size: u64, + }, +} + +impl<'a> Data<'a> { + /// Return ourselves as slice of bytes if this instance stores data. + /// Note that missing data is interpreted as empty slice, to facilitate additions and deletions. + pub fn as_slice(&self) -> Option<&'a [u8]> { + match self { + Data::Buffer(d) => Some(d), + Data::Missing => Some(&[]), + Data::TooLarge { .. } => None, + } + } +} diff --git a/gix-merge/src/blob/platform/set_resource.rs b/gix-merge/src/blob/platform/set_resource.rs new file mode 100644 index 00000000000..377642e2355 --- /dev/null +++ b/gix-merge/src/blob/platform/set_resource.rs @@ -0,0 +1,103 @@ +use bstr::{BStr, BString}; + +use crate::blob::platform::Resource; +use crate::blob::{pipeline, Platform, ResourceKind}; + +/// The error returned by [Platform::set_resource](Platform::set_resource). +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Can only diff blobs, not {mode:?}")] + InvalidMode { mode: gix_object::tree::EntryKind }, + #[error("Failed to read {kind:?} worktree data from '{rela_path}'")] + Io { + rela_path: BString, + kind: ResourceKind, + source: std::io::Error, + }, + #[error("Failed to obtain attributes for {kind:?} resource at '{rela_path}'")] + Attributes { + rela_path: BString, + kind: ResourceKind, + source: std::io::Error, + }, + #[error(transparent)] + ConvertToMergeable(#[from] pipeline::convert_to_mergeable::Error), +} + +/// Preparation +impl Platform { + /// Store enough information about a resource to eventually use it in a merge, where… + /// + /// * `id` is the hash of the resource. If it [is null](gix_hash::ObjectId::is_null()), it should either + /// be a resource in the worktree, or it's considered a non-existing, deleted object. + /// If an `id` is known, as the hash of the object as (would) be stored in `git`, then it should be provided + /// for completeness. Note that it's not expected to be in `objects` if `rela_path` is set and a worktree-root + /// is available for `kind`. + /// * `mode` is the kind of object (only blobs and links are allowed) + /// * `rela_path` is the relative path as seen from the (work)tree root. + /// * `kind` identifies the side of the merge this resource will be used for. + /// * `objects` provides access to the object database in case the resource can't be read from a worktree. + pub fn set_resource( + &mut self, + id: gix_hash::ObjectId, + mode: gix_object::tree::EntryKind, + rela_path: &BStr, + kind: ResourceKind, + objects: &impl gix_object::FindObjectOrHeader, + ) -> Result<(), Error> { + if !matches!( + mode, + gix_object::tree::EntryKind::Blob | gix_object::tree::EntryKind::BlobExecutable + ) { + return Err(Error::InvalidMode { mode }); + } + let entry = self + .attr_stack + .at_entry(rela_path, None, objects) + .map_err(|err| Error::Attributes { + source: err, + kind, + rela_path: rela_path.to_owned(), + })?; + + let storage = match kind { + ResourceKind::OtherOrTheirs => &mut self.other, + ResourceKind::CommonAncestorOrBase => &mut self.ancestor, + ResourceKind::CurrentOrOurs => &mut self.current, + }; + + let mut buf_storage = Vec::new(); + let out = self.filter.convert_to_mergeable( + &id, + mode, + rela_path, + kind, + &mut |_, out| { + let _ = entry.matching_attributes(out); + }, + objects, + self.filter_mode, + storage.as_mut().map_or(&mut buf_storage, |s| &mut s.buffer), + )?; + + match storage { + None => { + *storage = Some(Resource { + id, + rela_path: rela_path.to_owned(), + data: out, + mode, + buffer: buf_storage, + }); + } + Some(storage) => { + storage.id = id; + storage.rela_path = rela_path.to_owned(); + storage.data = out; + storage.mode = mode; + } + }; + Ok(()) + } +} diff --git a/gix-merge/tests/fixtures/generated-archives/make_blob_repo.tar b/gix-merge/tests/fixtures/generated-archives/make_blob_repo.tar index ee7859571ca..9105e6caa79 100644 Binary files a/gix-merge/tests/fixtures/generated-archives/make_blob_repo.tar 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 index 13af2c5c6bb..8f4d23f38ec 100644 --- a/gix-merge/tests/fixtures/make_blob_repo.sh +++ b/gix-merge/tests/fixtures/make_blob_repo.sh @@ -3,7 +3,7 @@ set -eu -o pipefail git init -q -echo a > a +echo just-set > just-set echo b > b echo union > union echo e > e-no-attr @@ -11,7 +11,7 @@ echo unset > unset echo unspecified > unspecified cat <.gitattributes -a merge=a +just-set merge b merge=b union merge=union missing merge=missing diff --git a/gix-merge/tests/merge/blob/builtin_driver.rs b/gix-merge/tests/merge/blob/builtin_driver.rs index d42ec7aa823..4976de7a570 100644 --- a/gix-merge/tests/merge/blob/builtin_driver.rs +++ b/gix-merge/tests/merge/blob/builtin_driver.rs @@ -89,12 +89,10 @@ mod text { let actual = gix_merge::blob::builtin_driver::text( &mut out, &mut input, + case.labels(), &case.ours, - Some(case.ours_marker.as_str().as_ref()), &case.base, - Some(case.base_marker.as_str().as_ref()), &case.theirs, - Some(case.theirs_marker.as_str().as_ref()), case.options, ); if is_case_diverging(&case) { @@ -132,7 +130,7 @@ mod text { mod baseline { use bstr::BString; - use gix_merge::blob::builtin_driver::text::{ConflictStyle, ResolveWith}; + use gix_merge::blob::builtin_driver::text::{Conflict, ConflictStyle}; use std::path::Path; #[derive(Debug)] @@ -148,6 +146,16 @@ mod text { pub options: gix_merge::blob::builtin_driver::text::Options, } + impl Expectation { + pub fn labels(&self) -> gix_merge::blob::builtin_driver::text::Labels<'_> { + gix_merge::blob::builtin_driver::text::Labels { + ancestor: Some(self.base_marker.as_str().as_ref()), + current: Some(self.ours_marker.as_str().as_ref()), + other: Some(self.theirs_marker.as_str().as_ref()), + } + } + } + pub struct Expectations<'a> { root: &'a Path, lines: std::str::Lines<'a>, @@ -178,12 +186,18 @@ mod text { let mut options = gix_merge::blob::builtin_driver::text::Options::default(); for arg in words { - match arg { - "--diff3" => options.conflict_style = ConflictStyle::Diff3, - "--zdiff3" => options.conflict_style = ConflictStyle::ZealousDiff3, - "--ours" => options.on_conflict = Some(ResolveWith::Ours), - "--theirs" => options.on_conflict = Some(ResolveWith::Theirs), - "--union" => options.on_conflict = Some(ResolveWith::Union), + options.conflict = match arg { + "--diff3" => Conflict::Keep { + style: ConflictStyle::Diff3, + marker_size: 7, + }, + "--zdiff3" => Conflict::Keep { + style: ConflictStyle::ZealousDiff3, + marker_size: 7, + }, + "--ours" => Conflict::ResolveWithOurs, + "--theirs" => Conflict::ResolveWithTheirs, + "--union" => Conflict::ResolveWithUnion, _ => panic!("Unknown argument to parse into options: '{arg}'"), } } diff --git a/gix-merge/tests/merge/blob/platform.rs b/gix-merge/tests/merge/blob/platform.rs index 6865e097f4d..23595a60bd9 100644 --- a/gix-merge/tests/merge/blob/platform.rs +++ b/gix-merge/tests/merge/blob/platform.rs @@ -1,42 +1,530 @@ -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 merge { + use crate::blob::platform::new_platform; + use crate::blob::util::ObjectDb; + use bstr::{BStr, ByteSlice}; + use gix_merge::blob::builtin_driver::text::ConflictStyle; + use gix_merge::blob::platform::builtin_merge::Pick; + use gix_merge::blob::platform::DriverChoice; + use gix_merge::blob::{builtin_driver, pipeline, platform, BuiltinDriver, Resolution, ResourceKind}; + use gix_object::tree::EntryKind; + use std::process::Stdio; + + #[test] + #[ignore = "TBD"] + fn builtin_text_uses_binary_if_needed() { + + // TODO: should this be part of prepare? Or rather, enforce it in `text` merge impl? + } + + #[test] + fn builtin_with_conflict() -> crate::Result { + let mut platform = new_platform(None, pipeline::Mode::ToGit); + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "b".into(), + ResourceKind::CommonAncestorOrBase, + &gix_object::find::Never, + )?; + + let mut db = ObjectDb::default(); + for (content, kind) in [ + ("ours", ResourceKind::CurrentOrOurs), + ("theirs", ResourceKind::OtherOrTheirs), + ] { + let id = db.insert(content); + platform.set_resource(id, EntryKind::Blob, "b".into(), kind, &db)?; + } + + let mut platform_ref = platform.prepare_merge(&db, Default::default())?; + assert_eq!(platform_ref.driver, DriverChoice::BuiltIn(BuiltinDriver::Text)); + let mut buf = Vec::new(); + let res = platform_ref.merge(&mut buf, default_labels(), Default::default())?; + assert_eq!(res, (Pick::Buffer, Resolution::Conflict)); + assert_eq!( + buf.as_bstr(), + r#"<<<<<<< current label +ours +======= +theirs +>>>>>>> other label +"#, + "default options apply, hence the 'merge' style conflict" + ); + platform_ref.options.text.conflict = builtin_driver::text::Conflict::Keep { + style: ConflictStyle::Diff3, + marker_size: 3, + }; + let res = platform_ref.merge(&mut buf, default_labels(), Default::default())?; + assert_eq!(res, (Pick::Buffer, Resolution::Conflict)); + + assert_eq!( + buf.as_bstr(), + r#"<<< current label +ours +||| ancestor label +b +=== +theirs +>>> other label +"#, + "options apply correctly" + ); + + platform_ref.options.text.conflict = builtin_driver::text::Conflict::ResolveWithOurs; + let res = platform_ref.merge(&mut buf, default_labels(), Default::default())?; + assert_eq!( + res, + (Pick::Buffer, Resolution::Complete), + "it's actually unclear now if there ever was a conflict, but we *could* compute it" + ); + assert_eq!(buf.as_bstr(), "ours"); + + platform_ref.options.text.conflict = builtin_driver::text::Conflict::ResolveWithTheirs; + let res = platform_ref.merge(&mut buf, default_labels(), Default::default())?; + assert_eq!(res, (Pick::Buffer, Resolution::Complete)); + assert_eq!(buf.as_bstr(), "theirs"); + + platform_ref.options.text.conflict = builtin_driver::text::Conflict::ResolveWithUnion; + let res = platform_ref.merge(&mut buf, default_labels(), Default::default())?; + assert_eq!(res, (Pick::Buffer, Resolution::Complete)); + assert_eq!(buf.as_bstr(), "ours\ntheirs"); + + platform_ref.driver = DriverChoice::BuiltIn(BuiltinDriver::Union); + platform_ref.options.text.conflict = builtin_driver::text::Conflict::default(); + let res = platform_ref.merge(&mut buf, default_labels(), Default::default())?; + assert_eq!(res, (Pick::Buffer, Resolution::Complete)); + assert_eq!(buf.as_bstr(), "ours\ntheirs"); + + platform_ref.driver = DriverChoice::BuiltIn(BuiltinDriver::Binary); + let res = platform_ref.merge(&mut buf, default_labels(), Default::default())?; + assert_eq!( + res, + (Pick::Ours, Resolution::Conflict), + "binary merges choose ours but conflict by default" + ); + assert!(buf.is_empty(), "it tells us where to get the content from"); + assert_eq!( + platform_ref.buffer_by_pick(res.0).unwrap().as_bstr(), + "ours", + "getting access to the content is simplified" + ); + + for (expected, expected_pick, resolve) in [ + ("ours", Pick::Ours, builtin_driver::binary::ResolveWith::Ours), + ("theirs", Pick::Theirs, builtin_driver::binary::ResolveWith::Theirs), + ("b\n", Pick::Ancestor, builtin_driver::binary::ResolveWith::Ancestor), + ] { + platform_ref.options.resolve_binary_with = Some(resolve); + let res = platform_ref.merge(&mut buf, default_labels(), Default::default())?; + assert_eq!(res, (expected_pick, Resolution::Complete)); + assert_eq!(platform_ref.buffer_by_pick(res.0).unwrap().as_bstr(), expected); + } + + Ok(()) + } + + #[test] + fn with_external() -> crate::Result { + let mut platform = new_platform( + [gix_merge::blob::Driver { + name: "b".into(), + command: + "for arg in %O %A %B %L %P %S %X %Y %F; do echo $arg >> \"%A\"; done; cat \"%O\" \"%B\" >> \"%A\"" + .into(), + ..Default::default() + }], + pipeline::Mode::ToGit, + ); + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "b".into(), + ResourceKind::CommonAncestorOrBase, + &gix_object::find::Never, + )?; + + let mut db = ObjectDb::default(); + for (content, kind) in [ + ("ours", ResourceKind::CurrentOrOurs), + ("theirs", ResourceKind::OtherOrTheirs), + ] { + let id = db.insert(content); + platform.set_resource(id, EntryKind::Blob, "b".into(), kind, &db)?; + } + + let platform_ref = platform.prepare_merge(&db, Default::default())?; + let mut buf = Vec::new(); + let res = platform_ref.merge(&mut buf, default_labels(), Default::default())?; + assert_eq!(res, (Pick::Buffer, Resolution::Complete), "merge drivers always merge "); + let mut lines = cleanup_driver_args(&buf)?; + for tmp_file in lines.by_ref().take(3) { + assert!(tmp_file.contains_str(&b".tmp"[..]), "{tmp_file}"); + } + + let lines: Vec<_> = lines.collect(); + assert_eq!( + lines, + [ + "7", + "b", + "ancestor label", + "current label", + "other label", + "%F", + "b", + "theirs" + ], + "we handle word-splitting and definitely pick-up what's written into the %A buffer" + ); + Ok(()) + } + + #[test] + fn missing_buffers_are_empty_buffers() -> crate::Result { + let mut platform = new_platform(None, pipeline::Mode::ToGit); + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "just-set".into(), + ResourceKind::CommonAncestorOrBase, + &gix_object::find::Never, + )?; + + // Two deletions + for kind in [ResourceKind::CurrentOrOurs, ResourceKind::OtherOrTheirs] { + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "does not matter for driver".into(), + kind, + &gix_object::find::Never, + )?; + } + + let platform_ref = platform.prepare_merge(&gix_object::find::Never, Default::default())?; + + let mut buf = Vec::new(); + let res = platform_ref.merge(&mut buf, Default::default(), Default::default())?; + assert_eq!( + res, + (Pick::Buffer, Resolution::Complete), + "both versions are deleted, an actual merge happened" + ); + assert!( + buf.is_empty(), + "the new buffer is considered empty, both sides were deleted, too" + ); + + let mut input = imara_diff::intern::InternedInput::new(&[][..], &[]); + let res = platform_ref.builtin_merge(BuiltinDriver::Text, &mut buf, &mut input, Default::default()); + assert_eq!( + res, + Some((Pick::Buffer, Resolution::Complete)), + "both versions are deleted" + ); + assert!(buf.is_empty(), "the result is the same on direct invocation"); + + let print_all = "for arg in $@ %O %A %B %L %P %S %X %Y %F; do echo $arg; done"; + let mut cmd = platform_ref.prepare_external_driver(print_all.into(), default_labels(), Default::default())?; + let stdout = cmd.stdout(Stdio::piped()).output()?.stdout; + let mut lines = cleanup_driver_args(&stdout)?; + for tmp_file in lines.by_ref().take(3) { + assert!(tmp_file.contains_str(&b".tmp"[..]), "{tmp_file}"); + } + let lines: Vec<_> = lines.collect(); + assert_eq!( + lines, + [ + "7", + "does not matter for driver", + "ancestor label", + "current label", + "other label", + "%F" + ], + "word splitting is prevented thanks to proper quoting" + ); + Ok(()) + } + + #[test] + fn one_buffer_too_large() -> crate::Result { + let mut platform = new_platform(None, pipeline::Mode::ToGit); + platform.filter.options.large_file_threshold_bytes = 9; + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "just-set".into(), + ResourceKind::CommonAncestorOrBase, + &gix_object::find::Never, + )?; + platform.filter.roots.other_root = platform.filter.roots.common_ancestor_root.clone(); + platform.filter.roots.current_root = platform.filter.roots.common_ancestor_root.clone(); + + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "b".into(), + ResourceKind::CurrentOrOurs, + &gix_object::find::Never, + )?; + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "unspecified".into(), + ResourceKind::OtherOrTheirs, + &gix_object::find::Never, + )?; + + let platform_ref = platform.prepare_merge(&gix_object::find::Never, Default::default())?; + assert_eq!(platform_ref.other.data, platform::resource::Data::TooLarge { size: 12 }); + + let mut out = Vec::new(); + let err = platform_ref + .merge(&mut out, Default::default(), Default::default()) + .unwrap_err(); + assert!(matches!(err, platform::merge::Error::ResourceTooLarge)); + + let mut input = imara_diff::intern::InternedInput::new(&[][..], &[]); + assert_eq!( + platform_ref.builtin_merge(BuiltinDriver::Text, &mut out, &mut input, Default::default(),), + None + ); + + let err = platform_ref + .prepare_external_driver("bogus".into(), Default::default(), Default::default()) + .unwrap_err(); + assert!(matches!( + err, + platform::prepare_external_driver::Error::ResourceTooLarge { .. } + )); + Ok(()) + } + + fn cleanup_driver_args(buf: &[u8]) -> std::io::Result> { + let current_dir = gix_path::into_bstr(std::env::current_dir()?); + Ok(buf + .lines() + .map(move |line| line.strip_prefix(current_dir.as_bytes()).unwrap_or(line).as_bstr())) + } + + fn default_labels() -> builtin_driver::text::Labels<'static> { + builtin_driver::text::Labels { + ancestor: Some("ancestor label".into()), + current: Some("current label".into()), + other: Some("other label".into()), + } + } +} + +mod prepare_merge { + use crate::blob::platform::new_platform; + use gix_merge::blob::platform::{resource, DriverChoice}; + use gix_merge::blob::{builtin_driver, pipeline, BuiltinDriver, ResourceKind}; + use gix_object::tree::EntryKind; + + #[test] + fn ancestor_and_current_and_other_do_not_exist() -> crate::Result { + let mut platform = new_platform(None, pipeline::Mode::ToGit); + 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(&gix_object::find::Never, Default::default()) + .expect("no validation is done here, let the caller inspect"); + assert_eq!(state.ancestor.data, resource::Data::Missing); + assert_eq!(state.current.data, resource::Data::Missing); + assert_eq!(state.other.data, resource::Data::Missing); + Ok(()) + } + + #[test] + fn driver_selection() -> crate::Result { + let mut platform = new_platform( + [ + gix_merge::blob::Driver { + name: "union".into(), + ..Default::default() + }, + gix_merge::blob::Driver { + name: "to proof it will be sorted".into(), + ..Default::default() + }, + gix_merge::blob::Driver { + name: "b".into(), + recursive: Some("for-recursion".into()), + ..Default::default() + }, + gix_merge::blob::Driver { + name: "for-recursion".into(), + recursive: Some("should not be looked up".into()), + ..Default::default() + }, + ], + pipeline::Mode::ToGit, + ); + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "just-set".into(), + ResourceKind::CommonAncestorOrBase, + &gix_object::find::Never, + )?; + + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "does not matter for driver".into(), + ResourceKind::CurrentOrOurs, + &gix_object::find::Never, + )?; + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::BlobExecutable, + "also does not matter for driver".into(), + ResourceKind::OtherOrTheirs, + &gix_object::find::Never, + )?; + + let prepared = platform.prepare_merge(&gix_object::find::Never, Default::default())?; + assert_eq!( + prepared.driver, + DriverChoice::BuiltIn(BuiltinDriver::Text), + "`merge` attribute means text" + ); + + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "unset".into(), + ResourceKind::CommonAncestorOrBase, + &gix_object::find::Never, + )?; + let prepared = platform.prepare_merge(&gix_object::find::Never, Default::default())?; + assert_eq!( + prepared.driver, + DriverChoice::BuiltIn(BuiltinDriver::Text), + "`-merge` attribute means binary, but it looked up 'current' which is still at some bogus worktree path" + ); + + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "unset".into(), + ResourceKind::CurrentOrOurs, + &gix_object::find::Never, + )?; + let prepared = platform.prepare_merge(&gix_object::find::Never, Default::default())?; + assert_eq!( + prepared.driver, + DriverChoice::BuiltIn(BuiltinDriver::Binary), + "`-merge` attribute means binary" + ); + + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "unspecified".into(), + ResourceKind::CurrentOrOurs, + &gix_object::find::Never, + )?; + let prepared = platform.prepare_merge(&gix_object::find::Never, Default::default())?; + assert_eq!( + prepared.driver, + DriverChoice::BuiltIn(BuiltinDriver::Text), + "`!merge` attribute means the hardcoded default" + ); + + platform.options.default_driver = Some("union".into()); + let prepared = platform.prepare_merge(&gix_object::find::Never, Default::default())?; + let expected_idx = 3; + assert_eq!( + prepared.driver, + DriverChoice::Index(expected_idx), + "`!merge` attribute will also pick up the 'merge.default' configuration, and find the name in passed drivers first.\ + Note that the index is 1, even though it was 0 when passing the drivers - they are sorted by name." + ); + assert_eq!(platform.drivers()[expected_idx].name, "union"); + + platform.options.default_driver = Some("binary".into()); + let prepared = platform.prepare_merge(&gix_object::find::Never, Default::default())?; + assert_eq!( + prepared.driver, + DriverChoice::BuiltIn(BuiltinDriver::Binary), + "`!merge` attribute will also pick up the 'merge.default' configuration, non-overridden builtin filters work as well" + ); + + platform.options.default_driver = Some("Binary".into()); + let prepared = platform.prepare_merge(&gix_object::find::Never, Default::default())?; + assert_eq!( + prepared.driver, + DriverChoice::BuiltIn(BuiltinDriver::Text), + "'merge.default' is case-sensitive" + ); + + platform.set_resource( + gix_hash::Kind::Sha1.null(), + EntryKind::Blob, + "b".into(), + ResourceKind::CurrentOrOurs, + &gix_object::find::Never, + )?; + let prepared = platform.prepare_merge(&gix_object::find::Never, Default::default())?; + let expected_idx = 0; + assert_eq!(prepared.driver, DriverChoice::Index(expected_idx)); + assert_eq!( + platform.drivers()[expected_idx].name, + "b", + "by default, even if recursive is specified, it doesn't look it up" + ); + + let prepared = platform.prepare_merge( + &gix_object::find::Never, + gix_merge::blob::platform::merge::Options { + is_virtual_ancestor: true, + resolve_binary_with: None, + ..Default::default() + }, + )?; + let expected_idx = 1; + assert_eq!(prepared.driver, DriverChoice::Index(expected_idx),); + assert_eq!( + prepared.options.resolve_binary_with, + Some(builtin_driver::binary::ResolveWith::Ours), + "it automatically adjusts the merge mode for binary operations to work for bases" + ); + assert_eq!( + platform.drivers()[expected_idx].name, + "for-recursion", + "It looks up the final driver, including recursion, it only looks it up once though" + ); + Ok(()) + } } mod set_resource { @@ -98,487 +586,3 @@ fn new_platform( 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(()) -// } diff --git a/gix-merge/tests/merge/main.rs b/gix-merge/tests/merge/main.rs index 05375cb2279..9f7a6989d2c 100644 --- a/gix-merge/tests/merge/main.rs +++ b/gix-merge/tests/merge/main.rs @@ -1,3 +1,5 @@ +extern crate core; + #[cfg(feature = "blob")] mod blob;