Skip to content

Commit f95bbfe

Browse files
committed
Add support for multiple merge-bases (squash with prior one)
1 parent 5c8cbca commit f95bbfe

File tree

6 files changed

+88
-23
lines changed

6 files changed

+88
-23
lines changed

gix-merge/src/commit.rs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,17 @@ pub struct Options {
3030
pub use_first_merge_base: bool,
3131
}
3232

33+
/// The result of [`commit()`](crate::commit()).
34+
#[derive(Clone)]
35+
pub struct Outcome<'a> {
36+
/// The outcome of the actual tree-merge.
37+
pub tree_merge: crate::tree::Outcome<'a>,
38+
}
39+
3340
pub(super) mod function {
3441
use crate::commit::{Error, Options};
3542
use gix_object::FindExt;
43+
use std::borrow::Cow;
3644

3745
/// Like [`tree()`](crate::tree()), but it takes only two commits, `our_commit` and `their_commit` to automatically
3846
/// compute the merge-bases among them.
@@ -47,45 +55,47 @@ pub(super) mod function {
4755
/// The `graph` is used to find the merge-base between `our_commit` and `their_commit`, and can also act as cache
4856
/// to speed up subsequent merge-base queries.
4957
///
58+
/// Use `abbreviate_hash(id)` to shorten the given `id` according to standard git shortening rules. It's used in case
59+
/// the ancestor-label isn't explicitly set so that the merge base label becomes the shortened `id`.
60+
///
5061
/// ### Performance
5162
///
5263
/// Note that `objects` *should* have an object cache to greatly accelerate tree-retrieval.
5364
#[allow(clippy::too_many_arguments)]
5465
pub fn commit<'objects, E>(
5566
our_commit: gix_hash::ObjectId,
5667
their_commit: gix_hash::ObjectId,
57-
mut labels: crate::blob::builtin_driver::text::Labels<'_>,
68+
labels: crate::blob::builtin_driver::text::Labels<'_>,
5869
graph: &mut gix_revwalk::Graph<'_, '_, gix_revwalk::graph::Commit<gix_revision::merge_base::Flags>>,
5970
diff_resource_cache: &mut gix_diff::blob::Platform,
6071
blob_merge: &mut crate::blob::Platform,
6172
objects: &'objects impl gix_object::FindObjectOrHeader,
6273
write_blob_to_odb: impl FnMut(&[u8]) -> Result<gix_hash::ObjectId, E>,
74+
abbreviate_hash: impl FnOnce(&gix_hash::oid) -> String,
6375
options: Options,
6476
) -> Result<crate::tree::Outcome<'objects>, Error>
6577
where
6678
E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
6779
{
6880
let merge_bases_commit_ids = gix_revision::merge_base(our_commit, &[their_commit], graph)?;
6981
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-
}
82+
let (merge_base_tree_id, ancestor_name): (_, Cow<'_, str>) = match merge_bases_commit_ids {
83+
Some(base_commit) if base_commit.len() == 1 => (
84+
objects.find_commit(&base_commit[0], &mut state.buf1)?.tree(),
85+
abbreviate_hash(&base_commit[0]).into(),
86+
),
7487
Some(base_commits) => {
7588
let virtual_base_tree = if options.use_first_merge_base {
7689
let first = *base_commits.first().expect("TODO: merge multiple bases into one");
7790
objects.find_commit(&first, &mut state.buf1)?.tree()
7891
} else {
7992
todo!("merge multiple merge bases")
8093
};
81-
(virtual_base_tree, Some("merged common ancestors".into()))
94+
(virtual_base_tree, "merged common ancestors".into())
8295
}
8396
None => {
8497
if options.allow_missing_merge_base {
85-
(
86-
gix_hash::ObjectId::empty_tree(our_commit.kind()),
87-
Some("empty tree".into()),
88-
)
98+
(gix_hash::ObjectId::empty_tree(our_commit.kind()), "empty tree".into())
8999
} else {
90100
return Err(Error::NoMergeBase {
91101
our_commit_id: our_commit,
@@ -94,8 +104,10 @@ pub(super) mod function {
94104
}
95105
}
96106
};
107+
108+
let mut labels = labels; // TODO(borrowchk): this re-assignment shouldn't be needed.
97109
if labels.ancestor.is_none() {
98-
labels.ancestor = ancestor_name;
110+
labels.ancestor = Some(ancestor_name.as_ref().into());
99111
}
100112

101113
let our_tree_id = objects.find_commit(&our_commit, &mut state.buf1)?.tree();

gix-merge/src/tree/utils.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,11 @@ where
132132
let labels = if our_location == their_location {
133133
labels
134134
} else {
135-
ancestor = previous_location.as_bstr();
135+
ancestor = labels.ancestor.map(|side| combined(side, previous_location));
136136
current = labels.current.map(|side| combined(side, our_location));
137137
other = labels.other.map(|side| combined(side, their_location));
138138
crate::blob::builtin_driver::text::Labels {
139-
ancestor: Some(ancestor),
139+
ancestor: ancestor.as_ref().map(|n| n.as_bstr()),
140140
current: current.as_ref().map(|n| n.as_bstr()),
141141
other: other.as_ref().map(|n| n.as_bstr()),
142142
}
128 KB
Binary file not shown.

gix-merge/tests/fixtures/tree-baseline.sh

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,25 @@ function baseline () (
4343
local our_commit_id
4444
local their_commit_id
4545

46+
local conflict_style="merge"
47+
if [[ "$output_name" == *-merge ]]; then
48+
conflict_style="merge"
49+
elif [[ "$output_name" == *-diff3 ]]; then
50+
conflict_style="diff3"
51+
fi
52+
4653
our_commit_id="$(git rev-parse "$our_committish")"
4754
their_commit_id="$(git rev-parse "$their_committish")"
4855
local maybe_expected_tree="$(git rev-parse expected^{tree})"
4956

5057
local merge_info="${output_name}.merge-info"
51-
git merge-tree -z --write-tree --allow-unrelated-histories "$our_committish" "$their_committish" > "$merge_info" || :
52-
echo "$dir" "$our_commit_id" "$our_committish" "$their_commit_id" "$their_committish" "$merge_info" "$maybe_expected_tree" "$opt_deviation_message" >> ../baseline.cases
58+
git -c merge.conflictStyle=$conflict_style merge-tree -z --write-tree --allow-unrelated-histories "$our_committish" "$their_committish" > "$merge_info" || :
59+
echo "$dir" "$conflict_style" "$our_commit_id" "$our_committish" "$their_commit_id" "$their_committish" "$merge_info" "$maybe_expected_tree" "$opt_deviation_message" >> ../baseline.cases
5360

5461
if [[ "$one_side" != "no-reverse" ]]; then
5562
local merge_info="${output_name}-reversed.merge-info"
56-
git merge-tree -z --write-tree --allow-unrelated-histories "$their_committish" "$our_committish" > "$merge_info" || :
57-
echo "$dir" "$their_commit_id" "$their_committish" "$our_commit_id" "$our_committish" "$merge_info" "$maybe_expected_tree" "$opt_deviation_message" >> ../baseline.cases
63+
git -c merge.conflictStyle=$conflict_style merge-tree -z --write-tree --allow-unrelated-histories "$their_committish" "$our_committish" > "$merge_info" || :
64+
echo "$dir" "$conflict_style" "$their_commit_id" "$their_committish" "$our_commit_id" "$our_committish" "$merge_info" "$maybe_expected_tree" "$opt_deviation_message" >> ../baseline.cases
5865
fi
5966
)
6067

@@ -677,38 +684,64 @@ git init big-file-merge
677684
git commit -am "a normal but conflicting file change"
678685
)
679686

687+
git init no-merge-base
688+
(cd no-merge-base
689+
git checkout -b A
690+
echo "A" >content && git add . && git commit -m "content A"
691+
692+
git checkout --orphan B
693+
echo "B" >content && git add . && git commit -m "content B"
694+
695+
git checkout -b expectation
696+
)
697+
698+
680699
baseline simple side-1-3-without-conflict side1 side3
700+
baseline simple side-1-3-without-conflict-diff3 side1 side3
681701
baseline simple side-1-2-various-conflicts side1 side2
702+
baseline simple side-1-2-various-conflicts-diff3 side1 side2
682703
baseline simple single-content-conflict side1 side4
704+
baseline simple single-content-conflict-diff3 side1 side4
683705
baseline simple tweak1-side2 tweak1 side2
706+
#baseline simple tweak1-side2-diff3 tweak1 side2 # TODO: fix failure
684707
baseline simple side-1-unrelated side1 unrelated
708+
baseline simple side-1-unrelated-diff3 side1 unrelated
685709
baseline rename-delete A-B A B
686710
baseline rename-delete A-similar A A
687711
baseline rename-delete B-similar B B
688712
baseline rename-add A-B A B
713+
baseline rename-add A-B-diff3 A B
689714
baseline rename-add-symlink A-B A B
715+
baseline rename-add-symlink A-B-diff3 A B
690716
baseline rename-rename-plus-content A-B A B
717+
baseline rename-rename-plus-content A-B-diff3 A B
691718
baseline rename-add-delete A-B A B
692719
baseline rename-rename-delete-delete A-B A B
693720
baseline super-1 A-B A B
721+
#baseline super-1 A-B-diff3 A B # TODO: fix failure
694722
baseline super-2 A-B A B
723+
baseline super-2 A-B-diff3 A B
695724

696725
baseline rename-within-rename A-B-deviates A B "Git doesn't detect the rename-nesting so there is duplication - we achieve the optimal result"
697-
baseline rename-within-rename-2 A-B-deviates A B "TBD"
726+
baseline rename-within-rename-2 A-B-deviates A B "TBD: Right, something is different documentation was forgotten :/"
698727
baseline conflicting-rename A-B A B
699728
baseline conflicting-rename-2 A-B A B
700729
baseline conflicting-rename-complex A-B A B "Git has different rename tracking which is why a-renamed/w disappears - it's still close enough"
701730

702731
baseline same-rename-different-mode A-B A B "Git works for the A/B case, but for B/A it forgets to set the executable bit"
732+
baseline same-rename-different-mode A-B-diff3 A B "Git works for the A/B case, but for B/A it forgets to set the executable bit"
703733
baseline renamed-symlink-with-conflict A-B A B
704734
baseline added-file-changed-content-and-mode A-B A B "We improve on executable bit handling, but loose on diff quality as we are definitely missing some tweaks"
705735

706736
baseline type-change-and-renamed A-B A B
707737
baseline change-and-delete A-B A B
708738
baseline submodule-both-modify A-B A B "We can't handle submodules yet and just mark them as conflicting. This is planned to be improved."
709739
baseline both-modify-union-attr A-B A B
740+
baseline both-modify-union-attr A-B-diff3 A B
710741
baseline both-modify-binary A-B A B
711742
baseline both-modify-binary A-B A B
712743
baseline both-modify-file-with-binary-attr A-B A B
713744
baseline big-file-merge A-B A B "Git actually ignores core.bigFileThreshold during merging and tries a normal merge (or binary one) anyway. We don't ignore it and treat big files like binary files" \
714745
no-reverse
746+
baseline no-merge-base A-B A B
747+
baseline no-merge-base A-B-diff3 A B

gix-merge/tests/merge/tree/baseline.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use bstr::{BStr, ByteSlice};
22
use gix_hash::ObjectId;
3+
use gix_merge::blob::builtin_driver::text::ConflictStyle;
34
use gix_object::tree::EntryMode;
45
use gix_object::FindExt;
56
use std::path::{Path, PathBuf};
@@ -87,6 +88,7 @@ pub struct MergeInfo {
8788

8889
pub struct Expectation {
8990
pub root: PathBuf,
91+
pub conflict_style: gix_merge::blob::builtin_driver::text::ConflictStyle,
9092
pub odb: gix_odb::memory::Proxy<gix_odb::Handle>,
9193
pub our_commit_id: gix_hash::ObjectId,
9294
pub our_side_name: String,
@@ -127,6 +129,7 @@ impl Iterator for Expectations<'_> {
127129
let mut tokens = line.split(' ');
128130
let (
129131
Some(subdir),
132+
Some(conflict_style),
130133
Some(our_commit_id),
131134
Some(our_side_name),
132135
Some(their_commit_id),
@@ -141,6 +144,7 @@ impl Iterator for Expectations<'_> {
141144
tokens.next(),
142145
tokens.next(),
143146
tokens.next(),
147+
tokens.next(),
144148
)
145149
else {
146150
unreachable!("invalid line: {line:?}")
@@ -156,13 +160,19 @@ impl Iterator for Expectations<'_> {
156160
});
157161

158162
let subdir_path = self.root.join(subdir);
163+
let conflict_style = match conflict_style {
164+
"merge" => ConflictStyle::Merge,
165+
"diff3" => ConflictStyle::Diff3,
166+
unknown => unreachable!("Unknown conflict style: '{unknown}'"),
167+
};
159168
let odb = gix_odb::at(subdir_path.join(".git/objects")).expect("object dir exists");
160169
let objects = gix_odb::memory::Proxy::new(odb, gix_hash::Kind::Sha1);
161170
let our_commit_id = gix_hash::ObjectId::from_hex(our_commit_id.as_bytes()).unwrap();
162171
let their_commit_id = gix_hash::ObjectId::from_hex(their_commit_id.as_bytes()).unwrap();
163172
let merge_info = parse_merge_info(std::fs::read_to_string(subdir_path.join(merge_info_filename)).unwrap());
164173
Some(Expectation {
165174
root: subdir_path,
175+
conflict_style,
166176
odb: objects,
167177
our_commit_id,
168178
our_side_name: our_side_name.to_owned(),

gix-merge/tests/merge/tree/mod.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ fn run_baseline() -> crate::Result {
2424
let new_test = None;
2525
for baseline::Expectation {
2626
root,
27+
conflict_style,
2728
odb,
2829
our_commit_id,
2930
our_side_name,
@@ -40,7 +41,14 @@ fn run_baseline() -> crate::Result {
4041
let large_file_threshold_bytes = 80;
4142
let mut blob_merge = new_blob_merge_platform(&root, large_file_threshold_bytes);
4243
let mut diff_resource_cache = new_diff_resource_cache(&root);
43-
let options = basic_merge_options();
44+
let mut options = basic_merge_options();
45+
options.tree_merge.blob_merge.text.conflict = gix_merge::blob::builtin_driver::text::Conflict::Keep {
46+
style: conflict_style,
47+
marker_size: gix_merge::blob::builtin_driver::text::Conflict::DEFAULT_MARKER_SIZE
48+
.try_into()
49+
.expect("non-zero"),
50+
};
51+
4452
dbg!(&case_name);
4553
let mut actual = gix_merge::commit(
4654
our_commit_id,
@@ -55,6 +63,7 @@ fn run_baseline() -> crate::Result {
5563
&mut blob_merge,
5664
&odb,
5765
|content| odb.write_buf(gix_object::Kind::Blob, content),
66+
|id| id.to_hex_with_len(7).to_string(),
5867
options,
5968
)?;
6069

@@ -71,15 +80,16 @@ fn run_baseline() -> crate::Result {
7180
expected_tree_id,
7281
}) => {
7382
// Sometimes only the reversed part of a specific test is different.
74-
if case_name != "same-rename-different-mode-A-B" {
83+
if case_name != "same-rename-different-mode-A-B" && case_name != "same-rename-different-mode-A-B-diff3"
84+
{
7585
assert_ne!(
7686
actual_id, merge_info.merged_tree,
77-
"Git caught up - adjust expectation {message}"
87+
"{case_name}: Git caught up - adjust expectation {message}"
7888
);
7989
} else {
8090
assert_eq!(
8191
actual_id, merge_info.merged_tree,
82-
"Git should match here, it just doesn't match in one of two cases"
92+
"{case_name}: Git should match here, it just doesn't match in one of two cases"
8393
);
8494
}
8595
eprintln!("{}", baseline::visualize_tree(&actual_id, &odb, None));
@@ -96,7 +106,7 @@ fn run_baseline() -> crate::Result {
96106
}
97107

98108
assert_eq!(
99-
actual_cases, 61,
109+
actual_cases, 85,
100110
"BUG: update this number, and don't forget to remove a filter in the end"
101111
);
102112

0 commit comments

Comments
 (0)