Skip to content

Commit

Permalink
add platform tests and implementation
Browse files Browse the repository at this point in the history
That way, the platform can be used to perform actual merges.
This will also be a good chance to try the API.
  • Loading branch information
Byron committed Sep 30, 2024
1 parent a6f3e30 commit 182f9ab
Show file tree
Hide file tree
Showing 17 changed files with 1,480 additions and 1,172 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion gix-merge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand All @@ -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 }
Expand Down
147 changes: 69 additions & 78 deletions gix-merge/src/blob/builtin_driver/text/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -23,12 +22,14 @@ use bstr::BStr;
pub fn merge<'a>(
out: &mut Vec<u8>,
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();
Expand Down Expand Up @@ -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, &current_tokens)
}
Expand Down Expand Up @@ -130,28 +131,22 @@ pub fn merge<'a>(
)
.or_else(|| detect_line_ending(our_hunks, input, &current_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, &current_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, &current_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,
&current_tokens,
) {
if hunks_differ_in_diff3(style, our_hunks, their_hunks, input, &current_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, &current_tokens, out);
let ancestor_hunk = Hunk {
before: first_hunk.before.start..last_hunk.before.end,
Expand All @@ -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, &current_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, &current_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, &current_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, &current_tokens, out);
}
Expand All @@ -176,64 +171,60 @@ pub fn merge<'a>(
write_hunks(back_hunks, input, &current_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, &current_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, &current_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, &current_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, &current_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, &current_tokens, out);
assure_ends_with_nl(out, detect_line_ending_or_nl(front_hunks, input, &current_tokens));
write_hunks(our_hunks, input, &current_tokens, out);
assure_ends_with_nl(out, detect_line_ending_or_nl(our_hunks, input, &current_tokens));
write_hunks(their_hunks, input, &current_tokens, out);
if !back_hunks.is_empty() {
assure_ends_with_nl(out, detect_line_ending_or_nl(their_hunks, input, &current_tokens));
}
write_hunks(back_hunks, input, &current_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, &current_tokens, out);
assure_ends_with_nl(out, detect_line_ending_or_nl(front_hunks, input, &current_tokens));
write_hunks(our_hunks, input, &current_tokens, out);
assure_ends_with_nl(out, detect_line_ending_or_nl(our_hunks, input, &current_tokens));
write_hunks(their_hunks, input, &current_tokens, out);
if !back_hunks.is_empty() {
assure_ends_with_nl(out, detect_line_ending_or_nl(their_hunks, input, &current_tokens));
}
write_hunks(back_hunks, input, &current_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 {
Expand Down
61 changes: 48 additions & 13 deletions gix-merge/src/blob/builtin_driver/text/mod.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -48,41 +50,74 @@ 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<ResolveWith>,
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,
}
}
}

/// 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<usize> {
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;
Expand Down
1 change: 0 additions & 1 deletion gix-merge/src/blob/builtin_driver/text/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Hunk>,
Expand Down
26 changes: 25 additions & 1 deletion gix-merge/src/blob/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// TODO: remove this - only needed while &mut Vec<u8> isn't used.
#![allow(clippy::ptr_arg)]

use crate::blob::platform::{DriverChoice, ResourceRef};
use bstr::BString;
use std::path::PathBuf;

Expand Down Expand Up @@ -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**
Expand All @@ -98,6 +99,8 @@ pub struct Driver {
/// ```
/// <driver-program> .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.
Expand Down Expand Up @@ -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,
}
Loading

0 comments on commit 182f9ab

Please sign in to comment.