Skip to content

Commit 2869349

Browse files
committed
Add support for multiple merge-bases (squash with prior one)
1 parent 7b0eb47 commit 2869349

File tree

7 files changed

+99
-28
lines changed

7 files changed

+99
-28
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/function.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ where
282282
id: their_id,
283283
location: their_location,
284284
entry_mode: their_mode,
285+
source_location,
285286
..
286287
},
287288
)
@@ -291,6 +292,7 @@ where
291292
id: their_id,
292293
location: their_location,
293294
entry_mode: their_mode,
295+
source_location,
294296
..
295297
},
296298
Change::Modification {
@@ -348,7 +350,7 @@ where
348350
&mut write_blob_to_odb,
349351
(our_location, *our_id, *our_mode),
350352
(their_location, *their_id, *their_mode),
351-
(our_location, *previous_id, *previous_entry_mode),
353+
(source_location, *previous_id, *previous_entry_mode),
352354
(0, outer_side),
353355
&options,
354356
)?;
@@ -650,8 +652,8 @@ where
650652
(id, Some(resolution))
651653
};
652654

653-
let merged_mode = merge_modes(*our_mode, *their_mode)
654-
.expect("BUG: Should have gated this case earlier");
655+
let merged_mode =
656+
merge_modes(*our_mode, *their_mode).expect("this case was assured earlier");
655657

656658
editor.remove(toc(source_location))?;
657659
our_tree.remove_existing_leaf(source_location.as_bstr());

gix-merge/src/tree/utils.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,18 +132,19 @@ 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
}
143143
};
144144
let prep = blob_merge.prepare_merge(objects, with_extra_markers(options, extra_markers))?;
145145
let (pick, resolution) = prep.merge(buf, labels, &options.blob_merge_command_ctx)?;
146146

147+
dbg!(&pick);
147148
let merged_blob_id = prep
148149
.id_by_pick(pick, buf, write_blob_to_odb)
149150
.map_err(|err| Error::WriteBlobToOdb(err.into()))?
159 KB
Binary file not shown.

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

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,28 @@ 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})"
56+
if [ -z "$opt_deviation_message" ]; then
57+
maybe_expected_tree="expected^{tree}"
58+
fi
4959

5060
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
61+
git -c merge.conflictStyle=$conflict_style merge-tree -z --write-tree --allow-unrelated-histories "$our_committish" "$their_committish" > "$merge_info" || :
62+
echo "$dir" "$conflict_style" "$our_commit_id" "$our_committish" "$their_commit_id" "$their_committish" "$merge_info" "$maybe_expected_tree" "$opt_deviation_message" >> ../baseline.cases
5363

5464
if [[ "$one_side" != "no-reverse" ]]; then
5565
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
66+
git -c merge.conflictStyle=$conflict_style merge-tree -z --write-tree --allow-unrelated-histories "$their_committish" "$our_committish" > "$merge_info" || :
67+
echo "$dir" "$conflict_style" "$their_commit_id" "$their_committish" "$our_commit_id" "$our_committish" "$merge_info" "$maybe_expected_tree" "$opt_deviation_message" >> ../baseline.cases
5868
fi
5969
)
6070

@@ -677,38 +687,64 @@ git init big-file-merge
677687
git commit -am "a normal but conflicting file change"
678688
)
679689

690+
git init no-merge-base
691+
(cd no-merge-base
692+
git checkout -b A
693+
echo "A" >content && git add . && git commit -m "content A"
694+
695+
git checkout --orphan B
696+
echo "B" >content && git add . && git commit -m "content B"
697+
698+
git checkout -b expectation
699+
)
700+
701+
680702
baseline simple side-1-3-without-conflict side1 side3
703+
baseline simple side-1-3-without-conflict-diff3 side1 side3
681704
baseline simple side-1-2-various-conflicts side1 side2
705+
baseline simple side-1-2-various-conflicts-diff3 side1 side2
682706
baseline simple single-content-conflict side1 side4
707+
baseline simple single-content-conflict-diff3 side1 side4
683708
baseline simple tweak1-side2 tweak1 side2
709+
baseline simple tweak1-side2-diff3 tweak1 side2
684710
baseline simple side-1-unrelated side1 unrelated
711+
baseline simple side-1-unrelated-diff3 side1 unrelated
685712
baseline rename-delete A-B A B
686713
baseline rename-delete A-similar A A
687714
baseline rename-delete B-similar B B
688715
baseline rename-add A-B A B
716+
baseline rename-add A-B-diff3 A B
689717
baseline rename-add-symlink A-B A B
718+
baseline rename-add-symlink A-B-diff3 A B
690719
baseline rename-rename-plus-content A-B A B
720+
baseline rename-rename-plus-content A-B-diff3 A B
691721
baseline rename-add-delete A-B A B
692722
baseline rename-rename-delete-delete A-B A B
693723
baseline super-1 A-B A B
724+
baseline super-1 A-B-diff3 A B
694725
baseline super-2 A-B A B
726+
baseline super-2 A-B-diff3 A B
695727

696728
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"
729+
baseline rename-within-rename-2 A-B-deviates A B "TBD: Right, something is different documentation was forgotten :/"
698730
baseline conflicting-rename A-B A B
699731
baseline conflicting-rename-2 A-B A B
700732
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"
701733

702734
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"
735+
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"
703736
baseline renamed-symlink-with-conflict A-B A B
704737
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"
705738

706739
baseline type-change-and-renamed A-B A B
707740
baseline change-and-delete A-B A B
708741
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."
709742
baseline both-modify-union-attr A-B A B
743+
baseline both-modify-union-attr A-B-diff3 A B
710744
baseline both-modify-binary A-B A B
711745
baseline both-modify-binary A-B A B
712746
baseline both-modify-file-with-binary-attr A-B A B
713747
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" \
714748
no-reverse
749+
baseline no-merge-base A-B A B
750+
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: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ fn run_baseline() -> crate::Result {
2020
let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?;
2121
let cases = std::fs::read_to_string(root.join("baseline.cases"))?;
2222
let mut actual_cases = 0;
23-
// let new_test = Some("added-file-changed-content-and-mode-A-B");
23+
// let new_test = Some("super-1-A-B-diff3");
2424
let new_test = None;
2525
for baseline::Expectation {
2626
root,
27+
conflict_style,
2728
odb,
2829
our_commit_id,
2930
our_side_name,
@@ -37,10 +38,17 @@ fn run_baseline() -> crate::Result {
3738
{
3839
actual_cases += 1;
3940
let mut graph = gix_revwalk::Graph::new(&odb, None);
40-
let large_file_threshold_bytes = 80;
41+
let large_file_threshold_bytes = 100;
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, 89,
100110
"BUG: update this number, and don't forget to remove a filter in the end"
101111
);
102112

0 commit comments

Comments
 (0)