Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

octopus-merge (part 3.5: gix-api and CLI) #1611

Merged
merged 7 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading