Skip to content

Commit 2a2f855

Browse files
committed
add all relevant tests for the merge processing pipeline
1 parent ad9587a commit 2a2f855

File tree

9 files changed

+1187
-208
lines changed

9 files changed

+1187
-208
lines changed

gix-merge/src/blob/mod.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,6 @@ pub struct Pipeline {
125125
pub filter: gix_filter::Pipeline,
126126
/// Options affecting the way we read files.
127127
pub options: pipeline::Options,
128-
/// All available merge drivers.
129-
///
130-
/// They are referenced in git-attributes by name, and we hand out indices into this array.
131-
drivers: Vec<Driver>,
132-
/// Pre-configured attributes to obtain additional merge-related information.
133-
attrs: gix_filter::attributes::search::Outcome,
134128
/// A buffer to produce disk-accessible paths from worktree roots.
135129
path: PathBuf,
136130
}
@@ -152,7 +146,14 @@ pub struct Platform {
152146
pub filter: Pipeline,
153147
/// A way to access `.gitattributes`
154148
pub attr_stack: gix_worktree::Stack,
155-
149+
/// Further configuration that affects the merge.
150+
pub options: platform::Options,
151+
/// All available merge drivers.
152+
///
153+
/// They are referenced in git-attributes by name, and we hand out indices into this array.
154+
drivers: Vec<Driver>,
155+
/// Pre-configured attributes to obtain additional merge-related information.
156+
attrs: gix_filter::attributes::search::Outcome,
156157
/// The way we convert resources into mergeable states.
157158
filter_mode: pipeline::Mode,
158159
}

gix-merge/src/blob/pipeline.rs

Lines changed: 72 additions & 169 deletions
Large diffs are not rendered by default.

gix-merge/src/blob/platform.rs

Lines changed: 134 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
use bstr::{BStr, BString};
2-
3-
use crate::blob::pipeline::DriverChoice;
4-
use crate::blob::{pipeline, Pipeline, Platform, ResourceKind};
1+
use crate::blob::{pipeline, BuiltinDriver, Pipeline, Platform, ResourceKind};
2+
use bstr::{BStr, BString, ByteSlice};
3+
use gix_filter::attributes;
54

65
/// A stored value representing a resource that participates in a merge.
76
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
@@ -10,8 +9,8 @@ pub(super) struct Resource {
109
id: gix_hash::ObjectId,
1110
/// The repository-relative path where the resource lives in the tree.
1211
rela_path: BString,
13-
/// The outcome of converting a resource into a diffable format using [Pipeline::convert_to_mergeable()].
14-
conversion: pipeline::Outcome,
12+
/// The outcome of converting a resource into a mergable format using [Pipeline::convert_to_mergeable()].
13+
data: Option<pipeline::Data>,
1514
/// The kind of the resource we are looking at. Only possible values are `Blob` and `BlobExecutable`.
1615
mode: gix_object::tree::EntryKind,
1716
/// A possibly empty buffer, depending on `conversion.data` which may indicate the data is considered binary
@@ -34,21 +33,61 @@ pub struct ResourceRef<'a> {
3433
pub id: &'a gix_hash::oid,
3534
}
3635

