From b26eb2618c1764f699530e251ddef4e6cf456ddd Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 30 Sep 2024 17:05:07 +0200 Subject: [PATCH 1/7] make `blob::Platform::filter_mode` public. That way it can be modified after it was initialized from Git configuration. --- gix-merge/src/blob/builtin_driver/text/mod.rs | 3 +-- gix-merge/src/blob/mod.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gix-merge/src/blob/builtin_driver/text/mod.rs b/gix-merge/src/blob/builtin_driver/text/mod.rs index 1c4287dc7be..6c5a432782d 100644 --- a/gix-merge/src/blob/builtin_driver/text/mod.rs +++ b/gix-merge/src/blob/builtin_driver/text/mod.rs @@ -66,8 +66,7 @@ pub struct Options { /// Determine of the diff will be performed. /// Defaults to [`imara_diff::Algorithm::Myers`]. pub diff_algorithm: imara_diff::Algorithm, - /// Decide what to do to automatically resolve conflicts, or to keep them - /// If `None`, add conflict markers according to `conflict_style` and `marker_size`. + /// Decide what to do to automatically resolve conflicts, or to keep them. pub conflict: Conflict, } diff --git a/gix-merge/src/blob/mod.rs b/gix-merge/src/blob/mod.rs index 07f544a2e23..d114f3dee05 100644 --- a/gix-merge/src/blob/mod.rs +++ b/gix-merge/src/blob/mod.rs @@ -158,7 +158,7 @@ pub struct Platform { /// Pre-configured attributes to obtain additional merge-related information. attrs: gix_filter::attributes::search::Outcome, /// The way we convert resources into mergeable states. - filter_mode: pipeline::Mode, + pub filter_mode: pipeline::Mode, } /// The product of a [`prepare_merge()`](Platform::prepare_merge()) call to finally From e0b09d2764fd02a2b69340d9b3aef9773ae899ce Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 30 Sep 2024 15:12:34 +0200 Subject: [PATCH 2/7] add all keys for merge-configuration --- gix/src/config/tree/mod.rs | 5 +++- gix/src/config/tree/sections/merge.rs | 38 +++++++++++++++++++++++++++ gix/src/config/tree/sections/mod.rs | 4 +++ src/plumbing/progress.rs | 4 --- 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 gix/src/config/tree/sections/merge.rs diff --git a/gix/src/config/tree/mod.rs b/gix/src/config/tree/mod.rs index 0c9ccb01dbe..373c14c7879 100644 --- a/gix/src/config/tree/mod.rs +++ b/gix/src/config/tree/mod.rs @@ -47,6 +47,8 @@ pub(crate) mod root { pub const INIT: sections::Init = sections::Init; /// The `mailmap` section. pub const MAILMAP: sections::Mailmap = sections::Mailmap; + /// The `merge` section. + pub const MERGE: sections::Merge = sections::Merge; /// The `pack` section. pub const PACK: sections::Pack = sections::Pack; /// The `protocol` section. @@ -86,6 +88,7 @@ pub(crate) mod root { &Self::INDEX, &Self::INIT, &Self::MAILMAP, + &Self::MERGE, &Self::PACK, &Self::PROTOCOL, &Self::PUSH, @@ -105,7 +108,7 @@ mod sections; pub use sections::{ branch, checkout, core, credential, extensions, fetch, gitoxide, http, index, protocol, push, remote, ssh, Author, Branch, Checkout, Clone, Committer, Core, Credential, Extensions, Fetch, Gitoxide, Http, Index, Init, Mailmap, - Pack, Protocol, Push, Remote, Safe, Ssh, Url, User, + Merge, Pack, Protocol, Push, Remote, Safe, Ssh, Url, User, }; #[cfg(feature = "blob-diff")] pub use sections::{diff, Diff}; diff --git a/gix/src/config/tree/sections/merge.rs b/gix/src/config/tree/sections/merge.rs new file mode 100644 index 00000000000..5c43d7b772d --- /dev/null +++ b/gix/src/config/tree/sections/merge.rs @@ -0,0 +1,38 @@ +use crate::config; +use crate::config::tree::SubSectionRequirement; +use crate::config::{ + tree::{keys, Key, Merge, Section}, + Tree, +}; + +impl Merge { + /// The `merge.renormalize` key + pub const RENORMALIZE: keys::Boolean = keys::Boolean::new_boolean("renormalize", &Tree::MERGE); + /// The `merge.default` key + pub const DEFAULT: keys::String = keys::String::new_string("default", &Tree::MERGE); + /// The `merge..name` key. + pub const DRIVER_NAME: keys::String = keys::String::new_string("name", &config::Tree::MERGE) + .with_subsection_requirement(Some(SubSectionRequirement::Parameter("driver"))); + /// The `merge..driver` key. + pub const DRIVER_COMMAND: keys::Program = keys::Program::new_program("driver", &config::Tree::MERGE) + .with_subsection_requirement(Some(SubSectionRequirement::Parameter("driver"))); + /// The `merge..recursive` key. + pub const DRIVER_RECURSIVE: keys::String = keys::String::new_string("recursive", &config::Tree::MERGE) + .with_subsection_requirement(Some(SubSectionRequirement::Parameter("driver"))); +} + +impl Section for Merge { + fn name(&self) -> &str { + "merge" + } + + fn keys(&self) -> &[&dyn Key] { + &[ + &Self::RENORMALIZE, + &Self::DEFAULT, + &Self::DRIVER_NAME, + &Self::DRIVER_COMMAND, + &Self::DRIVER_RECURSIVE, + ] + } +} diff --git a/gix/src/config/tree/sections/mod.rs b/gix/src/config/tree/sections/mod.rs index ab2b9542a2c..e4e8db0773f 100644 --- a/gix/src/config/tree/sections/mod.rs +++ b/gix/src/config/tree/sections/mod.rs @@ -76,6 +76,10 @@ mod init; pub struct Mailmap; mod mailmap; +#[derive(Copy, Clone, Default)] +pub struct Merge; +mod merge; + /// The `pack` top-level section. #[derive(Copy, Clone, Default)] pub struct Pack; diff --git a/src/plumbing/progress.rs b/src/plumbing/progress.rs index e0e9e15aece..846837694f8 100644 --- a/src/plumbing/progress.rs +++ b/src/plumbing/progress.rs @@ -146,10 +146,6 @@ static GIT_CONFIG: &[Record] = &[ config: "index.sparse", usage: Planned("We can read sparse indices and support for it will be added early on") }, - Record { - config: "merge.renormalize", - usage: Planned("Once merging is being implemented, renormalization should be respected") - }, Record { config: "sparse.expectFilesOutsideOfPatterns", usage: Planned("A feature definitely worth having") From 19374800ebfa19b0ddc5c2f30d1c42324732a34e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 30 Sep 2024 15:03:15 +0200 Subject: [PATCH 3/7] feat: `Repository::merge_resource_cache()` to obtain the foundation for merging files directly. --- Cargo.lock | 1 + gix/Cargo.toml | 7 +++- gix/src/config/cache/access.rs | 45 ++++++++++++++++++++ gix/src/config/mod.rs | 25 ++++++++++++ gix/src/config/tree/sections/merge.rs | 50 +++++++++++++++++++++++ gix/src/lib.rs | 2 + gix/src/repository/merge.rs | 59 +++++++++++++++++++++++++++ gix/src/repository/mod.rs | 27 ++++++++++++ gix/tests/config/tree.rs | 27 ++++++++++++ 9 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 gix/src/repository/merge.rs diff --git a/Cargo.lock b/Cargo.lock index 13e98214f49..ea9ef059fba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1325,6 +1325,7 @@ dependencies = [ "gix-index 0.35.0", "gix-lock 14.0.0", "gix-mailmap", + "gix-merge", "gix-negotiate", "gix-object 0.44.0", "gix-odb", diff --git a/gix/Cargo.toml b/gix/Cargo.toml index 9fd24a60b04..fef8f3a48ba 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -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. @@ -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"] @@ -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", diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 2575901df0e..d99b0f64fba 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -100,6 +100,51 @@ impl Cache { Ok(out) } + #[cfg(feature = "blob-merge")] + pub(crate) fn merge_drivers(&self) -> Result, config::merge::drivers::Error> { + let mut out = Vec::::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 { + 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, diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index b86a8e395a1..bd1d7f6a1e0 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -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 { /// diff --git a/gix/src/config/tree/sections/merge.rs b/gix/src/config/tree/sections/merge.rs index 5c43d7b772d..32eb833d84e 100644 --- a/gix/src/config/tree/sections/merge.rs +++ b/gix/src/config/tree/sections/merge.rs @@ -19,6 +19,10 @@ impl Merge { /// The `merge..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 { @@ -36,3 +40,49 @@ impl Section for Merge { ] } } + +/// The `merge.conflictStyle` key. +#[cfg(feature = "blob-merge")] +pub type ConflictStyle = keys::Any; + +#[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 { + 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> { + Merge::CONFLICT_STYLE.try_into_conflict_style(value.into())?; + Ok(()) + } + } +} diff --git a/gix/src/lib.rs b/gix/src/lib.rs index 6176e2461b5..7241c86b914 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -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; diff --git a/gix/src/repository/merge.rs b/gix/src/repository/merge.rs new file mode 100644 index 00000000000..9dbf3dd07ca --- /dev/null +++ b/gix/src/repository/merge.rs @@ -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 { + 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)) + } +} diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 696b18f458c..681989f96cd 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -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; @@ -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 { diff --git a/gix/tests/config/tree.rs b/gix/tests/config/tree.rs index 8305fb20698..d89a2a0a1a9 100644 --- a/gix/tests/config/tree.rs +++ b/gix/tests/config/tree.rs @@ -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; From 9e79ba37cf5dc7c0c295218b2de67b4b2eeff443 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 30 Sep 2024 17:03:34 +0200 Subject: [PATCH 4/7] fix!: unify location of error type of `Repository::diff_resource_cache()`. --- gix/src/config/cache/access.rs | 4 ++-- gix/src/object/tree/diff/for_each.rs | 2 +- gix/src/object/tree/diff/mod.rs | 2 +- gix/src/repository/diff.rs | 20 +++----------------- gix/src/repository/merge.rs | 7 ++----- gix/src/repository/mod.rs | 18 +++++++++++++++++- 6 files changed, 26 insertions(+), 27 deletions(-) diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index d99b0f64fba..d2aef845902 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -127,10 +127,10 @@ impl Cache { }; if let Some(command) = section.value(config::tree::Merge::DRIVER_COMMAND.name) { - driver.command = command.into_owned().into(); + driver.command = command.into_owned(); } if let Some(recursive_name) = section.value(config::tree::Merge::DRIVER_RECURSIVE.name) { - driver.recursive = Some(recursive_name.into_owned().into()); + driver.recursive = Some(recursive_name.into_owned()); } } Ok(out) diff --git a/gix/src/object/tree/diff/for_each.rs b/gix/src/object/tree/diff/for_each.rs index 0266db9793b..0f186429116 100644 --- a/gix/src/object/tree/diff/for_each.rs +++ b/gix/src/object/tree/diff/for_each.rs @@ -18,7 +18,7 @@ pub enum Error { #[error("The user-provided callback failed")] ForEach(#[source] Box), #[error(transparent)] - ResourceCache(#[from] crate::repository::diff::resource_cache::Error), + ResourceCache(#[from] crate::repository::diff_resource_cache::Error), #[error("Failure during rename tracking")] RenameTracking(#[from] tracker::emit::Error), } diff --git a/gix/src/object/tree/diff/mod.rs b/gix/src/object/tree/diff/mod.rs index dcf0e471a61..54c96392c78 100644 --- a/gix/src/object/tree/diff/mod.rs +++ b/gix/src/object/tree/diff/mod.rs @@ -121,7 +121,7 @@ pub mod stats { #[allow(missing_docs)] pub enum Error { #[error(transparent)] - CreateResourceCache(#[from] crate::repository::diff::resource_cache::Error), + CreateResourceCache(#[from] crate::repository::diff_resource_cache::Error), #[error(transparent)] ForEachChange(#[from] crate::object::tree::diff::for_each::Error), } diff --git a/gix/src/repository/diff.rs b/gix/src/repository/diff.rs index 4f98ebe52f3..5644c1ceec0 100644 --- a/gix/src/repository/diff.rs +++ b/gix/src/repository/diff.rs @@ -1,20 +1,6 @@ +use crate::repository::diff_resource_cache; use crate::Repository; -/// -pub mod resource_cache { - /// The error returned by [Repository::diff_resource_cache()](super::Repository::diff_resource_cache()). - #[derive(Debug, thiserror::Error)] - #[allow(missing_docs)] - pub enum Error { - #[error("Could not obtain resource cache for diffing")] - ResourceCache(#[from] crate::diff::resource_cache::Error), - #[error(transparent)] - Index(#[from] crate::repository::index_or_load_from_head::Error), - #[error(transparent)] - AttributeStack(#[from] crate::config::attribute_stack::Error), - } -} - /// Diff-utilities impl Repository { /// Create a resource cache for diffable objects, and configured with everything it needs to know to perform diffs @@ -31,7 +17,7 @@ impl Repository { &self, mode: gix_diff::blob::pipeline::Mode, worktree_roots: gix_diff::blob::pipeline::WorktreeRoots, - ) -> Result { + ) -> Result { let index = self.index_or_load_from_head()?; Ok(crate::diff::resource_cache( self, @@ -52,7 +38,7 @@ impl Repository { /// Return a resource cache suitable for diffing blobs from trees directly, where no worktree checkout exists. /// /// For more control, see [`diff_resource_cache()`](Self::diff_resource_cache). - pub fn diff_resource_cache_for_tree_diff(&self) -> Result { + pub fn diff_resource_cache_for_tree_diff(&self) -> Result { self.diff_resource_cache( gix_diff::blob::pipeline::Mode::ToGit, gix_diff::blob::pipeline::WorktreeRoots::default(), diff --git a/gix/src/repository/merge.rs b/gix/src/repository/merge.rs index 9dbf3dd07ca..d8d08fd1a99 100644 --- a/gix/src/repository/merge.rs +++ b/gix/src/repository/merge.rs @@ -2,6 +2,7 @@ use crate::config::cache::util::ApplyLeniencyDefault; use crate::config::tree; use crate::repository::merge_resource_cache; use crate::Repository; +use std::borrow::Cow; /// Merge-utilities impl Repository { @@ -47,11 +48,7 @@ impl Repository { 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()), + default_driver: self.config.resolved.string(&tree::Merge::DEFAULT).map(Cow::into_owned), }; let drivers = self.config.merge_drivers()?; Ok(gix_merge::blob::Platform::new(filter, mode, attrs, drivers, options)) diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 681989f96cd..22695c8b595 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -23,7 +23,7 @@ mod cache; mod config; /// #[cfg(feature = "blob-diff")] -pub mod diff; +mod diff; /// #[cfg(feature = "dirwalk")] mod dirwalk; @@ -82,6 +82,22 @@ pub mod merge_resource_cache { } } +/// +#[cfg(feature = "blob-diff")] +pub mod diff_resource_cache { + /// The error returned by [Repository::diff_resource_cache()](crate::Repository::diff_resource_cache()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("Could not obtain resource cache for diffing")] + ResourceCache(#[from] crate::diff::resource_cache::Error), + #[error(transparent)] + Index(#[from] crate::repository::index_or_load_from_head::Error), + #[error(transparent)] + AttributeStack(#[from] crate::config::attribute_stack::Error), + } +} + /// #[cfg(feature = "tree-editor")] pub mod edit_tree { From c02adc736c9c150d2eb71307a13adfa5f35e9d47 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 30 Sep 2024 18:45:33 +0200 Subject: [PATCH 5/7] feat: add `Repository::blob_merge_options()` to obtain options for merging blobs and `Repository::diff_algorithm()` --- gix/src/repository/config/mod.rs | 8 ++++++++ gix/src/repository/merge.rs | 28 +++++++++++++++++++++++++++- gix/src/repository/mod.rs | 14 ++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/gix/src/repository/config/mod.rs b/gix/src/repository/config/mod.rs index a7c61d435e8..dbad6965f8e 100644 --- a/gix/src/repository/config/mod.rs +++ b/gix/src/repository/config/mod.rs @@ -120,6 +120,14 @@ impl crate::Repository { pub fn object_hash(&self) -> gix_hash::Kind { self.config.object_hash } + + /// Return the algorithm to perform diffs or merges with. + /// + /// In case of merges, a diff is performed under the hood in order to learn which hunks need merging. + #[cfg(feature = "blob-diff")] + pub fn diff_algorithm(&self) -> Result { + self.config.diff_algorithm() + } } mod branch; diff --git a/gix/src/repository/merge.rs b/gix/src/repository/merge.rs index d8d08fd1a99..ff3c2537525 100644 --- a/gix/src/repository/merge.rs +++ b/gix/src/repository/merge.rs @@ -1,7 +1,8 @@ use crate::config::cache::util::ApplyLeniencyDefault; use crate::config::tree; -use crate::repository::merge_resource_cache; +use crate::repository::{blob_merge_options, merge_resource_cache}; use crate::Repository; +use gix_merge::blob::builtin_driver::text; use std::borrow::Cow; /// Merge-utilities @@ -53,4 +54,29 @@ impl Repository { let drivers = self.config.merge_drivers()?; Ok(gix_merge::blob::Platform::new(filter, mode, attrs, drivers, options)) } + + /// Return options for use with [`gix_merge::blob::PlatformRef::merge()`]. + pub fn blob_merge_options(&self) -> Result { + Ok(gix_merge::blob::platform::merge::Options { + is_virtual_ancestor: false, + resolve_binary_with: None, + text: gix_merge::blob::builtin_driver::text::Options { + diff_algorithm: self.config.diff_algorithm()?, + conflict: text::Conflict::Keep { + style: self + .config + .resolved + .string(&tree::Merge::CONFLICT_STYLE) + .map(|value| { + tree::Merge::CONFLICT_STYLE + .try_into_conflict_style(value) + .with_lenient_default(self.config.lenient_config) + }) + .transpose()? + .unwrap_or_default(), + marker_size: text::Conflict::DEFAULT_MARKER_SIZE, + }, + }, + }) + } } diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 22695c8b595..598720fc8eb 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -58,6 +58,20 @@ mod submodule; mod thread_safe; mod worktree; +/// +#[cfg(feature = "blob-merge")] +pub mod blob_merge_options { + /// The error returned by [Repository::blob_merge_options()](crate::Repository::blob_merge_options()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + DiffAlgorithm(#[from] crate::config::diff::algorithm::Error), + #[error(transparent)] + ConflictStyle(#[from] crate::config::key::GenericErrorWithValue), + } +} + /// #[cfg(feature = "blob-merge")] pub mod merge_resource_cache { From 3da2da9d7993adc16b19fc63e7524c768a6e2e7f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 30 Sep 2024 18:42:24 +0200 Subject: [PATCH 6/7] feat: add `gix merge-file` with similar features as `git merge-file` --- gitoxide-core/Cargo.toml | 2 +- gitoxide-core/src/repository/merge.rs | 113 ++++++++++++++++++++++++++ gitoxide-core/src/repository/mod.rs | 1 + gix/src/repository/merge.rs | 2 +- src/plumbing/main.rs | 37 +++++++++ src/plumbing/options/mod.rs | 42 ++++++++++ 6 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 gitoxide-core/src/repository/merge.rs diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index 9e4c005f4f7..64115e5406a 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -49,7 +49,7 @@ serde = ["gix/serde", "dep:serde_json", "dep:serde", "bytesize/serde"] [dependencies] # deselect everything else (like "performance") as this should be controllable by the parent application. -gix = { version = "^0.66.0", path = "../gix", default-features = false, features = ["blob-diff", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status", "dirwalk"] } +gix = { version = "^0.66.0", path = "../gix", default-features = false, features = ["blob-merge", "blob-diff", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status", "dirwalk"] } gix-pack-for-configuration-only = { package = "gix-pack", version = "^0.53.0", path = "../gix-pack", default-features = false, features = ["pack-cache-lru-dynamic", "pack-cache-lru-static", "generate", "streaming-input"] } gix-transport-configuration-only = { package = "gix-transport", version = "^0.42.3", path = "../gix-transport", default-features = false } gix-archive-for-configuration-only = { package = "gix-archive", version = "^0.15.0", path = "../gix-archive", optional = true, features = ["tar", "tar_gz"] } diff --git a/gitoxide-core/src/repository/merge.rs b/gitoxide-core/src/repository/merge.rs new file mode 100644 index 00000000000..6698dd3dd2f --- /dev/null +++ b/gitoxide-core/src/repository/merge.rs @@ -0,0 +1,113 @@ +use crate::OutputFormat; +use anyhow::{bail, Context}; +use gix::bstr::BString; +use gix::bstr::ByteSlice; +use gix::merge::blob::builtin_driver::binary; +use gix::merge::blob::builtin_driver::text::Conflict; +use gix::merge::blob::pipeline::WorktreeRoots; +use gix::merge::blob::{Resolution, ResourceKind}; +use gix::object::tree::EntryKind; +use gix::Id; +use std::path::Path; + +pub fn file( + repo: gix::Repository, + out: &mut dyn std::io::Write, + format: OutputFormat, + conflict: Option, + base: BString, + ours: BString, + theirs: BString, +) -> anyhow::Result<()> { + if format != OutputFormat::Human { + bail!("JSON output isn't implemented yet"); + } + let index = &repo.index_or_load_from_head()?; + let specs = repo.pathspec( + false, + [base, ours, theirs], + true, + index, + gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping.adjust_for_bare(repo.is_bare()), + )?; + // TODO: there should be a way to normalize paths without going through patterns, at least in this case maybe? + // `Search` actually sorts patterns by excluding or not, all that can lead to strange results. + let mut patterns = specs.search().patterns().map(|p| p.path().to_owned()); + let base = patterns.next().unwrap(); + let ours = patterns.next().unwrap(); + let theirs = patterns.next().unwrap(); + + let base_id = repo.rev_parse_single(base.as_bstr()).ok(); + let ours_id = repo.rev_parse_single(ours.as_bstr()).ok(); + let theirs_id = repo.rev_parse_single(theirs.as_bstr()).ok(); + let roots = worktree_roots(base_id, ours_id, theirs_id, repo.work_dir())?; + + let mut cache = repo.merge_resource_cache(roots)?; + let null = repo.object_hash().null(); + cache.set_resource( + base_id.map_or(null, Id::detach), + EntryKind::Blob, + base.as_bstr(), + ResourceKind::CommonAncestorOrBase, + &repo.objects, + )?; + cache.set_resource( + ours_id.map_or(null, Id::detach), + EntryKind::Blob, + ours.as_bstr(), + ResourceKind::CurrentOrOurs, + &repo.objects, + )?; + cache.set_resource( + theirs_id.map_or(null, Id::detach), + EntryKind::Blob, + theirs.as_bstr(), + ResourceKind::OtherOrTheirs, + &repo.objects, + )?; + + let mut options = repo.blob_merge_options()?; + if let Some(conflict) = conflict { + options.text.conflict = conflict; + options.resolve_binary_with = match conflict { + Conflict::Keep { .. } => None, + Conflict::ResolveWithOurs => Some(binary::ResolveWith::Ours), + Conflict::ResolveWithTheirs => Some(binary::ResolveWith::Theirs), + Conflict::ResolveWithUnion => None, + }; + } + let platform = cache.prepare_merge(&repo.objects, options)?; + let labels = gix::merge::blob::builtin_driver::text::Labels { + ancestor: Some(base.as_bstr()), + current: Some(ours.as_bstr()), + other: Some(theirs.as_bstr()), + }; + let mut buf = repo.empty_reusable_buffer(); + let (pick, resolution) = platform.merge(&mut buf, labels, repo.command_context()?)?; + let buf = platform.buffer_by_pick(pick).unwrap_or(&buf); + out.write_all(buf)?; + + if resolution == Resolution::Conflict { + bail!("File conflicted") + } + Ok(()) +} + +fn worktree_roots( + base: Option>, + ours: Option>, + theirs: Option>, + workdir: Option<&Path>, +) -> anyhow::Result { + let roots = if base.is_none() || ours.is_none() || theirs.is_none() { + let workdir = workdir.context("A workdir is required if one of the bases are provided as path.")?; + gix::merge::blob::pipeline::WorktreeRoots { + current_root: ours.is_none().then(|| workdir.to_owned()), + other_root: theirs.is_none().then(|| workdir.to_owned()), + common_ancestor_root: base.is_none().then(|| workdir.to_owned()), + } + } else { + WorktreeRoots::default() + }; + Ok(roots) +} diff --git a/gitoxide-core/src/repository/mod.rs b/gitoxide-core/src/repository/mod.rs index f9804f8a67c..f3ceeb8d86d 100644 --- a/gitoxide-core/src/repository/mod.rs +++ b/gitoxide-core/src/repository/mod.rs @@ -46,6 +46,7 @@ pub mod index; pub mod mailmap; mod merge_base; pub use merge_base::merge_base; +pub mod merge; pub mod odb; pub mod remote; pub mod revision; diff --git a/gix/src/repository/merge.rs b/gix/src/repository/merge.rs index ff3c2537525..6b038489b02 100644 --- a/gix/src/repository/merge.rs +++ b/gix/src/repository/merge.rs @@ -61,7 +61,7 @@ impl Repository { is_virtual_ancestor: false, resolve_binary_with: None, text: gix_merge::blob::builtin_driver::text::Options { - diff_algorithm: self.config.diff_algorithm()?, + diff_algorithm: self.diff_algorithm()?, conflict: text::Conflict::Keep { style: self .config diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index bb0c00c8953..bf6d089c866 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -14,6 +14,7 @@ use gitoxide_core as core; use gitoxide_core::{pack::verify, repository::PathsOrPatterns}; use gix::bstr::{io::BufReadExt, BString}; +use crate::plumbing::options::merge; use crate::plumbing::{ options::{ attributes, commit, commitgraph, config, credential, exclude, free, fsck, index, mailmap, odb, revision, tree, @@ -141,6 +142,42 @@ pub fn main() -> Result<()> { } match cmd { + Subcommands::Merge(merge::Platform { cmd }) => match cmd { + merge::SubCommands::File { + resolve_with, + ours, + base, + theirs, + } => prepare_and_run( + "merge-file", + trace, + verbose, + progress, + progress_keep_open, + None, + move |_progress, out, _err| { + core::repository::merge::file( + repository(Mode::Lenient)?, + out, + format, + resolve_with.map(|c| match c { + merge::ResolveWith::Union => { + gix::merge::blob::builtin_driver::text::Conflict::ResolveWithUnion + } + merge::ResolveWith::Ours => { + gix::merge::blob::builtin_driver::text::Conflict::ResolveWithOurs + } + merge::ResolveWith::Theirs => { + gix::merge::blob::builtin_driver::text::Conflict::ResolveWithTheirs + } + }), + base, + ours, + theirs, + ) + }, + ), + }, Subcommands::MergeBase(crate::plumbing::options::merge_base::Command { first, others }) => prepare_and_run( "merge-base", trace, diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index 6265cf959f8..4b1c95ecd4f 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -139,6 +139,7 @@ pub enum Subcommands { #[cfg(feature = "gitoxide-core-tools-corpus")] Corpus(corpus::Platform), MergeBase(merge_base::Command), + Merge(merge::Platform), Worktree(worktree::Platform), /// Subcommands that need no git repository to run. #[clap(subcommand)] @@ -337,6 +338,47 @@ pub mod corpus { } } +pub mod merge { + use gix::bstr::BString; + + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] + pub enum ResolveWith { + /// Use ours then theirs in case of conflict. + Union, + /// Use only ours in case of conflict. + Ours, + /// Use only theirs in case of conflict. + Theirs, + } + + #[derive(Debug, clap::Parser)] + #[command(about = "perform merges of various kinds")] + pub struct Platform { + #[clap(subcommand)] + pub cmd: SubCommands, + } + + #[derive(Debug, clap::Subcommand)] + pub enum SubCommands { + /// Merge a file by specifying ours, base and theirs. + File { + /// Decide how to resolve conflicts. If unset, write conflict markers and fail. + #[clap(long, short = 'c')] + resolve_with: Option, + + /// A path or revspec to our file + #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)] + ours: BString, + /// A path or revspec to the base for both ours and theirs + #[clap(value_name = "BASE", value_parser = crate::shared::AsBString)] + base: BString, + /// A path or revspec to their file + #[clap(value_name = "OURS", value_parser = crate::shared::AsBString)] + theirs: BString, + }, + } +} + pub mod config { use gix::bstr::BString; From 90399698b87019d115a86897d3eea5d75da30745 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 30 Sep 2024 21:59:27 +0200 Subject: [PATCH 7/7] adjust journey tests to deal with merge command --- .../remote/refs/remote ref-list-no-networking-in-small-failure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshots/plumbing/repository/remote/refs/remote ref-list-no-networking-in-small-failure b/tests/snapshots/plumbing/repository/remote/refs/remote ref-list-no-networking-in-small-failure index d3694f4acba..247ede986e1 100644 --- a/tests/snapshots/plumbing/repository/remote/refs/remote ref-list-no-networking-in-small-failure +++ b/tests/snapshots/plumbing/repository/remote/refs/remote ref-list-no-networking-in-small-failure @@ -1,6 +1,6 @@ error: unrecognized subcommand 'remote' - tip: some similar subcommands exist: 'r', 'tree', 'free' + tip: some similar subcommands exist: 'r', 'merge', 'tree', 'free' Usage: gix [OPTIONS]