Skip to content

Commit b7e8310

Browse files
committed
Finish the entire merge implementation and cover everything with tests.
1 parent 22e5f6c commit b7e8310

File tree

7 files changed

+367
-104
lines changed

7 files changed

+367
-104
lines changed

Cargo.lock

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

gix-merge/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ document-features = { version = "0.2.0", optional = true }
4343
[dev-dependencies]
4444
gix-testtools = { path = "../tests/tools" }
4545
gix-odb = { path = "../gix-odb" }
46+
gix-utils = { version = "^0.1.12", path = "../gix-utils" }
4647
pretty_assertions = "1.4.0"
4748

4849
[package.metadata.docs.rs]

gix-merge/src/commit.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ pub struct Options {
2424
pub allow_missing_merge_base: bool,
2525
/// Options to define how trees should be merged.
2626
pub tree_merge: crate::tree::Options,
27-
/// Options to define how to merge blobs.
28-
///
29-
/// Note that these are temporarily overwritten if multiple merge-bases are merged into one.
30-
pub blob_merge: crate::blob::platform::merge::Options,
3127
}
3228

3329
pub(super) mod function {

gix-merge/src/tree.rs

Lines changed: 173 additions & 79 deletions
Large diffs are not rendered by default.
Binary file not shown.

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ function baseline () (
3131
their_commit_id="$(git rev-parse "$their_committish")"
3232

3333
local merge_info="${output_name}.merge-info"
34-
git merge-tree -z --write-tree "$our_commit_id" "$their_commit_id" > "$merge_info" || :
35-
echo "$dir" "$our_commit_id" "$their_commit_id" "$merge_info" >> ../baseline.cases
34+
git merge-tree -z --write-tree "$our_committish" "$their_committish" > "$merge_info" || :
35+
echo "$dir" "$our_commit_id" "$our_committish" "$their_commit_id" "$their_committish" "$merge_info" >> ../baseline.cases
3636
)
3737

3838
git init simple
@@ -88,3 +88,4 @@ git init simple
8888
)
8989

9090
baseline simple without-conflict side1 side3
91+
baseline simple various-conflicts side1 side2

gix-merge/tests/merge/tree.rs

Lines changed: 189 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ fn run_baseline() -> crate::Result {
99
root,
1010
odb,
1111
our_commit_id,
12+
our_side_name,
1213
their_commit_id,
14+
their_side_name,
1315
merge_info,
1416
case_name,
1517
} in baseline::Expectations::new(&root, &cases)
@@ -25,16 +27,16 @@ fn run_baseline() -> crate::Result {
2527
percentage: Some(0.5),
2628
limit: 0,
2729
}),
30+
blob_merge: Default::default(),
2831
},
29-
blob_merge: Default::default(),
3032
};
3133
let mut actual = gix_merge::commit(
3234
our_commit_id,
3335
their_commit_id,
3436
gix_merge::blob::builtin_driver::text::Labels {
3537
ancestor: None,
36-
current: Some("ours".into()),
37-
other: Some("theirs".into()),
38+
current: Some(our_side_name.as_str().into()),
39+
other: Some(their_side_name.as_str().into()),
3840
},
3941
&mut graph,
4042
&mut diff_resource_cache,
@@ -43,32 +45,103 @@ fn run_baseline() -> crate::Result {
4345
options,
4446
)?;
4547

46-
match merge_info {
47-
Ok(expected_tree_id) => {
48-
let actual_id = actual.tree.write(|tree| odb.write(tree))?;
49-
assert_eq!(actual_id, expected_tree_id, "{case_name}: merged tree mismatch");
50-
}
51-
Err(_conflicts) => {
52-
todo!("compare conflicts")
53-
}
48+
let actual_id = actual.tree.write(|tree| odb.write(tree))?;
49+
assert_eq!(actual_id, merge_info.merged_tree, "{case_name}: merged tree mismatch");
50+
if let Some(conflicts) = merge_info.conflicts {
51+
dbg!(&conflicts, &merge_info.information);
52+
todo!("compare merge conflict information")
5453
}
5554
}
5655

5756
Ok(())
5857
}
5958

