Skip to content

Commit 1a60c28

Browse files
committed
feat: add tree() and commit() merge support, *probably* en par with merge-ORT
Note that this judgement of quality is based on a limited amount of partially complex test, but it's likely that in practice there will be deviations of sorts.
1 parent 82d1ad2 commit 1a60c28

File tree

13 files changed

+2505
-16
lines changed

13 files changed

+2505
-16
lines changed

Cargo.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crate-status.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,10 +340,11 @@ Check out the [performance discussion][gix-diff-performance] as well.
340340

341341
* [x] three-way merge analysis of **blobs** with choice of how to resolve conflicts
342342
- [ ] choose how to resolve conflicts on the data-structure
343-
- [ ] produce a new blob based on data-structure containing possible resolutions
343+
- [x] produce a new blob based on data-structure containing possible resolutions
344344
- [x] `merge` style
345345
- [x] `diff3` style
346346
- [x] `zdiff` style
347+
- [ ] various newlines-related options during the merge (see https://git-scm.com/docs/git-merge#Documentation/git-merge.txt-ignore-space-change).
347348
- [ ] a way to control inter-hunk merging based on proximity (maybe via `gix-diff` feature which could use the same)
348349
* [ ] diff-heuristics match Git perfectly
349350
* [x] API documentation

gix-merge/Cargo.toml

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,36 @@ workspace = true
1515
doctest = false
1616

1717
[features]
18-
default = ["blob"]
19-
## Enable diffing of blobs using imara-diff, which also allows for a generic rewrite tracking implementation.
20-
blob = ["dep:imara-diff", "dep:gix-filter", "dep:gix-worktree", "dep:gix-path", "dep:gix-fs", "dep:gix-command", "dep:gix-tempfile", "dep:gix-trace", "dep:gix-quote"]
2118
## Data structures implement `serde::Serialize` and `serde::Deserialize`.
2219
serde = ["dep:serde", "gix-hash/serde", "gix-object/serde"]
2320

2421
[dependencies]
2522
gix-hash = { version = "^0.15.0", path = "../gix-hash" }
2623
gix-object = { version = "^0.45.0", path = "../gix-object" }
27-
gix-filter = { version = "^0.14.0", path = "../gix-filter", optional = true }
28-
gix-worktree = { version = "^0.37.0", path = "../gix-worktree", default-features = false, features = ["attributes"], optional = true }
29-
gix-command = { version = "^0.3.10", path = "../gix-command", optional = true }
30-
gix-path = { version = "^0.10.12", path = "../gix-path", optional = true }
31-
gix-fs = { version = "^0.12.0", path = "../gix-fs", optional = true }
32-
gix-tempfile = { version = "^15.0.0", path = "../gix-tempfile", optional = true }
33-
gix-trace = { version = "^0.1.11", path = "../gix-trace", optional = true }
34-
gix-quote = { version = "^0.4.13", path = "../gix-quote", optional = true }
24+
gix-filter = { version = "^0.14.0", path = "../gix-filter" }
25+
gix-worktree = { version = "^0.37.0", path = "../gix-worktree", default-features = false, features = ["attributes"] }
26+
gix-command = { version = "^0.3.10", path = "../gix-command" }
27+
gix-path = { version = "^0.10.12", path = "../gix-path" }
28+
gix-fs = { version = "^0.12.0", path = "../gix-fs" }
29+
gix-tempfile = { version = "^15.0.0", path = "../gix-tempfile" }
30+
gix-trace = { version = "^0.1.11", path = "../gix-trace" }
31+
gix-quote = { version = "^0.4.13", path = "../gix-quote" }
32+
gix-revision = { version = "^0.30.0", path = "../gix-revision", default-features = false, features = ["merge_base"] }
33+
gix-revwalk = { version = "^0.16.0", path = "../gix-revwalk" }
34+
gix-diff = { version = "^0.47.0", path = "../gix-diff", default-features = false, features = ["blob"] }
3535

3636
thiserror = "1.0.63"
37-
imara-diff = { version = "0.1.7", optional = true }
37+
imara-diff = { version = "0.1.7" }
3838
bstr = { version = "1.5.0", default-features = false }
3939
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
4040

4141
document-features = { version = "0.2.0", optional = true }
4242

4343
[dev-dependencies]
4444
gix-testtools = { path = "../tests/tools" }
45+
gix-odb = { path = "../gix-odb" }
46+
gix-utils = { version = "^0.1.12", path = "../gix-utils" }
47+
termtree = "0.5.1"
4548
pretty_assertions = "1.4.0"
4649

4750
[package.metadata.docs.rs]

gix-merge/src/commit.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/// The error returned by [`commit()`](crate::commit()).
2+
#[derive(Debug, thiserror::Error)]
3+
#[allow(missing_docs)]
4+
pub enum Error {
5+
#[error(transparent)]
6+
MergeBase(#[from] gix_revision::merge_base::Error),
7+
#[error(transparent)]
8+
MergeTree(#[from] crate::tree::Error),
9+
#[error("No common ancestor between {our_commit_id} and {their_commit_id}")]
10+
NoMergeBase {
11+
/// The commit on our side that was to be merged.
12+
our_commit_id: gix_hash::ObjectId,
13+
/// The commit on their side that was to be merged.
14+
their_commit_id: gix_hash::ObjectId,
15+
},
16+
#[error("Could not find ancestor, our or their commit to extract tree from")]
17+
FindCommit(#[from] gix_object::find::existing_object::Error),
18+
}
19+
20+
/// A way to configure [`commit()`](crate::commit()).
21+
#[derive(Default, Debug, Clone)]
22+
pub struct Options {
23+
/// If `true`, merging unrelated commits is allowed, with the merge-base being assumed as empty tree.
24+
pub allow_missing_merge_base: bool,
25+
/// Options to define how trees should be merged.
26+
pub tree_merge: crate::tree::Options,
27+
/// If `true`, do not merge multiple merge-bases into one. Instead, just use the first one.
28+
// TODO: test
29+
#[doc(alias = "no_recursive", alias = "git2")]
30+
pub use_first_merge_base: bool,
31+
}
32+
33+
pub(super) mod function {
34+
use crate::commit::{Error, Options};
35+
use gix_object::FindExt;
36+
37+
/// Like [`tree()`](crate::tree()), but it takes only two commits, `our_commit` and `their_commit` to automatically
38+
/// compute the merge-bases among them.
39+
/// If there are multiple merge bases, these will be auto-merged into one, recursively, if
40+
/// [`allow_missing_merge_base`](Options::allow_missing_merge_base) is `true`.
41+
///
42+
/// `labels` are names where [`current`](crate::blob::builtin_driver::text::Labels::current) is a name for `our_commit`
43+
/// and [`other`](crate::blob::builtin_driver::text::Labels::other) is a name for `their_commit`.
44+
/// If [`ancestor`](crate::blob::builtin_driver::text::Labels::ancestor) is unset, it will be set by us based on the
45+
/// merge-bases of `our_commit` and `their_commit`.
46+
///
47+
/// The `graph` is used to find the merge-base between `our_commit` and `their_commit`, and can also act as cache
48+
/// to speed up subsequent merge-base queries.
49+
///
50+
/// ### Performance
51+
///
52+
/// Note that `objects` *should* have an object cache to greatly accelerate tree-retrieval.
53+
#[allow(clippy::too_many_arguments)]
54+
pub fn commit<'objects, E>(
55+
our_commit: gix_hash::ObjectId,
56+
their_commit: gix_hash::ObjectId,
57+
mut labels: crate::blob::builtin_driver::text::Labels<'_>,
58+
graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit<gix_revision::merge_base::Flags>>,
59+
diff_resource_cache: &mut gix_diff::blob::Platform,
60+
blob_merge: &mut crate::blob::Platform,
61+
objects: &'objects impl gix_object::FindObjectOrHeader,
62+
write_blob_to_odb: impl FnMut(&[u8]) -> Result<gix_hash::ObjectId, E>,
63+
options: Options,
64+
) -> Result<crate::tree::Outcome<'objects>, Error>
65+
where
66+
E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
67+
{
68+
let merge_bases_commit_ids = gix_revision::merge_base(our_commit, &[their_commit], graph)?;
69+
let mut state = gix_diff::tree::State::default();
70+
let (merge_base_tree_id, ancestor_name) = match merge_bases_commit_ids {
71+
Some(base_commit) if base_commit.len() == 1 => {
72+
(objects.find_commit(&base_commit[0], &mut state.buf1)?.tree(), None)
73+
}
74+
Some(base_commits) => {
75+
let virtual_base_tree = if options.use_first_merge_base {
76+
let first = *base_commits.first().expect("TODO: merge multiple bases into one");
77+
objects.find_commit(&first, &mut state.buf1)?.tree()
78+
} else {
79+
todo!("merge multiple merge bases")
80+
};
81+
(virtual_base_tree, Some("merged common ancestors".into()))
82+
}
83+
None => {
84+
if options.allow_missing_merge_base {
85+
(
86+
gix_hash::ObjectId::empty_tree(our_commit.kind()),
87+
Some("empty tree".into()),
88+
)
89+
} else {
90+
return Err(Error::NoMergeBase {
91+
our_commit_id: our_commit,
92+
their_commit_id: their_commit,
93+
});
94+
}
95+
}
96+
};
97+
if labels.ancestor.is_none() {
98+
labels.ancestor = ancestor_name;
99+
}
100+
101+
let our_tree_id = objects.find_commit(&our_commit, &mut state.buf1)?.tree();
102+
let their_tree_id = objects.find_commit(&their_commit, &mut state.buf1)?.tree();
103+
104+
Ok(crate::tree(
105+
&merge_base_tree_id,
106+
&our_tree_id,
107+
&their_tree_id,
108+
labels,
109+
objects,
110+
write_blob_to_odb,
111+
&mut state,
112+
diff_resource_cache,
113+
blob_merge,
114+
options.tree_merge,
115+
)?)
116+
}
117+
}

gix-merge/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@
22
#![forbid(unsafe_code)]
33

44
///
5-
#[cfg(feature = "blob")]
65
pub mod blob;
6+
///
7+
pub mod commit;
8+
pub use commit::function::commit;
9+
///
10+
pub mod tree;
11+
pub use tree::function::tree;

0 commit comments

Comments
 (0)