Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e864aaa

Browse files
committedSep 28, 2024··
add platform tests and implementation
That way, the platform can be used to perform actual merges. This will also be a good chance to try the API.
1 parent 333a8eb commit e864aaa

File tree

10 files changed

+707
-1066
lines changed

10 files changed

+707
-1066
lines changed
 

‎gix-merge/src/blob/mod.rs

+18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// TODO: remove this - only needed while &mut Vec<u8> isn't used.
22
#![allow(clippy::ptr_arg)]
33

4+
use crate::blob::platform::{DriverChoice, ResourceRef};
45
use bstr::BString;
56
use std::path::PathBuf;
67

@@ -157,3 +158,20 @@ pub struct Platform {
157158
/// The way we convert resources into mergeable states.
158159
filter_mode: pipeline::Mode,
159160
}
161+
162+
/// The product of a [`prepare_merge()`](Platform::prepare_merge()) call to finally
163+
/// perform the merge and retrieve the merge results.
164+
#[derive(Copy, Clone)]
165+
pub struct PlatformRef<'parent> {
166+
/// The platform that hosts the resources, used to access drivers.
167+
pub(super) parent: &'parent Platform,
168+
/// The current or our side of the merge operation.
169+
pub current: ResourceRef<'parent>,
170+
/// The ancestor or base of the merge operation.
171+
pub ancestor: ResourceRef<'parent>,
172+
/// The other or their side of the merge operation.
173+
pub other: ResourceRef<'parent>,
174+
/// Which driver to use according to the resource's configuration,
175+
/// using the path of `current` to read git-attributes.
176+
pub driver: DriverChoice,
177+
}

‎gix-merge/src/blob/platform.rs

-547
This file was deleted.