59+
// TODO: make sure everything is read eventually, even if only to improve debug messages in case of failure.
60+
#[allow(dead_code)]
6061
mod baseline {
62+
use gix_object::tree::EntryMode;
6163
use gix_worktree::stack::state::attributes;
6264
use std::path::{Path, PathBuf};
6365

64-
pub struct Conflict;
66+
/// An entry in the conflict
67+
#[derive(Debug)]
68+
pub struct Entry {
69+
/// The relative path in the repository
70+
pub location: String,
71+
/// The content id.
72+
pub id: gix_hash::ObjectId,
73+
/// The kind of entry.
74+
pub mode: EntryMode,
75+
}
76+
77+
/// Keep track of all the sides of a conflict. Some might not be set to indicate removal, including the ancestor.
78+
#[derive(Default, Debug)]
79+
pub struct Conflict {
80+
pub ancestor: Option<Entry>,
81+
pub ours: Option<Entry>,
82+
pub theirs: Option<Entry>,
83+
}
84+
85+
#[derive(Debug)]
86+
pub enum ConflictKind {
87+
/// The conflict was resolved by automatically merging the content.
88+
AutoMerging,
89+
/// The content could not be resolved so it's conflicting.
90+
ConflictContents,
91+
/// Directory in theirs in the way of our file
92+
ConflictDirectoryBlocksFile,
93+
/// Modified in ours but deleted in theirs
94+
ConflictModifyDelete,
95+
}
96+
97+
/// More loosely structured information about the `Conflict`.
98+
#[derive(Debug)]
99+
pub struct ConflictInfo {
100+
/// All the paths involved in the informational message
101+
pub paths: Vec<String>,
102+
/// The type of the conflict, further described in `message`.
103+
pub kind: ConflictKind,
104+
/// An arbitrary message formed from paths and kind
105+
pub message: String,
106+
}
107+
108+
impl Conflict {
109+
fn any_location(&self) -> Option<&str> {
110+
self.ancestor
111+
.as_ref()
112+
.or(self.ours.as_ref())
113+
.or(self.theirs.as_ref())
114+
.map(|a| a.location.as_str())
115+
}
116+
fn storage_for(&mut self, side: Side, location: &str) -> Option<&mut Option<Entry>> {
117+
let current_location = self.any_location();
118+
let location_is_same = current_location.is_none() || current_location == Some(location);
119+
let side = match side {
120+
Side::Ancestor => &mut self.ancestor,
121+
Side::Ours => &mut self.ours,
122+
Side::Theirs => &mut self.theirs,
123+
};
124+
(!side.is_some() && location_is_same).then_some(side)
125+
}
126+
}
127+
128+
pub struct MergeInfo {
129+
/// The hash of the merged tree - it may contain intermediate files if the merge didn't succeed entirely.
130+
pub merged_tree: gix_hash::ObjectId,
131+
/// If there were conflicts, this is the conflicting paths.
132+
pub conflicts: Option<Vec<Conflict>>,
133+
/// Structured details which to some extent can be compared to our own conflict information.
134+
pub information: Vec<ConflictInfo>,
135+
}
65136

66137
pub struct Expectation {
67138
pub root: PathBuf,
68139
pub odb: gix_odb::memory::Proxy<gix_odb::Handle>,
69140
pub our_commit_id: gix_hash::ObjectId,
141+
pub our_side_name: String,
70142
pub their_commit_id: gix_hash::ObjectId,
71-
pub merge_info: Result<gix_hash::ObjectId, Conflict>,
143+
pub their_side_name: String,
144+
pub merge_info: MergeInfo,
72145
pub case_name: String,
73146
}
74147

@@ -92,8 +165,21 @@ mod baseline {
92165
fn next(&mut self) -> Option<Self::Item> {
93166
let line = self.lines.next()?;
94167
let mut tokens = line.split(' ');
95-
let (Some(subdir), Some(our_commit_id), Some(their_commit_id), Some(merge_info_filename)) =
96-
(tokens.next(), tokens.next(), tokens.next(), tokens.next())
168+
let (
169+
Some(subdir),
170+
Some(our_commit_id),
171+
Some(our_side_name),
172+
Some(their_commit_id),
173+
Some(their_side_name),
174+
Some(merge_info_filename),
175+
) = (
176+
tokens.next(),
177+
tokens.next(),
178+
tokens.next(),
179+
tokens.next(),
180+
tokens.next(),
181+
tokens.next(),
182+
)
97183
else {
98184
unreachable!("invalid line: {line:?}")
99185
};
@@ -109,7 +195,9 @@ mod baseline {
109195
root: subdir_path,
110196
odb: objects,
111197
our_commit_id,
198+
our_side_name: our_side_name.to_owned(),
112199
their_commit_id,
200+
their_side_name: their_side_name.to_owned(),
113201
merge_info,
114202
case_name: format!(
115203
"{subdir}-{}",
@@ -122,11 +210,93 @@ mod baseline {
122210
}
123211
}
124212

125-
fn parse_merge_info(content: String) -> Result<gix_hash::ObjectId, Conflict> {
126-
let mut lines = content.split('\0').filter(|t| !t.is_empty());
213+
fn parse_merge_info(content: String) -> MergeInfo {
214+
let mut lines = content.split('\0').filter(|t| !t.is_empty()).peekable();
127215
let tree_id = gix_hash::ObjectId::from_hex(lines.next().unwrap().as_bytes()).unwrap();
128-
assert_eq!(lines.next(), None, "TODO: implement multi-line answer");
129-
Ok(tree_id)
216+
let mut out = MergeInfo {
217+
merged_tree: tree_id,
218+
conflicts: None,
219+
information: Vec::new(),
220+
};
221+
222+
let mut conflicts = Vec::new();
223+
let mut conflict = Conflict::default();
224+
while let Some(line) = lines.peek() {
225+
let (entry, side) = match parse_conflict_file_info(line) {
226+
Some(t) => t,
227+
None => break,
228+
};
229+
lines.next();
230+
let field = match conflict.storage_for(side, &entry.location) {
231+
None => {
232+
conflicts.push(conflict);
233+
conflict = Conflict::default();
234+
conflict
235+
.storage_for(side, &entry.location)
236+
.expect("always available for new side")
237+
}
238+
Some(field) => field,
239+
};
240+
*field = Some(entry);
241+
}
242+
243+
while lines.peek().is_some() {
244+
out.information
245+
.push(parse_info(&mut lines).expect("if there are lines, it should be valid info"));
246+
}
247+
assert_eq!(lines.next(), None, "TODO: conflict messages");
248+
out.conflicts = (!conflicts.is_empty()).then_some(conflicts);
249+
out
250+
}
251+
252+
#[derive(Copy, Clone)]
253+
enum Side {
254+
Ancestor,
255+
Ours,
256+
Theirs,
257+
}
258+
259+
fn parse_conflict_file_info(line: &str) -> Option<(Entry, Side)> {
260+
let (info, path) = line.split_at(line.find('\t')?);
261+
let mut tokens = info.split(' ');
262+
let (oct_mode, hex_id, stage) = (
263+
tokens.next().expect("mode"),
264+
tokens.next().expect("id"),
265+
tokens.next().expect("stage"),
266+
);
267+
assert_eq!(
268+
tokens.next(),
269+
None,
270+
"info line not understood, expected three fields only"
271+
);
272+
Some((
273+
Entry {
274+
location: path.to_owned(),
275+
id: gix_hash::ObjectId::from_hex(hex_id.as_bytes()).unwrap(),
276+
mode: EntryMode(gix_utils::btoi::to_signed_with_radix::<usize>(oct_mode.as_bytes(), 8).unwrap() as u16),
277+
},
278+
match stage {
279+
"1" => Side::Ancestor,
280+
"2" => Side::Ours,
281+
"3" => Side::Theirs,
282+
invalid => panic!("{invalid} is an unexpected side"),
283+
},
284+
))
285+
}
286+
287+
fn parse_info<'a>(mut lines: impl Iterator<Item = &'a str>) -> Option<ConflictInfo> {
288+
let num_paths: usize = lines.next()?.parse().ok()?;
289+
let paths: Vec<_> = lines.by_ref().take(num_paths).map(ToOwned::to_owned).collect();
290+
let kind = match lines.next()? {
291+
"Auto-merging" => ConflictKind::AutoMerging,
292+
"CONFLICT (contents)" => ConflictKind::ConflictContents,
293+
"CONFLICT (file/directory)" => ConflictKind::ConflictDirectoryBlocksFile,
294+
"CONFLICT (modify/delete)" => ConflictKind::ConflictModifyDelete,
295+
conflict_type => panic!("Unkonwn conflict type: {conflict_type}"),
296+
};
297+
let message = lines.next()?.to_owned();
298+
dbg!(&kind, &message);
299+
Some(ConflictInfo { paths, kind, message })
130300
}
131301

132302
pub fn new_platform(

0 commit comments

Comments
 (0)