Skip to content

Commit 84707c2

Browse files
committed
feat: add gix merge tree to merge trees similarly to git merge-tree.
1 parent d1ac584 commit 84707c2

File tree

5 files changed

+179
-11
lines changed

5 files changed

+179
-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+
pub mod tree;
5+
pub use tree::function::tree;
+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use crate::OutputFormat;
2+
3+
pub struct Options {
4+
pub format: OutputFormat,
5+
pub resolve_content_merge: Option<gix::merge::blob::builtin_driver::text::Conflict>,
6+
pub in_memory: bool,
7+
}
8+
9+
pub(super) mod function {
10+
11+
use crate::OutputFormat;
12+
use anyhow::{anyhow, bail, Context};
13+
use gix::bstr::BString;
14+
use gix::bstr::ByteSlice;
15+
use gix::merge::blob::builtin_driver::binary;
16+
use gix::merge::blob::builtin_driver::text::Conflict;
17+
use gix::merge::tree::UnresolvedConflict;
18+
use gix::prelude::Write;
19+
20+
use super::Options;
21+
22+
#[allow(clippy::too_many_arguments)]
23+
pub fn tree(
24+
mut repo: gix::Repository,
25+
out: &mut dyn std::io::Write,
26+
err: &mut dyn std::io::Write,
27+
base: BString,
28+
ours: BString,
29+
theirs: BString,
30+
Options {
31+
format,
32+
resolve_content_merge,
33+
in_memory,
34+
}: Options,
35+
) -> anyhow::Result<()> {
36+
if format != OutputFormat::Human {
37+
bail!("JSON output isn't implemented yet");
38+
}
39+
repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
40+
if in_memory {
41+
repo.objects.enable_object_memory();
42+
}
43+
let (base_ref, base_id) = refname_and_tree(&repo, base)?;
44+
let (ours_ref, ours_id) = refname_and_tree(&repo, ours)?;
45+
let (theirs_ref, theirs_id) = refname_and_tree(&repo, theirs)?;
46+
47+
let mut options = repo.tree_merge_options()?;
48+
if let Some(resolve) = resolve_content_merge {
49+
options.blob_merge.text.conflict = resolve;
50+
options.blob_merge.resolve_binary_with = match resolve {
51+
Conflict::Keep { .. } => None,
52+
Conflict::ResolveWithOurs => Some(binary::ResolveWith::Ours),
53+
Conflict::ResolveWithTheirs => Some(binary::ResolveWith::Theirs),
54+
Conflict::ResolveWithUnion => None,
55+
};
56+
}
57+
58+
let base_id_str = base_id.to_string();
59+
let ours_id_str = ours_id.to_string();
60+
let theirs_id_str = theirs_id.to_string();
61+
let labels = gix::merge::blob::builtin_driver::text::Labels {
62+
ancestor: base_ref
63+
.as_ref()
64+
.map_or(base_id_str.as_str().into(), |n| n.as_bstr())
65+
.into(),
66+
current: ours_ref
67+
.as_ref()
68+
.map_or(ours_id_str.as_str().into(), |n| n.as_bstr())
69+
.into(),
70+
other: theirs_ref
71+
.as_ref()
72+
.map_or(theirs_id_str.as_str().into(), |n| n.as_bstr())
73+
.into(),
74+
};
75+
let mut res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?;
76+
{
77+
let _span = gix::trace::detail!("Writing merged tree");
78+
let mut written = 0;
79+
let tree_id = res
80+
.tree
81+
.write(|tree| {
82+
written += 1;
83+
repo.write(tree)
84+
})
85+
.map_err(|err| anyhow!("{err}"))?;
86+
writeln!(out, "{tree_id} (wrote {written} trees)")?;
87+
}
88+
89+
if !res.conflicts.is_empty() {
90+
writeln!(err, "{} possibly resolved conflicts", res.conflicts.len())?;
91+
}
92+
if res.has_unresolved_conflicts(UnresolvedConflict::Renames) {
93+
bail!("Tree conflicted")
94+
}
95+
Ok(())
96+
}
97+
98+
fn refname_and_tree(
99+
repo: &gix::Repository,
100+
revspec: BString,
101+
) -> anyhow::Result<(Option<BString>, gix::hash::ObjectId)> {
102+
let spec = repo.rev_parse(revspec.as_bstr())?;
103+
let tree_id = spec
104+
.single()
105+
.context("Expected revspec to expand to a single rev only")?
106+
.object()?
107+
.peel_to_tree()?
108+
.id;
109+
let refname = spec.first_reference().map(|r| r.name.shorten().as_bstr().to_owned());
110+
Ok((refname, tree_id))
111+
}
112+
}

src/plumbing/main.rs

+30-11
Original file line numberDiff line numberDiff line change
@@ -164,23 +164,42 @@ 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),
178168
base,
179169
ours,
180170
theirs,
181171
)
182172
},
183173
),
174+
merge::SubCommands::Tree {
175+
in_memory,
176+
resolve_content_with,
177+
ours,
178+
base,
179+
theirs,
180+
} => prepare_and_run(
181+
"merge-tree",
182+
trace,
183+
verbose,
184+
progress,
185+
progress_keep_open,
186+
None,
187+
move |_progress, out, err| {
188+
core::repository::merge::tree(
189+
repository(Mode::Lenient)?,
190+
out,
191+
err,
192+
base,
193+
ours,
194+
theirs,
195+
core::repository::merge::tree::Options {
196+
format,
197+
resolve_content_merge: resolve_content_with.map(Into::into),
198+
in_memory,
199+
},
200+
)
201+
},
202+
),
184203
},
185204
Subcommands::MergeBase(crate::plumbing::options::merge_base::Command { first, others }) => prepare_and_run(
186205
"merge-base",

src/plumbing/options/mod.rs

+32
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,28 @@ 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+
/// Keep all objects to be written in memory to avoid any disk IO.
399+
///
400+
/// Note that the resulting tree won't be available or inspectable.
401+
#[clap(long, short = 'm')]
402+
in_memory: bool,
403+
/// Decide how to resolve content conflicts. If unset, write conflict markers and fail.
404+
#[clap(long, short = 'c')]
405+
resolve_content_with: Option<ResolveWith>,
406+
407+
/// A revspec to our treeish.
408+
#[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]
409+
ours: BString,
410+
/// A revspec to the base as treeish for both ours and theirs.
411+
#[clap(value_name = "BASE", value_parser = crate::shared::AsBString)]
412+
base: BString,
413+
/// A revspec to their treeish.
414+
#[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]
415+
theirs: BString,
416+
},
385417
}
386418
}
387419

0 commit comments

Comments
 (0)