Skip to content

Commit 6fdce59

Browse files
committed
feat: add gix merge-file with similar features as git merge-file
1 parent aef949c commit 6fdce59

File tree

6 files changed

+145
-2
lines changed

6 files changed

+145
-2
lines changed

gitoxide-core/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ serde = ["gix/serde", "dep:serde_json", "dep:serde", "bytesize/serde"]
4949

5050
[dependencies]
5151
# deselect everything else (like "performance") as this should be controllable by the parent application.
52-
gix = { version = "^0.66.0", path = "../gix", default-features = false, features = ["blob-diff", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status", "dirwalk"] }
52+
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"] }
5353
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"] }
5454
gix-transport-configuration-only = { package = "gix-transport", version = "^0.42.3", path = "../gix-transport", default-features = false }
5555
gix-archive-for-configuration-only = { package = "gix-archive", version = "^0.15.0", path = "../gix-archive", optional = true, features = ["tar", "tar_gz"] }

gitoxide-core/src/repository/merge.rs

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use crate::OutputFormat;
2+
use anyhow::{bail, Context};
3+
use gix::bstr::BString;
4+
use gix::bstr::ByteSlice;
5+
use gix::merge::blob::pipeline::WorktreeRoots;
6+
use gix::merge::blob::{Resolution, ResourceKind};
7+
use gix::object::tree::EntryKind;
8+
use gix::Id;
9+
use std::path::Path;
10+
11+
pub fn file(
12+
repo: gix::Repository,
13+
out: &mut dyn std::io::Write,
14+
format: OutputFormat,
15+
base: BString,
16+
ours: BString,
17+
theirs: BString,
18+
) -> anyhow::Result<()> {
19+
if format != OutputFormat::Human {
20+
bail!("JSON output isn't implemented yet");
21+
}
22+
let index = &repo.index_or_load_from_head()?;
23+
let specs = repo.pathspec(
24+
false,
25+
[base, ours, theirs],
26+
true,
27+
index,
28+
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping.adjust_for_bare(repo.is_bare()),
29+
)?;
30+
// TODO: there should be a way to normalize paths without going through patterns, at least in this case maybe?
31+
// `Search` actually sorts patterns by excluding or not, all that can lead to strange results.
32+
let mut patterns = specs.search().patterns().map(|p| p.path().to_owned());
33+
let base = patterns.next().unwrap();
34+
let ours = patterns.next().unwrap();
35+
let theirs = patterns.next().unwrap();
36+
37+
let base_id = repo.rev_parse_single(base.as_bstr()).ok();
38+
let ours_id = repo.rev_parse_single(ours.as_bstr()).ok();
39+
let theirs_id = repo.rev_parse_single(theirs.as_bstr()).ok();
40+
let roots = worktree_roots(base_id, ours_id, theirs_id, repo.work_dir())?;
41+
42+
let mut cache = repo.merge_resource_cache(roots)?;
43+
let null = repo.object_hash().null();
44+
cache.set_resource(
45+
base_id.map_or(null, Id::detach),
46+
EntryKind::Blob,
47+
base.as_bstr(),
48+
ResourceKind::CommonAncestorOrBase,
49+
&repo.objects,
50+
)?;
51+
cache.set_resource(
52+
ours_id.map_or(null, Id::detach),
53+
EntryKind::Blob,
54+
ours.as_bstr(),
55+
ResourceKind::CurrentOrOurs,
56+
&repo.objects,
57+
)?;
58+
cache.set_resource(
59+
theirs_id.map_or(null, Id::detach),
60+
EntryKind::Blob,
61+
theirs.as_bstr(),
62+
ResourceKind::OtherOrTheirs,
63+
&repo.objects,
64+
)?;
65+
66+
let platform = cache.prepare_merge(&repo.objects, repo.blob_merge_options()?)?;
67+
let labels = gix::merge::blob::builtin_driver::text::Labels {
68+
ancestor: Some(base.as_bstr()),
69+
current: Some(ours.as_bstr()),
70+
other: Some(theirs.as_bstr()),
71+
};
72+
let mut buf = repo.empty_reusable_buffer();
73+
let (pick, resolution) = platform.merge(&mut buf, labels, repo.command_context()?)?;
74+
let buf = platform.buffer_by_pick(pick).unwrap_or(&buf);
75+
out.write_all(buf)?;
76+
77+
if resolution == Resolution::Conflict {
78+
bail!("File conflicted")
79+
}
80+
Ok(())
81+
}
82+
83+
fn worktree_roots(
84+
base: Option<gix::Id<'_>>,
85+
ours: Option<gix::Id<'_>>,
86+
theirs: Option<gix::Id<'_>>,
87+
workdir: Option<&Path>,
88+
) -> anyhow::Result<gix::merge::blob::pipeline::WorktreeRoots> {
89+
let roots = if base.is_none() || ours.is_none() || theirs.is_none() {
90+
let workdir = workdir.context("A workdir is required if one of the bases are provided as path.")?;
91+
gix::merge::blob::pipeline::WorktreeRoots {
92+
current_root: ours.is_none().then(|| workdir.to_owned()),
93+
other_root: theirs.is_none().then(|| workdir.to_owned()),
94+
common_ancestor_root: base.is_none().then(|| workdir.to_owned()),
95+
}
96+
} else {
97+
WorktreeRoots::default()
98+
};
99+
Ok(roots)
100+
}

gitoxide-core/src/repository/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub mod index;
4646
pub mod mailmap;
4747
mod merge_base;
4848
pub use merge_base::merge_base;
49+
pub mod merge;
4950
pub mod odb;
5051
pub mod remote;
5152
pub mod revision;

gix/src/repository/merge.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ impl Repository {
6161
is_virtual_ancestor: false,
6262
resolve_binary_with: None,
6363
text: gix_merge::blob::builtin_driver::text::Options {
64-
diff_algorithm: self.config.diff_algorithm()?,
64+
diff_algorithm: self.diff_algorithm()?,
6565
conflict: text::Conflict::Keep {
6666
style: self
6767
.config

src/plumbing/main.rs

+14
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use gitoxide_core as core;
1414
use gitoxide_core::{pack::verify, repository::PathsOrPatterns};
1515
use gix::bstr::{io::BufReadExt, BString};
1616

17+
use crate::plumbing::options::merge;
1718
use crate::plumbing::{
1819
options::{
1920
attributes, commit, commitgraph, config, credential, exclude, free, fsck, index, mailmap, odb, revision, tree,
@@ -141,6 +142,19 @@ pub fn main() -> Result<()> {
141142
}
142143

143144
match cmd {
145+
Subcommands::Merge(merge::Platform { cmd }) => match cmd {
146+
merge::SubCommands::File { ours, base, theirs } => prepare_and_run(
147+
"merge-file",
148+
trace,
149+
verbose,
150+
progress,
151+
progress_keep_open,
152+
None,
153+
move |_progress, out, _err| {
154+
core::repository::merge::file(repository(Mode::Lenient)?, out, format, base, ours, theirs)
155+
},
156+
),
157+
},
144158
Subcommands::MergeBase(crate::plumbing::options::merge_base::Command { first, others }) => prepare_and_run(
145159
"merge-base",
146160
trace,

src/plumbing/options/mod.rs

+28
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ pub enum Subcommands {
139139
#[cfg(feature = "gitoxide-core-tools-corpus")]
140140
Corpus(corpus::Platform),
141141
MergeBase(merge_base::Command),
142+
Merge(merge::Platform),
142143
Worktree(worktree::Platform),
143144
/// Subcommands that need no git repository to run.
144145
#[clap(subcommand)]
@@ -337,6 +338,33 @@ pub mod corpus {
337338
}
338339
}
339340

341+
pub mod merge {
342+
use gix::bstr::BString;
343+
344+
#[derive(Debug, clap::Parser)]
345+
#[command(about = "perform merges of various kinds")]
346+
pub struct Platform {
347+
#[clap(subcommand)]
348+
pub cmd: SubCommands,
349+
}
350+
351+
#[derive(Debug, clap::Subcommand)]
352+
pub enum SubCommands {
353+
/// Merge a file by specifying ours, base and theirs.
354+
File {
355+
/// A path or revspec to our file
356+
#[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]
357+
ours: BString,
358+
/// A path or revspec to the base for both ours and theirs
359+
#[clap(value_name = "BASE", value_parser = crate::shared::AsBString)]
360+
base: BString,
361+
/// A path or revspec to their file
362+
#[clap(value_name = "OURS", value_parser = crate::shared::AsBString)]
363+
theirs: BString,
364+
},
365+
}
366+
}
367+
340368
pub mod config {
341369
use gix::bstr::BString;
342370

0 commit comments

Comments
 (0)