Skip to content

Commit

Permalink
feat: add gix merge tree to merge trees similarly to git merge-tree.
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Nov 2, 2024
1 parent d1ac584 commit 84707c2
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 11 deletions.
File renamed without changes.
5 changes: 5 additions & 0 deletions gitoxide-core/src/repository/merge/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod file;
pub use file::file;

pub mod tree;
pub use tree::function::tree;
112 changes: 112 additions & 0 deletions gitoxide-core/src/repository/merge/tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use crate::OutputFormat;

pub struct Options {
pub format: OutputFormat,
pub resolve_content_merge: Option<gix::merge::blob::builtin_driver::text::Conflict>,
pub in_memory: bool,
}

pub(super) mod function {

use crate::OutputFormat;
use anyhow::{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::tree::UnresolvedConflict;
use gix::prelude::Write;

use super::Options;

#[allow(clippy::too_many_arguments)]
pub fn tree(
mut repo: gix::Repository,
out: &mut dyn std::io::Write,
err: &mut dyn std::io::Write,
base: BString,
ours: BString,
theirs: BString,
Options {
format,
resolve_content_merge,
in_memory,
}: Options,
) -> anyhow::Result<()> {
if format != OutputFormat::Human {
bail!("JSON output isn't implemented yet");
}
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
if in_memory {
repo.objects.enable_object_memory();
}
let (base_ref, base_id) = refname_and_tree(&repo, base)?;
let (ours_ref, ours_id) = refname_and_tree(&repo, ours)?;
let (theirs_ref, theirs_id) = refname_and_tree(&repo, theirs)?;

let mut options = repo.tree_merge_options()?;
if let Some(resolve) = resolve_content_merge {
options.blob_merge.text.conflict = resolve;
options.blob_merge.resolve_binary_with = match resolve {
Conflict::Keep { .. } => None,
Conflict::ResolveWithOurs => Some(binary::ResolveWith::Ours),
Conflict::ResolveWithTheirs => Some(binary::ResolveWith::Theirs),
Conflict::ResolveWithUnion => None,
};
}

let base_id_str = base_id.to_string();
let ours_id_str = ours_id.to_string();
let theirs_id_str = theirs_id.to_string();
let labels = gix::merge::blob::builtin_driver::text::Labels {
ancestor: base_ref
.as_ref()
.map_or(base_id_str.as_str().into(), |n| n.as_bstr())
.into(),
current: ours_ref
.as_ref()
.map_or(ours_id_str.as_str().into(), |n| n.as_bstr())
.into(),
other: theirs_ref
.as_ref()
.map_or(theirs_id_str.as_str().into(), |n| n.as_bstr())
.into(),
};
let mut res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?;
{
let _span = gix::trace::detail!("Writing merged tree");
let mut written = 0;
let tree_id = res
.tree
.write(|tree| {
written += 1;
repo.write(tree)
})
.map_err(|err| anyhow!("{err}"))?;
writeln!(out, "{tree_id} (wrote {written} trees)")?;
}

if !res.conflicts.is_empty() {
writeln!(err, "{} possibly resolved conflicts", res.conflicts.len())?;
}
if res.has_unresolved_conflicts(UnresolvedConflict::Renames) {
bail!("Tree conflicted")
}
Ok(())
}

fn refname_and_tree(
repo: &gix::Repository,
revspec: BString,
) -> anyhow::Result<(Option<BString>, gix::hash::ObjectId)> {
let spec = repo.rev_parse(revspec.as_bstr())?;
let tree_id = spec
.single()
.context("Expected revspec to expand to a single rev only")?
.object()?
.peel_to_tree()?
.id;
let refname = spec.first_reference().map(|r| r.name.shorten().as_bstr().to_owned());
Ok((refname, tree_id))
}
}
41 changes: 30 additions & 11 deletions src/plumbing/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,23 +164,42 @@ pub fn main() -> Result<()> {
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
}
}),
resolve_with.map(Into::into),
base,
ours,
theirs,
)
},
),
merge::SubCommands::Tree {
in_memory,
resolve_content_with,
ours,
base,
theirs,
} => prepare_and_run(
"merge-tree",
trace,
verbose,
progress,
progress_keep_open,
None,
move |_progress, out, err| {
core::repository::merge::tree(
repository(Mode::Lenient)?,
out,
err,
base,
ours,
theirs,
core::repository::merge::tree::Options {
format,
resolve_content_merge: resolve_content_with.map(Into::into),
in_memory,
},
)
},
),
},
Subcommands::MergeBase(crate::plumbing::options::merge_base::Command { first, others }) => prepare_and_run(
"merge-base",
Expand Down
32 changes: 32 additions & 0 deletions src/plumbing/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,16 @@ pub mod merge {
Theirs,
}

impl From<ResolveWith> for gix::merge::blob::builtin_driver::text::Conflict {
fn from(value: ResolveWith) -> Self {
match value {
ResolveWith::Union => gix::merge::blob::builtin_driver::text::Conflict::ResolveWithUnion,
ResolveWith::Ours => gix::merge::blob::builtin_driver::text::Conflict::ResolveWithOurs,
ResolveWith::Theirs => gix::merge::blob::builtin_driver::text::Conflict::ResolveWithTheirs,
}
}
}

#[derive(Debug, clap::Parser)]
#[command(about = "perform merges of various kinds")]
pub struct Platform {
Expand All @@ -382,6 +392,28 @@ pub mod merge {
#[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]
theirs: BString,
},

/// Merge a tree by specifying ours, base and theirs, writing it to the object database.
Tree {
/// Keep all objects to be written in memory to avoid any disk IO.
///
/// Note that the resulting tree won't be available or inspectable.
#[clap(long, short = 'm')]
in_memory: bool,
/// Decide how to resolve content conflicts. If unset, write conflict markers and fail.
#[clap(long, short = 'c')]
resolve_content_with: Option<ResolveWith>,

/// A revspec to our treeish.
#[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]
ours: BString,
/// A revspec to the base as treeish for both ours and theirs.
#[clap(value_name = "BASE", value_parser = crate::shared::AsBString)]
base: BString,
/// A revspec to their treeish.
#[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]
theirs: BString,
},
}
}

Expand Down

0 comments on commit 84707c2

Please sign in to comment.