Skip to content

Commit 2125886

Browse files
committed
feat: add gix merge tree to merge trees similarly to git merge-tree.
1 parent 94d77fd commit 2125886

File tree

5 files changed

+132
-11
lines changed

5 files changed

+132
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod file;
2+
pub use file::file;
3+
4+
mod tree;
5+
pub use tree::tree;
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use crate::OutputFormat;
2+
use anyhow::{anyhow, bail, Context};
3+
use gix::bstr::BString;
4+
use gix::bstr::ByteSlice;
5+
use gix::merge::blob::builtin_driver::binary;
6+
use gix::merge::blob::builtin_driver::text::Conflict;
7+
use gix::merge::tree::UnresolvedConflict;
8+
use gix::prelude::Write;
9+
10+
#[allow(clippy::too_many_arguments)]
11+
pub fn tree(
12+
mut repo: gix::Repository,
13+
out: &mut dyn std::io::Write,
14+
err: &mut dyn std::io::Write,
15+
format: OutputFormat,
16+
resolve_content_merge: Option<gix::merge::blob::builtin_driver::text::Conflict>,
17+
base: BString,
18+
ours: BString,
19+
theirs: BString,
20+
) -> anyhow::Result<()> {
21+
if format != OutputFormat::Human {
22+
bail!("JSON output isn't implemented yet");
23+
}
24+
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
25+
let (base_ref, base_id) = refname_and_tree(&repo, base)?;
26+
let (ours_ref, ours_id) = refname_and_tree(&repo, ours)?;
27+
let (theirs_ref, theirs_id) = refname_and_tree(&repo, theirs)?;
28+
29+
let mut options = repo.tree_merge_options()?;
30+
if let Some(resolve) = resolve_content_merge {
31+
options.blob_merge.text.conflict = resolve;
32+
options.blob_merge.resolve_binary_with = match resolve {
33+
Conflict::Keep { .. } => None,
34+
Conflict::ResolveWithOurs => Some(binary::ResolveWith::Ours),
35+
Conflict::ResolveWithTheirs => Some(binary::ResolveWith::Theirs),
36+
Conflict::ResolveWithUnion => None,
37+
};
38+
}
39+
40+
let labels = gix::merge::blob::builtin_driver::text::Labels {
41+
ancestor: base_ref.as_ref().map(|n| n.as_bstr()),
42+
current: ours_ref.as_ref().map(|n| n.as_bstr()),
43+
other: theirs_ref.as_ref().map(|n| n.as_bstr()),
44+
};
45+
let mut res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?;
46+
let tree_id = res
47+
.tree
48+
.write(|tree| repo.objects.write(tree))
49+
.map_err(|err| anyhow!("{err}"))?;
50+
writeln!(out, "{tree_id}")?;
51+
52+
if !res.conflicts.is_empty() {
53+
writeln!(err, "{} possibly resolved conflicts", res.conflicts.len())?;
54+
}
55+
if res.has_unresolved_conflicts(UnresolvedConflict::Renames) {
56+
bail!("Tree conflicted")
57+
}
58+
Ok(())
59+
}
60+
61+
fn refname_and_tree(
62+
repo: &gix::Repository,
63+
revspec: BString,
64+
) -> anyhow::Result<(Option<BString>, gix::hash::ObjectId)> {
65+
let spec = repo.rev_parse(revspec.as_bstr())?;
66+
let tree_id = spec
67+
.single()
68+
.context("Expected revspec to expand to a single rev only")?
69+
.object()?
70+
.peel_to_tree()?
71+
.id;
72+
let refname = spec.first_reference().map(|r| r.name.shorten().as_bstr().to_owned());
73+
Ok((refname, tree_id))
74+
}

src/plumbing/main.rs

+26-11
Original file line numberDiff line numberDiff line change
@@ -164,17 +164,32 @@ pub fn main() -> Result<()> {
164164
repository(Mode::Lenient)?,
165165
out,
166166
format,
167-
resolve_with.map(|c| match c {
168-
merge::ResolveWith::Union => {
169-
gix::merge::blob::builtin_driver::text::Conflict::ResolveWithUnion
170-
}
171-
merge::ResolveWith::Ours => {
172-
gix::merge::blob::builtin_driver::text::Conflict::ResolveWithOurs
173-
}
174-
merge::ResolveWith::Theirs => {
175-
gix::merge::blob::builtin_driver::text::Conflict::ResolveWithTheirs
176-
}
177-
}),
167+
resolve_with.map(Into::into),
168+
base,
169+
ours,
170+
theirs,
171+
)
172+
},
173+
),
174+
merge::SubCommands::Tree {
175+
resolve_content_with,
176+
ours,
177+
base,
178+
theirs,
179+
} => prepare_and_run(
180+
"merge-tree",
181+
trace,
182+
verbose,
183+
progress,
184+
progress_keep_open,
185+
None,
186+
move |_progress, out, err| {
187+
core::repository::merge::tree(
188+
repository(Mode::Lenient)?,
189+
out,
190+
err,
191+
format,
192+
resolve_content_with.map(Into::into),
178193
base,
179194
ours,
180195
theirs,

src/plumbing/options/mod.rs

+27
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,16 @@ pub mod merge {
357357
Theirs,
358358
}
359359

360+
impl From<ResolveWith> for gix::merge::blob::builtin_driver::text::Conflict {
361+
fn from(value: ResolveWith) -> Self {
362+
match value {
363+
ResolveWith::Union => gix::merge::blob::builtin_driver::text::Conflict::ResolveWithUnion,
364+
ResolveWith::Ours => gix::merge::blob::builtin_driver::text::Conflict::ResolveWithOurs,
365+
ResolveWith::Theirs => gix::merge::blob::builtin_driver::text::Conflict::ResolveWithTheirs,
366+
}
367+
}
368+
}
369+
360370
#[derive(Debug, clap::Parser)]
361371
#[command(about = "perform merges of various kinds")]
362372
pub struct Platform {
@@ -382,6 +392,23 @@ pub mod merge {
382392
#[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]
383393
theirs: BString,
384394
},
395+
396+
/// Merge a tree by specifying ours, base and theirs, writing it to the object database.
397+
Tree {
398+
/// Decide how to resolve content conflicts. If unset, write conflict markers and fail.
399+
#[clap(long, short = 'c')]
400+
resolve_content_with: Option<ResolveWith>,
401+
402+
/// A revspec to our treeish.
403+
#[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]
404+
ours: BString,
405+
/// A revspec to the base as treeish for both ours and theirs.
406+
#[clap(value_name = "BASE", value_parser = crate::shared::AsBString)]
407+
base: BString,
408+
/// A revspec to their treeish.
409+
#[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]
410+
theirs: BString,
411+
},
385412
}
386413
}
387414

0 commit comments

Comments
 (0)