‎gix-merge/src/blob/platform/merge.rs

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
use crate::blob::{builtin_driver, PlatformRef, Resolution};
2+
use std::ops::{Deref, DerefMut};
3+
4+
/// Options for the use in the [`PlatformRef::merge()`] call.
5+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
6+
pub struct Options {
7+
/// If `true`, the resources being merged are contained in a virtual ancestor,
8+
/// which is the case when merge bases are merged into one.
9+
pub is_virtual_ancestor: bool,
10+
/// Determine how to resolve conflicts. If `None`, no conflict resolution is possible, and it picks a side.
11+
pub resolve_binary_with: Option<builtin_driver::binary::ResolveWith>,
12+
/// Options for the builtin [text driver](crate::blob::BuiltinDriver::Text).
13+
pub text: builtin_driver::text::Options,
14+
}
15+
16+
/// The error returned by [`PlatformRef::merge()`].
17+
#[derive(Debug, thiserror::Error)]
18+
#[allow(missing_docs)]
19+
pub enum Error {
20+
#[error(transparent)]
21+
PrepareExternalDriver(#[from] inner::prepare_external_driver::Error),
22+
}
23+
24+
/// The product of a [`prepare_external_driver`](PlatformRef::prepare_external_driver()) operation.
25+
///
26+
/// This type acts like [`std::process::Command`], ready to run, with `stderr` set to *inherit*,
27+
/// but `stdin` closed and `stdout` setup to be captured.
28+
// TODO: remove dead-code annotation
29+
#[allow(dead_code)]
30+
pub struct Command {
31+
/// The pre-configured command
32+
cmd: std::process::Command,
33+
/// A tempfile holding the *current* (ours) state of the resource.
34+
current: gix_tempfile::Handle<gix_tempfile::handle::Closed>,
35+
/// A tempfile holding the *ancestor* (base) state of the resource.
36+
ancestor: gix_tempfile::Handle<gix_tempfile::handle::Closed>,
37+
/// A tempfile holding the *other* (their) state of the resource.
38+
other: gix_tempfile::Handle<gix_tempfile::handle::Closed>,
39+
}
40+
41+
impl Deref for Command {
42+
type Target = std::process::Command;
43+
44+
fn deref(&self) -> &Self::Target {
45+
&self.cmd
46+
}
47+
}
48+
49+
impl DerefMut for Command {
50+
fn deref_mut(&mut self) -> &mut Self::Target {
51+
&mut self.cmd
52+
}
53+
}
54+
55+
// Just to keep things here but move them a level up later.
56+
pub(super) mod inner {
57+
///
58+
pub mod prepare_external_driver {
59+
use crate::blob::platform::{merge, DriverChoice};
60+
use crate::blob::{BuiltinDriver, Driver, PlatformRef, ResourceKind};
61+
use bstr::BString;
62+
63+
/// The error returned by [PlatformRef::prepare_external_driver()](PlatformRef::prepare_external_driver()).
64+
#[derive(Debug, thiserror::Error)]
65+
#[allow(missing_docs)]
66+
pub enum Error {
67+
#[error("Binary resources can't be diffed with an external command (as we don't have the data anymore)")]
68+
SourceOrDestinationAreBinary,
69+
#[error(
70+
"Tempfile to store content of '{rela_path}' ({kind:?}) for passing to external merge command could not be created"
71+
)]
72+
CreateTempfile {
73+
rela_path: BString,
74+
kind: ResourceKind,
75+
source: std::io::Error,
76+
},
77+
#[error(
78+
"Could not write content of '{rela_path}' ({kind:?}) to tempfile for passing to external merge command"
79+
)]
80+
WriteTempfile {
81+
rela_path: BString,
82+
kind: ResourceKind,
83+
source: std::io::Error,
84+
},
85+
}
86+
87+
/// Plumbing
88+
impl<'parent> PlatformRef<'parent> {
89+
/// Given `merge_command` and `context`, typically obtained from git-configuration, and the currently set merge-resources,
90+
/// prepare the invocation and temporary files needed to launch it according to protocol.
91+
///
92+
/// Please note that this is an expensive operation this will always create three temporary files to hold all sides of the merge.
93+
///
94+
/// ### Deviation
95+
///
96+
/// 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.
97+
pub fn prepare_external_driver(
98+
&self,
99+
_merge_command: BString,
100+
_context: gix_command::Context,
101+
) -> Result<merge::Command, Error> {
102+
todo!("prepare command")
103+
}
104+
105+
/// Return the configured driver program for use with [`Self::prepare_external_driver()`], or `Err`
106+
/// with the built-in driver to use instead.
107+
pub fn configured_driver(&self) -> Result<&'parent Driver, BuiltinDriver> {
108+
match self.driver {
109+
DriverChoice::BuiltIn(builtin) => Err(builtin),
110+
DriverChoice::Index(idx) => self.parent.drivers.get(idx).ok_or(BuiltinDriver::default()),
111+
}
112+
}
113+
}
114+
}
115+
116+
///
117+
pub mod builtin_merge {
118+
use crate::blob::platform::merge;
119+
use crate::blob::{BuiltinDriver, PlatformRef, Resolution};
120+
121+
/// An identifier to tell us how a merge conflict was resolved by [builtin_merge](PlatformRef::builtin_merge).
122+
pub enum Pick {
123+
/// Chose the ancestor.
124+
Ancestor,
125+
/// Chose our side.
126+
Ours,
127+
/// Chose their side.
128+
Theirs,
129+
/// New data was produced with the result of the merge, to be found in the buffer that was passed to
130+
/// [builtin_merge()](PlatformRef::builtin_merge).
131+
Buffer,
132+
}
133+
134+
/// Plumbing
135+
impl<'parent> PlatformRef<'parent> {
136+
/// Perform the merge using the given `driver`, possibly placing the output in `out`.
137+
/// Note that if the *pick* wasn't [`Pick::Buffer`], then `out` will not have been cleared,
138+
/// and one has to take the data from the respective resource.
139+
pub fn builtin_merge(
140+
&self,
141+
_out: &mut Vec<u8>,
142+
_driver: BuiltinDriver,
143+
_opts: merge::Options,
144+
) -> (Pick, Resolution) {
145+
todo!("do full merge")
146+
}
147+
}
148+
}
149+
}
150+
151+
/// Convenience
152+
impl<'parent> PlatformRef<'parent> {
153+
/// Perform the merge, possibly invoking an external merge command, and store the result in `out`.
154+
/// The merge is configured by `opts` and possible merge driver command executions are affected by `context`.
155+
pub fn merge(
156+
&self,
157+
_out: &mut Vec<u8>,
158+
_opts: Options,
159+
context: gix_command::Context,
160+
) -> Result<(Option<inner::builtin_merge::Pick>, Resolution), Error> {
161+
match self.configured_driver() {
162+
Ok(driver) => {
163+
let _cmd = self.prepare_external_driver(driver.command.clone(), context)?;
164+
todo!("invoke command and copy result")
165+
}
166+
Err(_builtin) => {
167+
let (pick, resolution) = self.builtin_merge(_out, _builtin, _opts);
168+
Ok((Some(pick), resolution))
169+
}
170+
}
171+
}
172+
}

