Skip to content

Commit 1937480

Browse files
committedSep 30, 2024
feat: Repository::merge_resource_cache() to obtain the foundation for merging files directly.
1 parent e0b09d2 commit 1937480

File tree

9 files changed

+242
-1
lines changed

9 files changed

+242
-1
lines changed
 

‎Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎gix/Cargo.toml

+6-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ extras = [
6464
"credentials",
6565
"interrupt",
6666
"status",
67-
"dirwalk"
67+
"dirwalk",
68+
"blob-merge"
6869
]
6970

7071
## A collection of features that need a larger MSRV, and thus are disabled by default.
@@ -137,6 +138,9 @@ revparse-regex = ["regex", "revision"]
137138
## which relies on line-by-line diffs in some cases.
138139
blob-diff = ["gix-diff/blob", "attributes"]
139140

141+
## Add functions to specifically merge files, using the standard three-way merge that git offers.
142+
blob-merge = ["dep:gix-merge", "gix-merge/blob", "attributes"]
143+
140144
## Make it possible to turn a tree into a stream of bytes, which can be decoded to entries and turned into various other formats.
141145
worktree-stream = ["gix-worktree-stream", "attributes"]
142146

@@ -337,6 +341,7 @@ gix-path = { version = "^0.10.11", path = "../gix-path" }
337341
gix-url = { version = "^0.27.5", path = "../gix-url" }
338342
gix-traverse = { version = "^0.41.0", path = "../gix-traverse" }
339343
gix-diff = { version = "^0.46.0", path = "../gix-diff", default-features = false }
344+
gix-merge = { version = "^0.0.0", path = "../gix-merge", default-features = false, optional = true }
340345
gix-mailmap = { version = "^0.24.0", path = "../gix-mailmap", optional = true }
341346
gix-features = { version = "^0.38.2", path = "../gix-features", features = [
342347
"progress",

‎gix/src/config/cache/access.rs

+45
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,51 @@ impl Cache {
100100
Ok(out)
101101
}
102102

103+
#[cfg(feature = "blob-merge")]
104+
pub(crate) fn merge_drivers(&self) -> Result<Vec<gix_merge::blob::Driver>, config::merge::drivers::Error> {
105+
let mut out = Vec::<gix_merge::blob::Driver>::new();
106+
for section in self
107+
.resolved
108+
.sections_by_name("merge")
109+
.into_iter()
110+
.flatten()
111+
.filter(|s| (self.filter_config_section)(s.meta()))
112+
{
113+
let Some(name) = section.header().subsection_name().filter(|n| !n.is_empty()) else {
114+
continue;
115+
};
116+
117+
let driver = match out.iter_mut().find(|d| d.name == name) {
118+
Some(existing) => existing,
119+
None => {
120+
out.push(gix_merge::blob::Driver {
121+
name: name.into(),
122+
display_name: name.into(),
123+
..Default::default()
124+
});
125+
out.last_mut().expect("just pushed")
126+
}
127+
};
128+
129+
if let Some(command) = section.value(config::tree::Merge::DRIVER_COMMAND.name) {
130+
driver.command = command.into_owned().into();
131+
}
132+
if let Some(recursive_name) = section.value(config::tree::Merge::DRIVER_RECURSIVE.name) {
133+
driver.recursive = Some(recursive_name.into_owned().into());
134+
}
135+
}
136+
Ok(out)
137+
}
138+
139+
#[cfg(feature = "blob-merge")]
140+
pub(crate) fn merge_pipeline_options(
141+
&self,
142+
) -> Result<gix_merge::blob::pipeline::Options, config::merge::pipeline_options::Error> {
143+
Ok(gix_merge::blob::pipeline::Options {
144+
large_file_threshold_bytes: self.big_file_threshold()?,
145+
})
146+
}
147+
103148
#[cfg(feature = "blob-diff")]
104149
pub(crate) fn diff_pipeline_options(
105150
&self,

‎gix/src/config/mod.rs

+25
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,31 @@ pub enum Error {
109109
},
110110
}
111111

112+
///
113+
pub mod merge {
114+
///
115+
pub mod pipeline_options {
116+
/// The error produced when obtaining options needed to fill in [gix_merge::blob::pipeline::Options].
117+
#[derive(Debug, thiserror::Error)]
118+
#[allow(missing_docs)]
119+
pub enum Error {
120+
#[error(transparent)]
121+
BigFileThreshold(#[from] crate::config::unsigned_integer::Error),
122+
}
123+
}
124+
125+
///
126+
pub mod drivers {
127+
/// The error produced when obtaining a list of [Drivers](gix_merge::blob::Driver).
128+
#[derive(Debug, thiserror::Error)]
129+
#[allow(missing_docs)]
130+
pub enum Error {
131+
#[error(transparent)]
132+
ConfigBoolean(#[from] crate::config::boolean::Error),
133+
}
134+
}
135+
}
136+
112137
///
113138
pub mod diff {
114139
///

‎gix/src/config/tree/sections/merge.rs

+50
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ impl Merge {
1919
/// The `merge.<driver>.recursive` key.
2020
pub const DRIVER_RECURSIVE: keys::String = keys::String::new_string("recursive", &config::Tree::MERGE)
2121
.with_subsection_requirement(Some(SubSectionRequirement::Parameter("driver")));
22+
/// The `merge.conflictStyle` key.
23+
#[cfg(feature = "blob-merge")]
24+
pub const CONFLICT_STYLE: ConflictStyle =
25+
ConflictStyle::new_with_validate("conflictStyle", &config::Tree::MERGE, validate::ConflictStyle);
2226
}
2327

2428
impl Section for Merge {
@@ -36,3 +40,49 @@ impl Section for Merge {
3640
]
3741
}
3842
}
43+
44+
/// The `merge.conflictStyle` key.
45+
#[cfg(feature = "blob-merge")]
46+
pub type ConflictStyle = keys::Any<validate::ConflictStyle>;
47+
48+
#[cfg(feature = "blob-merge")]
49+
mod conflict_style {
50+
use crate::{bstr::BStr, config, config::tree::sections::merge::ConflictStyle};
51+
use gix_merge::blob::builtin_driver::text;
52+
use std::borrow::Cow;
53+
54+
impl ConflictStyle {
55+
/// Derive the diff algorithm identified by `name`, case-insensitively.
56+
pub fn try_into_conflict_style(
57+
&'static self,
58+
name: Cow<'_, BStr>,
59+
) -> Result<text::ConflictStyle, config::key::GenericErrorWithValue> {
60+
let style = if name.as_ref() == "merge" {
61+
text::ConflictStyle::Merge
62+
} else if name.as_ref() == "diff3" {
63+
text::ConflictStyle::Diff3
64+
} else if name.as_ref() == "zdiff3" {
65+
text::ConflictStyle::ZealousDiff3
66+
} else {
67+
return Err(config::key::GenericErrorWithValue::from_value(self, name.into_owned()));
68+
};
69+
Ok(style)
70+
}
71+
}
72+
}
73+
74+
#[cfg(feature = "blob-merge")]
75+
mod validate {
76+
use crate::{
77+
bstr::BStr,
78+
config::tree::{keys, Merge},
79+
};
80+
81+
pub struct ConflictStyle;
82+
impl keys::Validate for ConflictStyle {
83+
fn validate(&self, value: &BStr) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
84+
Merge::CONFLICT_STYLE.try_into_conflict_style(value.into())?;
85+
Ok(())
86+
}
87+
}
88+
}

‎gix/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ pub use gix_ignore as ignore;
120120
#[cfg(feature = "index")]
121121
pub use gix_index as index;
122122
pub use gix_lock as lock;
123+
#[cfg(feature = "blob-merge")]
124+
pub use gix_merge as merge;
123125
#[cfg(feature = "credentials")]
124126
pub use gix_negotiate as negotiate;
125127
pub use gix_object as objs;

‎gix/src/repository/merge.rs

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use crate::config::cache::util::ApplyLeniencyDefault;
2+
use crate::config::tree;
3+
use crate::repository::merge_resource_cache;
4+
use crate::Repository;
5+
6+
/// Merge-utilities
7+
impl Repository {
8+
/// Create a resource cache that can hold the three resources needed for a three-way merge. `worktree_roots`
9+
/// determines which side of the merge is read from the worktree, or from which worktree.
10+
///
11+
/// The platform can be used to setup resources and finally perform a merge.
12+
///
13+
/// Note that the current index is used for attribute queries.
14+
pub fn merge_resource_cache(
15+
&self,
16+
worktree_roots: gix_merge::blob::pipeline::WorktreeRoots,
17+
) -> Result<gix_merge::blob::Platform, merge_resource_cache::Error> {
18+
let index = self.index_or_load_from_head()?;
19+
let mode = {
20+
let renormalize = self
21+
.config
22+
.resolved
23+
.boolean(&tree::Merge::RENORMALIZE)
24+
.map(|res| {
25+
tree::Merge::RENORMALIZE
26+
.enrich_error(res)
27+
.with_lenient_default(self.config.lenient_config)
28+
})
29+
.transpose()?
30+
.unwrap_or_default();
31+
if renormalize {
32+
gix_merge::blob::pipeline::Mode::Renormalize
33+
} else {
34+
gix_merge::blob::pipeline::Mode::ToGit
35+
}
36+
};
37+
let attrs = self
38+
.attributes_only(
39+
&index,
40+
if worktree_roots.is_unset() {
41+
gix_worktree::stack::state::attributes::Source::IdMapping
42+
} else {
43+
gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping
44+
},
45+
)?
46+
.inner;
47+
let filter = gix_filter::Pipeline::new(self.command_context()?, crate::filter::Pipeline::options(self)?);
48+
let filter = gix_merge::blob::Pipeline::new(worktree_roots, filter, self.config.merge_pipeline_options()?);
49+
let options = gix_merge::blob::platform::Options {
50+
default_driver: self
51+
.config
52+
.resolved
53+
.string(&tree::Merge::DEFAULT)
54+
.map(|name| name.into_owned()),
55+
};
56+
let drivers = self.config.merge_drivers()?;
57+
Ok(gix_merge::blob::Platform::new(filter, mode, attrs, drivers, options))
58+
}
59+
}

‎gix/src/repository/mod.rs

+27
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ mod kind;
4242
mod location;
4343
#[cfg(feature = "mailmap")]
4444
mod mailmap;
45+
///
46+
#[cfg(feature = "blob-merge")]
47+
mod merge;
4548
mod object;
4649
#[cfg(feature = "attributes")]
4750
mod pathspec;
@@ -55,6 +58,30 @@ mod submodule;
5558
mod thread_safe;
5659
mod worktree;
5760

61+
///
62+
#[cfg(feature = "blob-merge")]
63+
pub mod merge_resource_cache {
64+
/// The error returned by [Repository::merge_resource_cache()](crate::Repository::merge_resource_cache()).
65+
#[derive(Debug, thiserror::Error)]
66+
#[allow(missing_docs)]
67+
pub enum Error {
68+
#[error(transparent)]
69+
RenormalizeConfig(#[from] crate::config::boolean::Error),
70+
#[error(transparent)]
71+
PipelineOptions(#[from] crate::config::merge::pipeline_options::Error),
72+
#[error(transparent)]
73+
Index(#[from] crate::repository::index_or_load_from_head::Error),
74+
#[error(transparent)]
75+
AttributeStack(#[from] crate::config::attribute_stack::Error),
76+
#[error(transparent)]
77+
CommandContext(#[from] crate::config::command_context::Error),
78+
#[error(transparent)]
79+
FilterPipeline(#[from] crate::filter::pipeline::options::Error),
80+
#[error(transparent)]
81+
DriversConfig(#[from] crate::config::merge::drivers::Error),
82+
}
83+
}
84+
5885
///
5986
#[cfg(feature = "tree-editor")]
6087
pub mod edit_tree {

‎gix/tests/config/tree.rs

+27
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,33 @@ mod diff {
365365
}
366366
}
367367

368+
#[cfg(feature = "blob-merge")]
369+
mod merge {
370+
use crate::config::tree::bcow;
371+
use gix::config::tree::{Key, Merge};
372+
use gix_merge::blob::builtin_driver::text::ConflictStyle;
373+
374+
#[test]
375+
fn conflict_style() -> crate::Result {
376+
for (actual, expected) in [
377+
("merge", ConflictStyle::Merge),
378+
("diff3", ConflictStyle::Diff3),
379+
("zdiff3", ConflictStyle::ZealousDiff3),
380+
] {
381+
assert_eq!(Merge::CONFLICT_STYLE.try_into_conflict_style(bcow(actual))?, expected);
382+
assert!(Merge::CONFLICT_STYLE.validate(actual.into()).is_ok());
383+
}
384+
assert_eq!(
385+
Merge::CONFLICT_STYLE
386+
.try_into_conflict_style(bcow("foo"))
387+
.unwrap_err()
388+
.to_string(),
389+
"The key \"merge.conflictStyle=foo\" was invalid"
390+
);
391+
Ok(())
392+
}
393+
}
394+
368395
mod core {
369396
use std::time::Duration;
370397

0 commit comments

Comments
 (0)
Please sign in to comment.