Skip to content

Commit

Permalink
feat: Repository::merge_resource_cache() to obtain the foundation f…
Browse files Browse the repository at this point in the history
…or merging files directly.
  • Loading branch information
Byron committed Sep 30, 2024
1 parent e0b09d2 commit 1937480
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 1 deletion.
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.

7 changes: 6 additions & 1 deletion gix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ extras = [
"credentials",
"interrupt",
"status",
"dirwalk"
"dirwalk",
"blob-merge"
]

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

## Add functions to specifically merge files, using the standard three-way merge that git offers.
blob-merge = ["dep:gix-merge", "gix-merge/blob", "attributes"]

## Make it possible to turn a tree into a stream of bytes, which can be decoded to entries and turned into various other formats.
worktree-stream = ["gix-worktree-stream", "attributes"]

Expand Down Expand Up @@ -337,6 +341,7 @@ gix-path = { version = "^0.10.11", path = "../gix-path" }
gix-url = { version = "^0.27.5", path = "../gix-url" }
gix-traverse = { version = "^0.41.0", path = "../gix-traverse" }
gix-diff = { version = "^0.46.0", path = "../gix-diff", default-features = false }
gix-merge = { version = "^0.0.0", path = "../gix-merge", default-features = false, optional = true }
gix-mailmap = { version = "^0.24.0", path = "../gix-mailmap", optional = true }
gix-features = { version = "^0.38.2", path = "../gix-features", features = [
"progress",
Expand Down
45 changes: 45 additions & 0 deletions gix/src/config/cache/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,51 @@ impl Cache {
Ok(out)
}

#[cfg(feature = "blob-merge")]
pub(crate) fn merge_drivers(&self) -> Result<Vec<gix_merge::blob::Driver>, config::merge::drivers::Error> {
let mut out = Vec::<gix_merge::blob::Driver>::new();
for section in self
.resolved
.sections_by_name("merge")
.into_iter()
.flatten()
.filter(|s| (self.filter_config_section)(s.meta()))
{
let Some(name) = section.header().subsection_name().filter(|n| !n.is_empty()) else {
continue;
};

let driver = match out.iter_mut().find(|d| d.name == name) {
Some(existing) => existing,
None => {
out.push(gix_merge::blob::Driver {
name: name.into(),
display_name: name.into(),
..Default::default()
});
out.last_mut().expect("just pushed")
}
};

if let Some(command) = section.value(config::tree::Merge::DRIVER_COMMAND.name) {
driver.command = command.into_owned().into();
}
if let Some(recursive_name) = section.value(config::tree::Merge::DRIVER_RECURSIVE.name) {
driver.recursive = Some(recursive_name.into_owned().into());
}
}
Ok(out)
}

#[cfg(feature = "blob-merge")]
pub(crate) fn merge_pipeline_options(
&self,
) -> Result<gix_merge::blob::pipeline::Options, config::merge::pipeline_options::Error> {
Ok(gix_merge::blob::pipeline::Options {
large_file_threshold_bytes: self.big_file_threshold()?,
})
}

#[cfg(feature = "blob-diff")]
pub(crate) fn diff_pipeline_options(
&self,
Expand Down
25 changes: 25 additions & 0 deletions gix/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,31 @@ pub enum Error {
},
}

///
pub mod merge {
///
pub mod pipeline_options {
/// The error produced when obtaining options needed to fill in [gix_merge::blob::pipeline::Options].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
BigFileThreshold(#[from] crate::config::unsigned_integer::Error),
}
}

///
pub mod drivers {
/// The error produced when obtaining a list of [Drivers](gix_merge::blob::Driver).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
ConfigBoolean(#[from] crate::config::boolean::Error),
}
}
}

///
pub mod diff {
///
Expand Down
50 changes: 50 additions & 0 deletions gix/src/config/tree/sections/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ impl Merge {
/// The `merge.<driver>.recursive` key.
pub const DRIVER_RECURSIVE: keys::String = keys::String::new_string("recursive", &config::Tree::MERGE)
.with_subsection_requirement(Some(SubSectionRequirement::Parameter("driver")));
/// The `merge.conflictStyle` key.
#[cfg(feature = "blob-merge")]
pub const CONFLICT_STYLE: ConflictStyle =
ConflictStyle::new_with_validate("conflictStyle", &config::Tree::MERGE, validate::ConflictStyle);
}

impl Section for Merge {
Expand All @@ -36,3 +40,49 @@ impl Section for Merge {
]
}
}

/// The `merge.conflictStyle` key.
#[cfg(feature = "blob-merge")]
pub type ConflictStyle = keys::Any<validate::ConflictStyle>;

#[cfg(feature = "blob-merge")]
mod conflict_style {
use crate::{bstr::BStr, config, config::tree::sections::merge::ConflictStyle};
use gix_merge::blob::builtin_driver::text;
use std::borrow::Cow;

impl ConflictStyle {
/// Derive the diff algorithm identified by `name`, case-insensitively.
pub fn try_into_conflict_style(
&'static self,
name: Cow<'_, BStr>,
) -> Result<text::ConflictStyle, config::key::GenericErrorWithValue> {
let style = if name.as_ref() == "merge" {
text::ConflictStyle::Merge
} else if name.as_ref() == "diff3" {
text::ConflictStyle::Diff3
} else if name.as_ref() == "zdiff3" {
text::ConflictStyle::ZealousDiff3
} else {
return Err(config::key::GenericErrorWithValue::from_value(self, name.into_owned()));
};
Ok(style)
}
}
}

#[cfg(feature = "blob-merge")]
mod validate {
use crate::{
bstr::BStr,
config::tree::{keys, Merge},
};

pub struct ConflictStyle;
impl keys::Validate for ConflictStyle {
fn validate(&self, value: &BStr) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
Merge::CONFLICT_STYLE.try_into_conflict_style(value.into())?;
Ok(())
}
}
}
2 changes: 2 additions & 0 deletions gix/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ pub use gix_ignore as ignore;
#[cfg(feature = "index")]
pub use gix_index as index;
pub use gix_lock as lock;
#[cfg(feature = "blob-merge")]
pub use gix_merge as merge;
#[cfg(feature = "credentials")]
pub use gix_negotiate as negotiate;
pub use gix_object as objs;
Expand Down
59 changes: 59 additions & 0 deletions gix/src/repository/merge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use crate::config::cache::util::ApplyLeniencyDefault;
use crate::config::tree;
use crate::repository::merge_resource_cache;
use crate::Repository;

/// Merge-utilities
impl Repository {
/// Create a resource cache that can hold the three resources needed for a three-way merge. `worktree_roots`
/// determines which side of the merge is read from the worktree, or from which worktree.
///
/// The platform can be used to setup resources and finally perform a merge.
///
/// Note that the current index is used for attribute queries.
pub fn merge_resource_cache(
&self,
worktree_roots: gix_merge::blob::pipeline::WorktreeRoots,
) -> Result<gix_merge::blob::Platform, merge_resource_cache::Error> {
let index = self.index_or_load_from_head()?;
let mode = {
let renormalize = self
.config
.resolved
.boolean(&tree::Merge::RENORMALIZE)
.map(|res| {
tree::Merge::RENORMALIZE
.enrich_error(res)
.with_lenient_default(self.config.lenient_config)
})
.transpose()?
.unwrap_or_default();
if renormalize {
gix_merge::blob::pipeline::Mode::Renormalize
} else {
gix_merge::blob::pipeline::Mode::ToGit
}
};
let attrs = self
.attributes_only(
&index,
if worktree_roots.is_unset() {
gix_worktree::stack::state::attributes::Source::IdMapping
} else {
gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping
},
)?
.inner;
let filter = gix_filter::Pipeline::new(self.command_context()?, crate::filter::Pipeline::options(self)?);
let filter = gix_merge::blob::Pipeline::new(worktree_roots, filter, self.config.merge_pipeline_options()?);
let options = gix_merge::blob::platform::Options {
default_driver: self
.config
.resolved
.string(&tree::Merge::DEFAULT)
.map(|name| name.into_owned()),
};
let drivers = self.config.merge_drivers()?;
Ok(gix_merge::blob::Platform::new(filter, mode, attrs, drivers, options))
}
}
27 changes: 27 additions & 0 deletions gix/src/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ mod kind;
mod location;
#[cfg(feature = "mailmap")]
mod mailmap;
///
#[cfg(feature = "blob-merge")]
mod merge;
mod object;
#[cfg(feature = "attributes")]
mod pathspec;
Expand All @@ -55,6 +58,30 @@ mod submodule;
mod thread_safe;
mod worktree;

///
#[cfg(feature = "blob-merge")]
pub mod merge_resource_cache {
/// The error returned by [Repository::merge_resource_cache()](crate::Repository::merge_resource_cache()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
RenormalizeConfig(#[from] crate::config::boolean::Error),
#[error(transparent)]
PipelineOptions(#[from] crate::config::merge::pipeline_options::Error),
#[error(transparent)]
Index(#[from] crate::repository::index_or_load_from_head::Error),
#[error(transparent)]
AttributeStack(#[from] crate::config::attribute_stack::Error),
#[error(transparent)]
CommandContext(#[from] crate::config::command_context::Error),
#[error(transparent)]
FilterPipeline(#[from] crate::filter::pipeline::options::Error),
#[error(transparent)]
DriversConfig(#[from] crate::config::merge::drivers::Error),
}
}

///
#[cfg(feature = "tree-editor")]
pub mod edit_tree {
Expand Down
27 changes: 27 additions & 0 deletions gix/tests/config/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,33 @@ mod diff {
}
}

#[cfg(feature = "blob-merge")]
mod merge {
use crate::config::tree::bcow;
use gix::config::tree::{Key, Merge};
use gix_merge::blob::builtin_driver::text::ConflictStyle;

#[test]
fn conflict_style() -> crate::Result {
for (actual, expected) in [
("merge", ConflictStyle::Merge),
("diff3", ConflictStyle::Diff3),
("zdiff3", ConflictStyle::ZealousDiff3),
] {
assert_eq!(Merge::CONFLICT_STYLE.try_into_conflict_style(bcow(actual))?, expected);
assert!(Merge::CONFLICT_STYLE.validate(actual.into()).is_ok());
}
assert_eq!(
Merge::CONFLICT_STYLE
.try_into_conflict_style(bcow("foo"))
.unwrap_err()
.to_string(),
"The key \"merge.conflictStyle=foo\" was invalid"
);
Ok(())
}
}

mod core {
use std::time::Duration;

Expand Down

0 comments on commit 1937480

Please sign in to comment.