36+
/// Options for use in a [`Platform`].
37+
#[derive(Default, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
38+
pub struct Options {
39+
/// Define which driver to use by name if the `merge` attribute for a resource is unspecified.
40+
///
41+
/// This is the value of the `merge.default` git configuration.
42+
pub default_driver: Option<BString>,
43+
}
44+
45+
/// The selection of the driver to use by a resource obtained with [`Pipeline::convert_to_mergeable()`].
46+
///
47+
/// If available, an index into the `drivers` field to access more diff-related information of the driver for items
48+
/// at the given path, as previously determined by git-attributes.
49+
///
50+
/// * `merge` is set
51+
/// - Use the [`BuiltinDriver::Text`]
52+
/// * `-merge` is unset
53+
/// - Use the [`BuiltinDriver::Binary`]
54+
/// * `!merge` is unspecified
55+
/// - Use [`Options::default_driver`] or [`BuiltinDriver::Text`].
56+
/// * `merge=name`
57+
/// - Search for a user-configured or built-in driver called `name`.
58+
/// - If not found, silently default to [`BuiltinDriver::Text`]
59+
///
60+
/// Note that drivers are queried even if there is no object available.
61+
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
62+
pub enum DriverChoice {
63+
/// Use the given built-in driver to perform the merge.
64+
BuiltIn(BuiltinDriver),
65+
/// Use the user-provided driver program using the index into [the pipelines driver array](Pipeline::drivers().
66+
Index(usize),
67+
}
68+
69+
impl Default for DriverChoice {
70+
fn default() -> Self {
71+
DriverChoice::BuiltIn(Default::default())
72+
}
73+
}
74+
3775
///
3876
pub mod resource {
77+
use crate::blob::platform::DriverChoice;
3978
use crate::blob::{
4079
pipeline,
4180
platform::{Resource, ResourceRef},
4281
};
4382

4483
impl<'a> ResourceRef<'a> {
45-
pub(super) fn new(cache: &'a Resource) -> Self {
84+
pub(super) fn new(cache: &'a Resource, driver: DriverChoice) -> Self {
4685
ResourceRef {
47-
data: cache.conversion.data.map_or(Data::Missing, |data| match data {
86+
data: cache.data.map_or(Data::Missing, |data| match data {
4887
pipeline::Data::Buffer => Data::Buffer(&cache.buffer),
49-
pipeline::Data::Binary { size } => Data::Binary { size },
88+
pipeline::Data::TooLarge { size } => Data::Binary { size },
5089
}),
51-
driver_choice: cache.conversion.driver,
90+
driver_choice: driver,
5291
rela_path: cache.rela_path.as_ref(),
5392
id: &cache.id,
5493
}
@@ -118,7 +157,7 @@ pub mod set_resource {
118157

119158
///
120159
pub mod merge {
121-
use crate::blob::pipeline::DriverChoice;
160+
use crate::blob::platform::DriverChoice;
122161
use crate::blob::platform::ResourceRef;
123162
use crate::blob::{builtin_driver, BuiltinDriver, Driver, Resolution};
124163
use bstr::BString;
@@ -269,7 +308,7 @@ pub mod merge {
269308
pub fn configured_driver(&self) -> Result<&'parent Driver, BuiltinDriver> {
270309
match self.current.driver_choice {
271310
DriverChoice::BuiltIn(builtin) => Err(builtin),
272-
DriverChoice::Index(idx) => self.parent.filter.drivers.get(idx).ok_or(BuiltinDriver::default()),
311+
DriverChoice::Index(idx) => self.parent.drivers.get(idx).ok_or(BuiltinDriver::default()),
273312
}
274313
}
275314
}
@@ -299,6 +338,9 @@ pub mod merge {
299338

300339
///
301340
pub mod prepare_merge {
341+
use crate::blob::ResourceKind;
342+
use bstr::BString;
343+
302344
/// The error returned by [Platform::prepare_merge()](super::Platform::prepare_merge_state()).
303345
#[derive(Debug, thiserror::Error)]
304346
#[allow(missing_docs)]
@@ -307,6 +349,12 @@ pub mod prepare_merge {
307349
UnsetResource,
308350
#[error("Tried to merge 'current' and 'other' where at least one of them is removed")]
309351
CurrentOrOtherRemoved,
352+
#[error("Failed to obtain attributes for {kind:?} resource at '{rela_path}'")]
353+
Attributes {
354+
rela_path: BString,
355+
kind: ResourceKind,
356+
source: std::io::Error,
357+
},
310358
}
311359
}
312360

@@ -315,18 +363,44 @@ impl Platform {
315363
/// Create a new instance with a way to `filter` data from the object database and turn it into something that is merge-able.
316364
/// `filter_mode` decides how to do that specifically.
317365
/// Use `attr_stack` to access attributes pertaining worktree filters and merge settings.
318-
pub fn new(filter: Pipeline, filter_mode: pipeline::Mode, attr_stack: gix_worktree::Stack) -> Self {
366+
/// `drivers` are the list of available merge drivers that individual paths can refer to by means of git attributes.
367+
/// `options` further configure the operation.
368+
pub fn new(
369+
filter: Pipeline,
370+
filter_mode: pipeline::Mode,
371+
attr_stack: gix_worktree::Stack,
372+
mut drivers: Vec<super::Driver>,
373+
options: Options,
374+
) -> Self {
375+
drivers.sort_by(|a, b| a.name.cmp(&b.name));
319376
Platform {
377+
drivers,
320378
current: None,
321379
ancestor: None,
322380
other: None,
323381
filter,
324382
filter_mode,
325383
attr_stack,
384+
attrs: {
385+
let mut out = attributes::search::Outcome::default();
386+
out.initialize_with_selection(&Default::default(), Some("merge"));
387+
out
388+
},
389+
options,
326390
}
327391
}
328392
}
329393

394+
/// Access
395+
impl Platform {
396+
/// Return all drivers that this instance was initialized with.
397+
///
398+
/// They are sorted by [`name`](super::Driver::name) to support binary searches.
399+
pub fn drivers(&self) -> &[super::Driver] {
400+
&self.drivers
401+
}
402+
}
403+
330404
/// Preparation
331405
impl Platform {
332406
/// Store enough information about a resource to eventually use it in a merge, where…
@@ -351,30 +425,60 @@ impl Platform {
351425
self.set_resource_inner(id, mode, rela_path, kind, objects)
352426
}
353427

354-
/// Returns the resource of the given kind if it was set.
355-
pub fn resource(&self, kind: ResourceKind) -> Option<ResourceRef<'_>> {
356-
let cache = match kind {
357-
ResourceKind::CurrentOrOurs => self.current.as_ref(),
358-
ResourceKind::CommonAncestorOrBase => self.ancestor.as_ref(),
359-
ResourceKind::OtherOrTheirs => self.other.as_ref(),
360-
}?;
361-
ResourceRef::new(cache).into()
362-
}
363-
364428
/// Prepare all state needed for performing a merge, using all [previously set](Self::set_resource()) resources.
365-
pub fn prepare_merge_state(&self) -> Result<merge::State<'_>, prepare_merge::Error> {
429+
pub fn prepare_merge_state(
430+
&mut self,
431+
objects: &impl gix_object::Find,
432+
) -> Result<merge::State<'_>, prepare_merge::Error> {
366433
let current = self.current.as_ref().ok_or(prepare_merge::Error::UnsetResource)?;
367434
let ancestor = self.ancestor.as_ref().ok_or(prepare_merge::Error::UnsetResource)?;
368435
let other = self.other.as_ref().ok_or(prepare_merge::Error::UnsetResource)?;
369436

437+
let entry = self
438+
.attr_stack
439+
.at_entry(current.rela_path.as_bstr(), None, objects)
440+
.map_err(|err| prepare_merge::Error::Attributes {
441+
source: err,
442+
kind: ResourceKind::CurrentOrOurs,
443+
rela_path: current.rela_path.clone(),
444+
})?;
445+
entry.matching_attributes(&mut self.attrs);
446+
let attr = self.attrs.iter_selected().next().expect("pre-initialized with 'diff'");
447+
let driver = match attr.assignment.state {
448+
attributes::StateRef::Set => DriverChoice::BuiltIn(BuiltinDriver::Text),
449+
attributes::StateRef::Unset => DriverChoice::BuiltIn(BuiltinDriver::Binary),
450+
attributes::StateRef::Value(_) | attributes::StateRef::Unspecified => {
451+
let name = match attr.assignment.state {
452+
attributes::StateRef::Value(name) => Some(name.as_bstr()),
453+
attributes::StateRef::Unspecified => {
454+
self.options.default_driver.as_ref().map(|name| name.as_bstr())
455+
}
456+
_ => unreachable!("only value and unspecified are possible here"),
457+
};
458+
name.and_then(|name| {
459+
self.drivers
460+
.binary_search_by(|d| d.name.as_bstr().cmp(name))
461+
.ok()
462+
.map(DriverChoice::Index)
463+
.or_else(|| {
464+
name.to_str()
465+
.ok()
466+
.and_then(BuiltinDriver::by_name)
467+
.map(DriverChoice::BuiltIn)
468+
})
469+
})
470+
.unwrap_or_default()
471+
}
472+
};
473+
370474
let out = merge::State {
371475
parent: self,
372-
current: ResourceRef::new(current),
373-
ancestor: ResourceRef::new(ancestor),
374-
other: ResourceRef::new(other),
476+
current: ResourceRef::new(current, driver),
477+
ancestor: ResourceRef::new(ancestor, driver),
478+
other: ResourceRef::new(other, driver),
375479
};
376480

377-
match (current.conversion.data, other.conversion.data) {
481+
match (current.data, other.data) {
378482
(None, None) => Err(prepare_merge::Error::CurrentOrOtherRemoved),
379483
(_, _) => Ok(out),
380484
}
@@ -430,15 +534,15 @@ impl Platform {
430534
*storage = Some(Resource {
431535
id,
432536
rela_path: rela_path.to_owned(),
433-
conversion: out,
537+
data: out,
434538
mode,
435539
buffer: buf_storage,
436540
});
437541
}
438542
Some(storage) => {
439543
storage.id = id;
440544
storage.rela_path = rela_path.to_owned();
441-
storage.conversion = out;
545+
storage.data = out;
442546
storage.mode = mode;
443547
}
444548
};
72.5 KB
Binary file not shown.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env bash
2+
set -eu -o pipefail
3+
4+
git init -q
5+
6+
echo a > a
7+
echo b > b
8+
echo union > union
9+
echo e > e-no-attr
10+
echo unset > unset
11+
echo unspecified > unspecified
12+
13+
cat <<EOF >.gitattributes
14+
a merge=a
15+
b merge=b
16+
union merge=union
17+
missing merge=missing
18+
unset -merge
19+
unspecified !merge
20+
EOF
21+
22+
git add . && git commit -m "init"

gix-merge/tests/merge/blob/builtin_driver.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ mod text {
123123
"Number of expected diverging cases must match the actual one - probably the implementation improved"
124124
);
125125
assert_eq!(
126-
(num_diverging as f32 / num_cases as f32) * 100.0,
127-
12.053572,
126+
((num_diverging as f32 / num_cases as f32) * 100.0) as usize,
127+
12,
128128
"Just to show the percentage of skipped tests - this should get better"
129129
);
130130
Ok(())

gix-merge/tests/merge/blob/mod.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,52 @@
11
mod builtin_driver;
2+
mod pipeline;
3+
mod platform;
4+
5+
mod util {
6+
use std::collections::HashMap;
7+
8+
use gix_hash::oid;
9+
use gix_object::{bstr::BString, find::Error};
10+
11+
#[derive(Default)]
12+
pub struct ObjectDb {
13+
data_by_id: HashMap<gix_hash::ObjectId, BString>,
14+
}
15+
16+
impl gix_object::FindHeader for ObjectDb {
17+
fn try_header(&self, id: &oid) -> Result<Option<gix_object::Header>, Error> {
18+
match self.data_by_id.get(&id.to_owned()) {
19+
Some(data) => Ok(Some(gix_object::Header {
20+
kind: gix_object::Kind::Blob,
21+
size: data.len() as u64,
22+
})),
23+
None => Ok(None),
24+
}
25+
}
26+
}
27+
28+
impl gix_object::Find for ObjectDb {
29+
fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec<u8>) -> Result<Option<gix_object::Data<'a>>, Error> {
30+
match self.data_by_id.get(&id.to_owned()) {
31+
Some(data) => {
32+
buffer.clear();
33+
buffer.extend_from_slice(data);
34+
Ok(Some(gix_object::Data {
35+
kind: gix_object::Kind::Blob,
36+
data: buffer.as_slice(),
37+
}))
38+
}
39+
None => Ok(None),
40+
}
41+
}
42+
}
43+
44+
impl ObjectDb {
45+
/// Insert `data` and return its hash. That can be used to find it again.
46+
pub fn insert(&mut self, data: &str) -> gix_hash::ObjectId {
47+
let id = gix_object::compute_hash(gix_hash::Kind::Sha1, gix_object::Kind::Blob, data.as_bytes());
48+
self.data_by_id.insert(id, data.into());
49+
id
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)