‎gix-merge/src/blob/platform/mod.rs

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use crate::blob::{pipeline, BuiltinDriver, Pipeline, Platform};
2+
use bstr::{BStr, BString};
3+
use gix_filter::attributes;
4+
5+
/// A stored value representing a resource that participates in a merge.
6+
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
7+
pub(super) struct Resource {
8+
/// The `id` of the value, or `null` if it's only living in a worktree.
9+
id: gix_hash::ObjectId,
10+
/// The repository-relative path where the resource lives in the tree.
11+
rela_path: BString,
12+
/// The outcome of converting a resource into a mergable format using [Pipeline::convert_to_mergeable()].
13+
data: Option<pipeline::Data>,
14+
/// The kind of the resource we are looking at. Only possible values are `Blob` and `BlobExecutable`.
15+
mode: gix_object::tree::EntryKind,
16+
/// A possibly empty buffer, depending on `conversion.data` which may indicate the data is considered binary
17+
/// or the resource doesn't exist.
18+
buffer: Vec<u8>,
19+
}
20+
21+
/// A blob or executable ready to be merged in one way or another.
22+
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
23+
pub struct ResourceRef<'a> {
24+
/// The data itself, suitable for merging, and if the object or worktree item is present at all.
25+
pub data: resource::Data<'a>,
26+
/// The location of the resource, relative to the working tree.
27+
pub rela_path: &'a BStr,
28+
/// The id of the content as it would be stored in `git`, or `null` if the content doesn't exist anymore at
29+
/// `rela_path` or if it was never computed. This can happen with content read from the worktree, which
30+
/// after its 'to-git' conversion never had its hash computed.
31+
pub id: &'a gix_hash::oid,
32+
}
33+
34+
/// Options for use in [`Platform::new()`].
35+
#[derive(Default, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
36+
pub struct Options {
37+
/// Define which driver to use by name if the `merge` attribute for a resource is unspecified.
38+
///
39+
/// This is the value of the `merge.default` git configuration.
40+
pub default_driver: Option<BString>,
41+
}
42+
43+
/// The selection of the driver to use by a resource obtained with [`Platform::prepare_merge()`].
44+
///
45+
/// If available, an index into the `drivers` field to access more diff-related information of the driver for items
46+
/// at the given path, as previously determined by git-attributes.
47+
///
48+
/// * `merge` is set
49+
/// - Use the [`BuiltinDriver::Text`]
50+
/// * `-merge` is unset
51+
/// - Use the [`BuiltinDriver::Binary`]
52+
/// * `!merge` is unspecified
53+
/// - Use [`Options::default_driver`] or [`BuiltinDriver::Text`].
54+
/// * `merge=name`
55+
/// - Search for a user-configured or built-in driver called `name`.
56+
/// - If not found, silently default to [`BuiltinDriver::Text`]
57+
///
58+
/// Note that drivers are queried even if there is no object available.
59+
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
60+
pub enum DriverChoice {
61+
/// Use the given built-in driver to perform the merge.
62+
BuiltIn(BuiltinDriver),
63+
/// Use the user-provided driver program using the index into [the platform drivers array](Platform::drivers()).
64+
Index(usize),
65+
}
66+
67+
impl Default for DriverChoice {
68+
fn default() -> Self {
69+
DriverChoice::BuiltIn(Default::default())
70+
}
71+
}
72+
73+
/// Lifecycle
74+
impl Platform {
75+
/// Create a new instance with a way to `filter` data from the object database and turn it into something that is merge-able.
76+
/// `filter_mode` decides how to do that specifically.
77+
/// Use `attr_stack` to access attributes pertaining worktree filters and merge settings.
78+
/// `drivers` are the list of available merge drivers that individual paths can refer to by means of git attributes.
79+
/// `options` further configure the operation.
80+
pub fn new(
81+
filter: Pipeline,
82+
filter_mode: pipeline::Mode,
83+
attr_stack: gix_worktree::Stack,
84+
mut drivers: Vec<super::Driver>,
85+
options: Options,
86+
) -> Self {
87+
drivers.sort_by(|a, b| a.name.cmp(&b.name));
88+
Platform {
89+
drivers,
90+
current: None,
91+
ancestor: None,
92+
other: None,
93+
filter,
94+
filter_mode,
95+
attr_stack,
96+
attrs: {
97+
let mut out = attributes::search::Outcome::default();
98+
out.initialize_with_selection(&Default::default(), Some("merge"));
99+
out
100+
},
101+
options,
102+
}
103+
}
104+
}
105+
106+
/// Access
107+
impl Platform {
108+
/// Return all drivers that this instance was initialized with.
109+
///
110+
/// They are sorted by [`name`](super::Driver::name) to support binary searches.
111+
pub fn drivers(&self) -> &[super::Driver] {
112+
&self.drivers
113+
}
114+
}
115+
116+
///
117+
pub mod set_resource;
118+
119+
///
120+
pub mod resource;
121+
122+
///
123+
pub mod merge;
124+
pub use merge::inner::{builtin_merge, prepare_external_driver};
125+
126+
///
127+
pub mod prepare_merge;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use crate::blob::platform::{DriverChoice, ResourceRef};
2+
use crate::blob::{BuiltinDriver, Platform, PlatformRef, ResourceKind};
3+
use bstr::{BString, ByteSlice};
4+
use gix_filter::attributes;
5+
6+
/// The error returned by [Platform::prepare_merge_state()](Platform::prepare_merge()).
7+
#[derive(Debug, thiserror::Error)]
8+
#[allow(missing_docs)]
9+
pub enum Error {
10+
#[error("The 'current', 'ancestor' or 'other' resource for the merge operation were not set")]
11+
UnsetResource,
12+
#[error("Failed to obtain attributes for {kind:?} resource at '{rela_path}'")]
13+
Attributes {
14+
rela_path: BString,
15+
kind: ResourceKind,
16+
source: std::io::Error,
17+
},
18+
}
19+
20+
impl Platform {
21+
/// Prepare all state needed for performing a merge, using all [previously set](Self::set_resource()) resources.
22+
/// `objets` is used to possibly lookup attribute files when obtaining merge-related attributes.
23+
/// Note that no additional validation is performed here to facilitate inspection.
24+
pub fn prepare_merge(&mut self, objects: &impl gix_object::Find) -> Result<PlatformRef<'_>, Error> {
25+
let current = self.current.as_ref().ok_or(Error::UnsetResource)?;
26+
let ancestor = self.ancestor.as_ref().ok_or(Error::UnsetResource)?;
27+
let other = self.other.as_ref().ok_or(Error::UnsetResource)?;
28+
29+
let entry = self
30+
.attr_stack
31+
.at_entry(current.rela_path.as_bstr(), None, objects)
32+
.map_err(|err| Error::Attributes {
33+
source: err,
34+
kind: ResourceKind::CurrentOrOurs,
35+
rela_path: current.rela_path.clone(),
36+
})?;
37+
entry.matching_attributes(&mut self.attrs);
38+
let attr = self.attrs.iter_selected().next().expect("pre-initialized with 'diff'");
39+
let driver = match attr.assignment.state {
40+
attributes::StateRef::Set => DriverChoice::BuiltIn(BuiltinDriver::Text),
41+
attributes::StateRef::Unset => DriverChoice::BuiltIn(BuiltinDriver::Binary),
42+
attributes::StateRef::Value(_) | attributes::StateRef::Unspecified => {
43+
let name = match attr.assignment.state {
44+
attributes::StateRef::Value(name) => Some(name.as_bstr()),
45+
attributes::StateRef::Unspecified => {
46+
self.options.default_driver.as_ref().map(|name| name.as_bstr())
47+
}
48+
_ => unreachable!("only value and unspecified are possible here"),
49+
};
50+
name.and_then(|name| {
51+
self.drivers
52+
.binary_search_by(|d| d.name.as_bstr().cmp(name))
53+
.ok()
54+
.map(DriverChoice::Index)
55+
.or_else(|| {
56+
name.to_str()
57+
.ok()
58+
.and_then(BuiltinDriver::by_name)
59+
.map(DriverChoice::BuiltIn)
60+
})
61+
})
62+
.unwrap_or_default()
63+
}
64+
};
65+
66+
let out = PlatformRef {
67+
parent: self,
68+
driver,
69+
current: ResourceRef::new(current),
70+
ancestor: ResourceRef::new(ancestor),
71+
other: ResourceRef::new(other),
72+
};
73+
74+
Ok(out)
75+
}
76+
}
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use crate::blob::{
2+
pipeline,
3+
platform::{Resource, ResourceRef},
4+
};
5+
6+
impl<'a> ResourceRef<'a> {
7+
pub(super) fn new(cache: &'a Resource) -> Self {
8+
ResourceRef {
9+
data: cache.data.map_or(Data::Missing, |data| match data {
10+
pipeline::Data::Buffer => Data::Buffer(&cache.buffer),
11+
pipeline::Data::TooLarge { size } => Data::TooLarge { size },
12+
}),
13+
rela_path: cache.rela_path.as_ref(),
14+
id: &cache.id,
15+
}
16+
}
17+
}
18+
19+
/// The data of a mergeable resource, as it could be determined and computed previously.
20+
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
21+
pub enum Data<'a> {
22+
/// The object is missing, either because it didn't exist in the working tree or because its `id` was null.
23+
Missing,
24+
/// The textual data as processed and ready for merging, i.e. suitable for storage in Git.
25+
Buffer(&'a [u8]),
26+
/// The file or blob is above the big-file threshold and cannot be processed.
27+
///
28+
/// In this state, the file cannot be merged.
29+
TooLarge {
30+
/// The size of the object prior to performing any filtering or as it was found on disk.
31+
///
32+
/// Note that technically, the size isn't always representative of the same 'state' of the
33+
/// content, as once it can be the size of the blob in Git, and once it's the size of file
34+
/// in the worktree.
35+
size: u64,
36+
},
37+
}
38+
39+
impl<'a> Data<'a> {
40+
/// Return ourselves as slice of bytes if this instance stores data.
41+
pub fn as_slice(&self) -> Option<&'a [u8]> {
42+
match self {
43+
Data::Buffer(d) => Some(d),
44+
Data::TooLarge { .. } | Data::Missing => None,
45+
}
46+
}
47+
}
+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use bstr::{BStr, BString};
2+
3+
use crate::blob::platform::Resource;
4+
use crate::blob::{pipeline, Platform, ResourceKind};
5+
6+
/// The error returned by [Platform::set_resource](Platform::set_resource).
7+
#[derive(Debug, thiserror::Error)]
8+
#[allow(missing_docs)]
9+
pub enum Error {
10+
#[error("Can only diff blobs, not {mode:?}")]
11+
InvalidMode { mode: gix_object::tree::EntryKind },
12+
#[error("Failed to read {kind:?} worktree data from '{rela_path}'")]
13+
Io {
14+
rela_path: BString,
15+
kind: ResourceKind,
16+
source: std::io::Error,
17+
},
18+
#[error("Failed to obtain attributes for {kind:?} resource at '{rela_path}'")]
19+
Attributes {
20+
rela_path: BString,
21+
kind: ResourceKind,
22+
source: std::io::Error,
23+
},
24+
#[error(transparent)]
25+
ConvertToMergeable(#[from] pipeline::convert_to_mergeable::Error),
26+
}
27+
28+
/// Preparation
29+
impl Platform {
30+
/// Store enough information about a resource to eventually use it in a merge, where…
31+
///
32+
/// * `id` is the hash of the resource. If it [is null](gix_hash::ObjectId::is_null()), it should either
33+
/// be a resource in the worktree, or it's considered a non-existing, deleted object.
34+
/// If an `id` is known, as the hash of the object as (would) be stored in `git`, then it should be provided
35+
/// for completeness. Note that it's not expected to be in `objects` if `rela_path` is set and a worktree-root
36+
/// is available for `kind`.
37+
/// * `mode` is the kind of object (only blobs and links are allowed)
38+
/// * `rela_path` is the relative path as seen from the (work)tree root.
39+
/// * `kind` identifies the side of the merge this resource will be used for.
40+
/// * `objects` provides access to the object database in case the resource can't be read from a worktree.
41+
pub fn set_resource(
42+
&mut self,
43+
id: gix_hash::ObjectId,
44+
mode: gix_object::tree::EntryKind,
45+
rela_path: &BStr,
46+
kind: ResourceKind,
47+
objects: &impl gix_object::FindObjectOrHeader,
48+
) -> Result<(), Error> {
49+
if !matches!(
50+
mode,
51+
gix_object::tree::EntryKind::Blob | gix_object::tree::EntryKind::BlobExecutable
52+
) {
53+
return Err(Error::InvalidMode { mode });
54+
}
55+
let entry = self
56+
.attr_stack
57+
.at_entry(rela_path, None, objects)
58+
.map_err(|err| Error::Attributes {
59+
source: err,
60+
kind,
61+
rela_path: rela_path.to_owned(),
62+
})?;
63+
64+
let storage = match kind {
65+
ResourceKind::OtherOrTheirs => &mut self.other,
66+
ResourceKind::CommonAncestorOrBase => &mut self.ancestor,
67+
ResourceKind::CurrentOrOurs => &mut self.current,
68+
};
69+
70+
let mut buf_storage = Vec::new();
71+
let out = self.filter.convert_to_mergeable(
72+
&id,
73+
mode,
74+
rela_path,
75+
kind,
76+
&mut |_, out| {
77+
let _ = entry.matching_attributes(out);
78+
},
79+
objects,
80+
self.filter_mode,
81+
storage.as_mut().map_or(&mut buf_storage, |s| &mut s.buffer),
82+
)?;
83+
84+
match storage {
85+
None => {
86+
*storage = Some(Resource {
87+
id,
88+
rela_path: rela_path.to_owned(),
89+
data: out,
90+
mode,
91+
buffer: buf_storage,
92+
});
93+
}
94+
Some(storage) => {
95+
storage.id = id;
96+
storage.rela_path = rela_path.to_owned();
97+
storage.data = out;
98+
storage.mode = mode;
99+
}
100+
};
101+
Ok(())
102+
}
103+
}
Binary file not shown.

‎gix-merge/tests/fixtures/make_blob_repo.sh

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ set -eu -o pipefail
33

44
git init -q
55

6-
echo a > a
6+
echo just-set > just-set
77
echo b > b
88
echo union > union
99
echo e > e-no-attr
1010
echo unset > unset
1111
echo unspecified > unspecified
1212

1313
cat <<EOF >.gitattributes
14-
a merge=a
14+
just-set merge
1515
b merge=b
1616
union merge=union
1717
missing merge=missing

‎gix-merge/tests/merge/blob/platform.rs

+162-517
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.