Skip to content

Commit

Permalink
Merge pull request #1611 from Byron/merge
Browse files Browse the repository at this point in the history
octopus-merge (part 3.5: gix-api and CLI)
  • Loading branch information
Byron authored Sep 30, 2024
2 parents 2261de4 + 9039969 commit 5ffccd2
Show file tree
Hide file tree
Showing 24 changed files with 552 additions and 31 deletions.
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.

2 changes: 1 addition & 1 deletion gitoxide-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
113 changes: 113 additions & 0 deletions gitoxide-core/src/repository/merge.rs
Original file line number Diff line number Diff line change
@@ -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<gix::merge::blob::builtin_driver::text::Conflict>,
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<gix::Id<'_>>,
ours: Option<gix::Id<'_>>,
theirs: Option<gix::Id<'_>>,
workdir: Option<&Path>,
) -> anyhow::Result<gix::merge::blob::pipeline::WorktreeRoots> {
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)
}
1 change: 1 addition & 0 deletions gitoxide-core/src/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 1 addition & 2 deletions gix-merge/src/blob/builtin_driver/text/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
2 changes: 1 addition & 1 deletion gix-merge/src/blob/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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();
}
if let Some(recursive_name) = section.value(config::tree::Merge::DRIVER_RECURSIVE.name) {
driver.recursive = Some(recursive_name.into_owned());
}
}
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
5 changes: 4 additions & 1 deletion gix/src/config/tree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -86,6 +88,7 @@ pub(crate) mod root {
&Self::INDEX,
&Self::INIT,
&Self::MAILMAP,
&Self::MERGE,
&Self::PACK,
&Self::PROTOCOL,
&Self::PUSH,
Expand All @@ -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};
Expand Down
88 changes: 88 additions & 0 deletions gix/src/config/tree/sections/merge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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.<driver>.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>.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.<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 {
fn name(&self) -> &str {
"merge"
}

fn keys(&self) -> &[&dyn Key] {
&[
&Self::RENORMALIZE,
&Self::DEFAULT,
&Self::DRIVER_NAME,
&Self::DRIVER_COMMAND,
&Self::DRIVER_RECURSIVE,
]
}
}

/// 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(())
}
}
}
4 changes: 4 additions & 0 deletions gix/src/config/tree/sections/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 5ffccd2

Please sign in to comment.