Skip to content

Commit 5ffccd2

Browse files
authored
Merge pull request #1611 from Byron/merge
octopus-merge (part 3.5: gix-api and CLI)
2 parents 2261de4 + 9039969 commit 5ffccd2

File tree

24 files changed

+552
-31
lines changed

24 files changed

+552
-31
lines changed

Diff for: Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: 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"] }

Diff for: gitoxide-core/src/repository/merge.rs

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

Diff for: 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;

Diff for: gix-merge/src/blob/builtin_driver/text/mod.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ pub struct Options {
6666
/// Determine of the diff will be performed.
6767
/// Defaults to [`imara_diff::Algorithm::Myers`].
6868
pub diff_algorithm: imara_diff::Algorithm,
69-
/// Decide what to do to automatically resolve conflicts, or to keep them
70-
/// If `None`, add conflict markers according to `conflict_style` and `marker_size`.
69+
/// Decide what to do to automatically resolve conflicts, or to keep them.
7170
pub conflict: Conflict,
7271
}
7372

Diff for: gix-merge/src/blob/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ pub struct Platform {
158158
/// Pre-configured attributes to obtain additional merge-related information.
159159
attrs: gix_filter::attributes::search::Outcome,
160160
/// The way we convert resources into mergeable states.
161-
filter_mode: pipeline::Mode,
161+
pub filter_mode: pipeline::Mode,
162162
}
163163

164164
/// The product of a [`prepare_merge()`](Platform::prepare_merge()) call to finally

Diff for: gix/Cargo.toml

+6-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ extras = [
6464
"credentials",
6565
"interrupt",
6666
"status",
67-
"dirwalk"
67+
"dirwalk",
68+
"blob-merge"
6869
]
6970

7071
## A collection of features that need a larger MSRV, and thus are disabled by default.
@@ -137,6 +138,9 @@ revparse-regex = ["regex", "revision"]
137138
## which relies on line-by-line diffs in some cases.
138139
blob-diff = ["gix-diff/blob", "attributes"]
139140

141+
## Add functions to specifically merge files, using the standard three-way merge that git offers.
142+
blob-merge = ["dep:gix-merge", "gix-merge/blob", "attributes"]
143+
140144
## Make it possible to turn a tree into a stream of bytes, which can be decoded to entries and turned into various other formats.
141145
worktree-stream = ["gix-worktree-stream", "attributes"]
142146

@@ -337,6 +341,7 @@ gix-path = { version = "^0.10.11", path = "../gix-path" }
337341
gix-url = { version = "^0.27.5", path = "../gix-url" }
338342
gix-traverse = { version = "^0.41.0", path = "../gix-traverse" }
339343
gix-diff = { version = "^0.46.0", path = "../gix-diff", default-features = false }
344+
gix-merge = { version = "^0.0.0", path = "../gix-merge", default-features = false, optional = true }
340345
gix-mailmap = { version = "^0.24.0", path = "../gix-mailmap", optional = true }
341346
gix-features = { version = "^0.38.2", path = "../gix-features", features = [
342347
"progress",

Diff for: gix/src/config/cache/access.rs

+45
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,51 @@ impl Cache {
100100
Ok(out)
101101
}
102102

103+
#[cfg(feature = "blob-merge")]
104+
pub(crate) fn merge_drivers(&self) -> Result<Vec<gix_merge::blob::Driver>, config::merge::drivers::Error> {
105+
let mut out = Vec::<gix_merge::blob::Driver>::new();
106+
for section in self
107+
.resolved
108+
.sections_by_name("merge")
109+
.into_iter()
110+
.flatten()
111+
.filter(|s| (self.filter_config_section)(s.meta()))
112+
{
113+
let Some(name) = section.header().subsection_name().filter(|n| !n.is_empty()) else {
114+
continue;
115+
};
116+
117+
let driver = match out.iter_mut().find(|d| d.name == name) {
118+
Some(existing) => existing,
119+
None => {
120+
out.push(gix_merge::blob::Driver {
121+
name: name.into(),
122+
display_name: name.into(),
123+
..Default::default()
124+
});
125+
out.last_mut().expect("just pushed")
126+
}
127+
};
128+
129+
if let Some(command) = section.value(config::tree::Merge::DRIVER_COMMAND.name) {
130+
driver.command = command.into_owned();
131+
}
132+
if let Some(recursive_name) = section.value(config::tree::Merge::DRIVER_RECURSIVE.name) {
133+
driver.recursive = Some(recursive_name.into_owned());
134+
}
135+
}
136+
Ok(out)
137+
}
138+
139+
#[cfg(feature = "blob-merge")]
140+
pub(crate) fn merge_pipeline_options(
141+
&self,
142+
) -> Result<gix_merge::blob::pipeline::Options, config::merge::pipeline_options::Error> {
143+
Ok(gix_merge::blob::pipeline::Options {
144+
large_file_threshold_bytes: self.big_file_threshold()?,
145+
})
146+
}
147+
103148
#[cfg(feature = "blob-diff")]
104149
pub(crate) fn diff_pipeline_options(
105150
&self,

Diff for: gix/src/config/mod.rs

+25
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,31 @@ pub enum Error {
109109
},
110110
}
111111

112+
///
113+
pub mod merge {
114+
///
115+
pub mod pipeline_options {
116+
/// The error produced when obtaining options needed to fill in [gix_merge::blob::pipeline::Options].
117+
#[derive(Debug, thiserror::Error)]
118+
#[allow(missing_docs)]
119+
pub enum Error {
120+
#[error(transparent)]
121+
BigFileThreshold(#[from] crate::config::unsigned_integer::Error),
122+
}
123+
}
124+
125+
///
126+
pub mod drivers {
127+
/// The error produced when obtaining a list of [Drivers](gix_merge::blob::Driver).
128+
#[derive(Debug, thiserror::Error)]
129+
#[allow(missing_docs)]
130+
pub enum Error {
131+
#[error(transparent)]
132+
ConfigBoolean(#[from] crate::config::boolean::Error),
133+
}
134+
}
135+
}
136+
112137
///
113138
pub mod diff {
114139
///

Diff for: gix/src/config/tree/mod.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ pub(crate) mod root {
4747
pub const INIT: sections::Init = sections::Init;
4848
/// The `mailmap` section.
4949
pub const MAILMAP: sections::Mailmap = sections::Mailmap;
50+
/// The `merge` section.
51+
pub const MERGE: sections::Merge = sections::Merge;
5052
/// The `pack` section.
5153
pub const PACK: sections::Pack = sections::Pack;
5254
/// The `protocol` section.
@@ -86,6 +88,7 @@ pub(crate) mod root {
8688
&Self::INDEX,
8789
&Self::INIT,
8890
&Self::MAILMAP,
91+
&Self::MERGE,
8992
&Self::PACK,
9093
&Self::PROTOCOL,
9194
&Self::PUSH,
@@ -105,7 +108,7 @@ mod sections;
105108
pub use sections::{
106109
branch, checkout, core, credential, extensions, fetch, gitoxide, http, index, protocol, push, remote, ssh, Author,
107110
Branch, Checkout, Clone, Committer, Core, Credential, Extensions, Fetch, Gitoxide, Http, Index, Init, Mailmap,
108-
Pack, Protocol, Push, Remote, Safe, Ssh, Url, User,
111+
Merge, Pack, Protocol, Push, Remote, Safe, Ssh, Url, User,
109112
};
110113
#[cfg(feature = "blob-diff")]
111114
pub use sections::{diff, Diff};

Diff for: gix/src/config/tree/sections/merge.rs

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use crate::config;
2+
use crate::config::tree::SubSectionRequirement;
3+
use crate::config::{
4+
tree::{keys, Key, Merge, Section},
5+
Tree,
6+
};
7+
8+
impl Merge {
9+
/// The `merge.renormalize` key
10+
pub const RENORMALIZE: keys::Boolean = keys::Boolean::new_boolean("renormalize", &Tree::MERGE);
11+
/// The `merge.default` key
12+
pub const DEFAULT: keys::String = keys::String::new_string("default", &Tree::MERGE);
13+
/// The `merge.<driver>.name` key.
14+
pub const DRIVER_NAME: keys::String = keys::String::new_string("name", &config::Tree::MERGE)
15+
.with_subsection_requirement(Some(SubSectionRequirement::Parameter("driver")));
16+
/// The `merge.<driver>.driver` key.
17+
pub const DRIVER_COMMAND: keys::Program = keys::Program::new_program("driver", &config::Tree::MERGE)
18+
.with_subsection_requirement(Some(SubSectionRequirement::Parameter("driver")));
19+
/// The `merge.<driver>.recursive` key.
20+
pub const DRIVER_RECURSIVE: keys::String = keys::String::new_string("recursive", &config::Tree::MERGE)
21+
.with_subsection_requirement(Some(SubSectionRequirement::Parameter("driver")));
22+
/// The `merge.conflictStyle` key.
23+
#[cfg(feature = "blob-merge")]
24+
pub const CONFLICT_STYLE: ConflictStyle =
25+
ConflictStyle::new_with_validate("conflictStyle", &config::Tree::MERGE, validate::ConflictStyle);
26+
}
27+
28+
impl Section for Merge {
29+
fn name(&self) -> &str {
30+
"merge"
31+
}
32+
33+
fn keys(&self) -> &[&dyn Key] {
34+
&[
35+
&Self::RENORMALIZE,
36+
&Self::DEFAULT,
37+
&Self::DRIVER_NAME,
38+
&Self::DRIVER_COMMAND,
39+
&Self::DRIVER_RECURSIVE,
40+
]
41+
}
42+
}
43+
44+
/// The `merge.conflictStyle` key.
45+
#[cfg(feature = "blob-merge")]
46+
pub type ConflictStyle = keys::Any<validate::ConflictStyle>;
47+
48+
#[cfg(feature = "blob-merge")]
49+
mod conflict_style {
50+
use crate::{bstr::BStr, config, config::tree::sections::merge::ConflictStyle};
51+
use gix_merge::blob::builtin_driver::text;
52+
use std::borrow::Cow;
53+
54+
impl ConflictStyle {
55+
/// Derive the diff algorithm identified by `name`, case-insensitively.
56+
pub fn try_into_conflict_style(
57+
&'static self,
58+
name: Cow<'_, BStr>,
59+
) -> Result<text::ConflictStyle, config::key::GenericErrorWithValue> {
60+
let style = if name.as_ref() == "merge" {
61+
text::ConflictStyle::Merge
62+
} else if name.as_ref() == "diff3" {
63+
text::ConflictStyle::Diff3
64+
} else if name.as_ref() == "zdiff3" {
65+
text::ConflictStyle::ZealousDiff3
66+
} else {
67+
return Err(config::key::GenericErrorWithValue::from_value(self, name.into_owned()));
68+
};
69+
Ok(style)
70+
}
71+
}
72+
}
73+
74+
#[cfg(feature = "blob-merge")]
75+
mod validate {
76+
use crate::{
77+
bstr::BStr,
78+
config::tree::{keys, Merge},
79+
};
80+
81+
pub struct ConflictStyle;
82+
impl keys::Validate for ConflictStyle {
83+
fn validate(&self, value: &BStr) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
84+
Merge::CONFLICT_STYLE.try_into_conflict_style(value.into())?;
85+
Ok(())
86+
}
87+
}
88+
}

Diff for: gix/src/config/tree/sections/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ mod init;
7676
pub struct Mailmap;
7777
mod mailmap;
7878

79+
#[derive(Copy, Clone, Default)]
80+
pub struct Merge;
81+
mod merge;
82+
7983
/// The `pack` top-level section.
8084
#[derive(Copy, Clone, Default)]
8185
pub struct Pack;

0 commit comments

Comments
 (0)