From af2aa2302506df476297a14db388fa4169a811ba Mon Sep 17 00:00:00 2001 From: "GPT 5.6" Date: Mon, 27 Jul 2026 08:10:23 +0200 Subject: [PATCH 1/4] Add structural tree-merge validation. Add complementary benchmark, Cartesian, and fuzz harnesses around gix_merge::tree() so the scheduling state machine can be measured and checked without commit traversal or result-tree serialization obscuring failures. The Criterion benchmark builds a shallow in-memory repository containing 34 independent additions, deletions, modifications, mode and type changes, file/directory conflicts, and rename interactions. It warms and reuses merge platforms, uses exact-only rename detection and path-unique blobs, reports structural cases as throughput, and asserts that at least twelve conflicts remain so the fixture cannot silently become trivial. Exercise every Cartesian operation pair in both directions with default and ancestor conflict handling, fail-fast behavior, and rename detection disabled. Check side-order symmetry, payload retention, forced-resolution provenance, the public early-exit flag, and independently unambiguous merge results, and record the resulting status quo. Add a bounded structural libFuzzer target covering valid base and side trees, all supported entry modes, file/directory replacements, and file or subtree renames in both side orderings. Keep blob work bounded with NUL-containing resources, a one-byte large-file threshold, identity-only rename tracking, and the Histogram text algorithm, then write each editor result back to the in-memory object database. Treat only the documented missing binary ancestor during ancestor resolution as an expected generated outcome; all other errors remain fuzz failures. Tests: - cargo bench -p gix-merge --bench tree --all-features -- --test - cargo test -p gix-merge --test merge tree::cartesian - cargo check --manifest-path gix-merge/fuzz/Cargo.toml Co-authored-by: Sebastian Thiel --- Cargo.lock | 1 + gix-merge/Cargo.toml | 5 + gix-merge/benches/tree.rs | 472 ++++++++++++++++++ gix-merge/fuzz/Cargo.toml | 12 + gix-merge/fuzz/fuzz_targets/tree.rs | 384 ++++++++++++++ .../tests/merge/tree/cartesian-baseline.txt | 37 ++ gix-merge/tests/merge/tree/cartesian.rs | 285 +++++++++++ 7 files changed, 1196 insertions(+) create mode 100644 gix-merge/benches/tree.rs create mode 100644 gix-merge/fuzz/fuzz_targets/tree.rs diff --git a/Cargo.lock b/Cargo.lock index aeb589dff5b..1427cfb6a87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2106,6 +2106,7 @@ version = "0.19.0" dependencies = [ "arbitrary", "bstr", + "criterion", "document-features", "gix-command", "gix-diff", diff --git a/gix-merge/Cargo.toml b/gix-merge/Cargo.toml index 1768e265a47..7effed83c75 100644 --- a/gix-merge/Cargo.toml +++ b/gix-merge/Cargo.toml @@ -15,6 +15,10 @@ workspace = true [lib] doctest = false +[[bench]] +name = "tree" +harness = false + [features] ## Enable support for the SHA-1 hash by enabling the respective feature in the `gix-hash` crate. sha1 = ["gix-hash/sha1"] @@ -48,6 +52,7 @@ serde = { version = "1.0.114", optional = true, default-features = false, featur document-features = { version = "0.2.0", optional = true } [dev-dependencies] +criterion = "0.8.2" gix-testtools = { path = "../tests/tools" } gix-odb = { path = "../gix-odb" } gix-utils = { path = "../gix-utils" } diff --git a/gix-merge/benches/tree.rs b/gix-merge/benches/tree.rs new file mode 100644 index 00000000000..94a3d985f1f --- /dev/null +++ b/gix-merge/benches/tree.rs @@ -0,0 +1,472 @@ +use std::{collections::BTreeMap, hint::black_box, path::Path}; + +use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main}; +use gix_diff::Rewrites; +use gix_hash::ObjectId; +use gix_merge::tree::{Options, Outcome}; +use gix_object::{ + Kind, Tree, Write, + tree::{EntryKind, EntryMode}, +}; +use gix_worktree::stack::state::attributes; + +const CASE_COUNT: u64 = 34; + +type ObjectDb = gix_odb::memory::Proxy; +type Entries = BTreeMap<&'static str, (EntryMode, ObjectId)>; + +/// A small, shallow tree containing independent examples of the main structural merge cases. +/// +/// The numbered paths cover additions, deletions, modifications, modes, types, rename combinations, +/// directory renames, and file/directory replacements. Exact rename detection avoids spending the +/// benchmark on similarity scoring, and only the modify/modify case needs a genuine text merge. +struct Scenario { + objects: ObjectDb, + base: ObjectId, + ours: ObjectId, + theirs: ObjectId, +} + +fn tree_merge(c: &mut Criterion) { + let scenario = scenario(); + let mut diff_state = gix_diff::tree::State::default(); + let mut diff_resource_cache = new_diff_resource_cache(); + let mut blob_merge = new_blob_merge_platform(); + let options = options(); + + let outcome = merge( + &scenario, + scenario.ours, + scenario.theirs, + &mut diff_state, + &mut diff_resource_cache, + &mut blob_merge, + options.clone(), + ); + assert!( + outcome.conflicts.len() >= 12, + "the mixed scenario should keep exercising many conflict resolutions" + ); + + let mut group = c.benchmark_group("tree-merge/mixed-structural-cases"); + group.throughput(Throughput::Elements(CASE_COUNT)); + group.bench_function("merge", |b| { + b.iter_batched( + || options.clone(), + |options| { + black_box(merge( + &scenario, + scenario.ours, + scenario.theirs, + &mut diff_state, + &mut diff_resource_cache, + &mut blob_merge, + options, + )) + }, + BatchSize::SmallInput, + ); + }); + group.finish(); +} + +fn merge<'objects>( + scenario: &'objects Scenario, + ours: ObjectId, + theirs: ObjectId, + diff_state: &mut gix_diff::tree::State, + diff_resource_cache: &mut gix_diff::blob::Platform, + blob_merge: &mut gix_merge::blob::Platform, + options: Options, +) -> Outcome<'objects> { + gix_merge::tree( + &scenario.base, + &ours, + &theirs, + gix_merge::blob::builtin_driver::text::Labels { + ancestor: Some("BASE".into()), + current: Some("OURS".into()), + other: Some("THEIRS".into()), + }, + &scenario.objects, + |buf| scenario.objects.write_buf(Kind::Blob, buf), + diff_state, + diff_resource_cache, + blob_merge, + options, + ) + .expect("in-memory tree merge succeeds") +} + +fn scenario() -> Scenario { + let objects = ObjectDb::new(gix_object::find::Never, gix_hash::Kind::Sha1); + let mut base = Entries::new(); + + let modify_ours = insert_base(&objects, &mut base, "01-modify-ours", EntryKind::Blob); + let modify_theirs = insert_base(&objects, &mut base, "02-modify-theirs", EntryKind::Blob); + let modify_same = insert_base(&objects, &mut base, "03-modify-same", EntryKind::Blob); + let _modify_both = insert_base(&objects, &mut base, "04-modify-both", EntryKind::Blob); + let _delete_ours = insert_base(&objects, &mut base, "05-delete-ours", EntryKind::Blob); + let _delete_both = insert_base(&objects, &mut base, "06-delete-both", EntryKind::Blob); + let _modify_delete = insert_base(&objects, &mut base, "07-modify-delete", EntryKind::Blob); + let mode_ours = insert_base(&objects, &mut base, "08-mode-ours", EntryKind::Blob); + let mode_and_modify = insert_base(&objects, &mut base, "09-mode-and-modify", EntryKind::Blob); + let _symlink_delete = insert_base(&objects, &mut base, "10-symlink-delete", EntryKind::Link); + + for path in [ + "16-rename-clean/source", + "17-rename-same/source", + "18-rename-modify/source", + "19-rename-delete/source", + "20-rename-different/source", + "21-rename-add/source", + "22-rename-destination/source", + "22-rename-destination/target", + "23-rename-destination-delete/source", + "23-rename-destination-delete/target", + "24-two-to-one/one", + "24-two-to-one/two", + "25-directory-rename/old/existing", + "26-directory-rename-modify/old/file", + "27-directory-rename-different/old/file", + "28-file-to-directory/node", + "29-directory-to-file/node/child", + "30-both-file-to-directory/node", + "31-delete-vs-file-to-directory/node", + "32-rename-vs-file-to-directory/node", + ] { + insert_base(&objects, &mut base, path, EntryKind::Blob); + } + + let mut ours = base.clone(); + let mut theirs = base.clone(); + + set_blob(&objects, &mut ours, "01-modify-ours", b"ours\n", EntryKind::Blob); + set_blob(&objects, &mut theirs, "02-modify-theirs", b"theirs\n", EntryKind::Blob); + let same = blob(&objects, b"same\n"); + ours.insert("03-modify-same", (EntryKind::Blob.into(), same)); + theirs.insert("03-modify-same", (EntryKind::Blob.into(), same)); + set_blob(&objects, &mut ours, "04-modify-both", b"ours\n", EntryKind::Blob); + set_blob(&objects, &mut theirs, "04-modify-both", b"theirs\n", EntryKind::Blob); + ours.remove("05-delete-ours"); + ours.remove("06-delete-both"); + theirs.remove("06-delete-both"); + set_blob(&objects, &mut ours, "07-modify-delete", b"modified\n", EntryKind::Blob); + theirs.remove("07-modify-delete"); + ours.insert("08-mode-ours", (EntryKind::BlobExecutable.into(), mode_ours)); + ours.insert( + "09-mode-and-modify", + (EntryKind::BlobExecutable.into(), mode_and_modify), + ); + set_blob( + &objects, + &mut theirs, + "09-mode-and-modify", + b"modified\n", + EntryKind::Blob, + ); + set_blob( + &objects, + &mut ours, + "10-symlink-delete", + b"new-target\n", + EntryKind::Link, + ); + theirs.remove("10-symlink-delete"); + + set_blob(&objects, &mut ours, "11-add-ours", b"added\n", EntryKind::Blob); + let same_addition = blob(&objects, b"same addition\n"); + ours.insert("12-add-same", (EntryKind::Blob.into(), same_addition)); + theirs.insert("12-add-same", (EntryKind::Blob.into(), same_addition)); + let mode_addition = blob(&objects, b"mode addition\n"); + ours.insert("13-add-mode", (EntryKind::Blob.into(), mode_addition)); + theirs.insert("13-add-mode", (EntryKind::BlobExecutable.into(), mode_addition)); + let type_addition = blob(&objects, b"type addition\n"); + ours.insert("14-add-type", (EntryKind::Blob.into(), type_addition)); + theirs.insert("14-add-type", (EntryKind::Link.into(), type_addition)); + set_blob(&objects, &mut ours, "15-add-directory/ours", b"ours\n", EntryKind::Blob); + set_blob( + &objects, + &mut theirs, + "15-add-directory/theirs", + b"theirs\n", + EntryKind::Blob, + ); + + rename(&mut ours, "16-rename-clean/source", "16-rename-clean/target"); + rename(&mut ours, "17-rename-same/source", "17-rename-same/target"); + rename(&mut theirs, "17-rename-same/source", "17-rename-same/target"); + rename(&mut ours, "18-rename-modify/source", "18-rename-modify/target"); + set_blob( + &objects, + &mut theirs, + "18-rename-modify/source", + b"modified\n", + EntryKind::Blob, + ); + rename(&mut ours, "19-rename-delete/source", "19-rename-delete/target"); + theirs.remove("19-rename-delete/source"); + rename(&mut ours, "20-rename-different/source", "20-rename-different/ours"); + rename(&mut theirs, "20-rename-different/source", "20-rename-different/theirs"); + rename(&mut ours, "21-rename-add/source", "21-rename-add/target"); + set_blob( + &objects, + &mut theirs, + "21-rename-add/target", + b"addition\n", + EntryKind::Blob, + ); + replace_destination_with_source( + &mut ours, + "22-rename-destination/source", + "22-rename-destination/target", + ); + set_blob( + &objects, + &mut theirs, + "22-rename-destination/target", + b"modified target\n", + EntryKind::Blob, + ); + replace_destination_with_source( + &mut ours, + "23-rename-destination-delete/source", + "23-rename-destination-delete/target", + ); + theirs.remove("23-rename-destination-delete/target"); + rename(&mut ours, "24-two-to-one/one", "24-two-to-one/target"); + rename(&mut theirs, "24-two-to-one/two", "24-two-to-one/target"); + + rename( + &mut ours, + "25-directory-rename/old/existing", + "25-directory-rename/new/existing", + ); + set_blob( + &objects, + &mut theirs, + "25-directory-rename/old/added", + b"added\n", + EntryKind::Blob, + ); + rename( + &mut ours, + "26-directory-rename-modify/old/file", + "26-directory-rename-modify/new/file", + ); + set_blob( + &objects, + &mut theirs, + "26-directory-rename-modify/old/file", + b"modified\n", + EntryKind::Blob, + ); + rename( + &mut ours, + "27-directory-rename-different/old/file", + "27-directory-rename-different/ours/file", + ); + rename( + &mut theirs, + "27-directory-rename-different/old/file", + "27-directory-rename-different/theirs/file", + ); + + ours.remove("28-file-to-directory/node"); + set_blob( + &objects, + &mut ours, + "28-file-to-directory/node/child", + b"child\n", + EntryKind::Blob, + ); + set_blob( + &objects, + &mut theirs, + "28-file-to-directory/node", + b"modified\n", + EntryKind::Blob, + ); + ours.remove("29-directory-to-file/node/child"); + set_blob( + &objects, + &mut ours, + "29-directory-to-file/node", + b"file\n", + EntryKind::Blob, + ); + set_blob( + &objects, + &mut theirs, + "29-directory-to-file/node/child", + b"modified\n", + EntryKind::Blob, + ); + ours.remove("30-both-file-to-directory/node"); + theirs.remove("30-both-file-to-directory/node"); + set_blob( + &objects, + &mut ours, + "30-both-file-to-directory/node/ours", + b"ours\n", + EntryKind::Blob, + ); + set_blob( + &objects, + &mut theirs, + "30-both-file-to-directory/node/theirs", + b"theirs\n", + EntryKind::Blob, + ); + ours.remove("31-delete-vs-file-to-directory/node"); + theirs.remove("31-delete-vs-file-to-directory/node"); + set_blob( + &objects, + &mut theirs, + "31-delete-vs-file-to-directory/node/child", + b"child\n", + EntryKind::Blob, + ); + rename( + &mut ours, + "32-rename-vs-file-to-directory/node", + "32-rename-vs-file-to-directory/away", + ); + theirs.remove("32-rename-vs-file-to-directory/node"); + set_blob( + &objects, + &mut theirs, + "32-rename-vs-file-to-directory/node/child", + b"child\n", + EntryKind::Blob, + ); + set_blob( + &objects, + &mut ours, + "33-add-file-vs-directory/node", + b"file\n", + EntryKind::Blob, + ); + set_blob( + &objects, + &mut theirs, + "33-add-file-vs-directory/node/child", + b"child\n", + EntryKind::Blob, + ); + let executable_addition = blob(&objects, b"executable\n"); + ours.insert( + "34-add-executable-same", + (EntryKind::BlobExecutable.into(), executable_addition), + ); + theirs.insert( + "34-add-executable-same", + (EntryKind::BlobExecutable.into(), executable_addition), + ); + + let base = write_tree(&objects, &base); + let ours = write_tree(&objects, &ours); + let theirs = write_tree(&objects, &theirs); + assert_ne!(modify_ours, modify_theirs); + assert_ne!(modify_same, modify_ours); + + Scenario { + objects, + base, + ours, + theirs, + } +} + +fn insert_base(objects: &ObjectDb, entries: &mut Entries, path: &'static str, kind: EntryKind) -> ObjectId { + let id = blob(objects, path.as_bytes()); + entries.insert(path, (kind.into(), id)); + id +} + +fn set_blob(objects: &ObjectDb, entries: &mut Entries, path: &'static str, data: &[u8], kind: EntryKind) { + let mut unique_data = path.as_bytes().to_vec(); + unique_data.push(b'\n'); + unique_data.extend_from_slice(data); + entries.insert(path, (kind.into(), blob(objects, &unique_data))); +} + +fn blob(objects: &ObjectDb, data: &[u8]) -> ObjectId { + objects + .write_buf(Kind::Blob, data) + .expect("in-memory object writes succeed") +} + +fn rename(entries: &mut Entries, source: &'static str, destination: &'static str) { + let entry = entries.remove(source).expect("rename source exists"); + entries.insert(destination, entry); +} + +fn replace_destination_with_source(entries: &mut Entries, source: &'static str, destination: &'static str) { + let source = entries.remove(source).expect("rename source exists"); + entries.insert(destination, source); +} + +fn write_tree(objects: &ObjectDb, entries: &Entries) -> ObjectId { + let mut editor = gix_object::tree::Editor::new(Tree::default(), &gix_object::find::Never, gix_hash::Kind::Sha1); + for (path, (mode, id)) in entries { + editor + .upsert(path.split('/'), mode.kind(), *id) + .expect("benchmark paths are valid"); + } + editor + .write(|tree| objects.write(tree)) + .expect("in-memory tree writes succeed") +} + +fn options() -> Options { + Options { + rewrites: Some(Rewrites { + copies: None, + percentage: Some(1.0), + limit: 0, + track_empty: false, + }), + ..Default::default() + } +} + +fn new_diff_resource_cache() -> gix_diff::blob::Platform { + gix_diff::blob::Platform::new( + Default::default(), + gix_diff::blob::Pipeline::new(Default::default(), Default::default(), Vec::new(), Default::default()), + Default::default(), + gix_worktree::Stack::new( + Path::new("gix-merge-benchmark-no-worktree"), + gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::default()), + Default::default(), + Vec::new(), + Vec::new(), + ), + ) +} + +fn new_blob_merge_platform() -> gix_merge::blob::Platform { + let attributes = gix_worktree::Stack::new( + Path::new("gix-merge-benchmark-no-worktree"), + gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::new( + Default::default(), + None, + attributes::Source::WorktreeThenIdMapping, + Default::default(), + )), + gix_worktree::glob::pattern::Case::Sensitive, + Vec::new(), + Vec::new(), + ); + gix_merge::blob::Platform::new( + gix_merge::blob::Pipeline::new(Default::default(), gix_filter::Pipeline::default(), Default::default()), + gix_merge::blob::pipeline::Mode::ToGit, + attributes, + vec![], + Default::default(), + ) +} + +criterion_group!(benches, tree_merge); +criterion_main!(benches); diff --git a/gix-merge/fuzz/Cargo.toml b/gix-merge/fuzz/Cargo.toml index d46cc616b1d..736d6d57e7e 100644 --- a/gix-merge/fuzz/Cargo.toml +++ b/gix-merge/fuzz/Cargo.toml @@ -13,7 +13,13 @@ anyhow = "1.0.76" libfuzzer-sys = "0.4" arbitrary = { version = "1.3.2", features = ["derive"] } imara-diff = { package = "gix-imara-diff", version = "0.2.0", path = "../../gix-imara-diff" } +gix-diff = { path = "../../gix-diff" } +gix-filter = { path = "../../gix-filter" } +gix-hash = { path = "../../gix-hash", features = ["sha1"] } gix-merge = { path = "..", features = ["sha1"] } +gix-object = { path = "../../gix-object" } +gix-odb = { path = "../../gix-odb" } +gix-worktree = { path = "../../gix-worktree" } # Prevent this from interfering with workspaces [workspace] @@ -24,3 +30,9 @@ name = "blob" path = "fuzz_targets/blob.rs" test = false doc = false + +[[bin]] +name = "tree" +path = "fuzz_targets/tree.rs" +test = false +doc = false diff --git a/gix-merge/fuzz/fuzz_targets/tree.rs b/gix-merge/fuzz/fuzz_targets/tree.rs new file mode 100644 index 00000000000..3699ef75862 --- /dev/null +++ b/gix-merge/fuzz/fuzz_targets/tree.rs @@ -0,0 +1,384 @@ +#![no_main] + +use std::{collections::BTreeMap, path::Path as FsPath}; + +use gix_diff::Rewrites; +use gix_hash::ObjectId; +use gix_merge::{ + blob::builtin_driver::binary, + tree::{Options, ResolveWith, TreatAsUnresolved}, +}; +use gix_object::{ + Kind, Tree, Write, + tree::{EntryKind, EntryMode}, +}; +use gix_worktree::stack::state::attributes; +use libfuzzer_sys::{Corpus, fuzz_target}; + +const OPERATION_SIZE: usize = 12; +const MAX_OPERATIONS: usize = 64; +const MAX_INPUT_SIZE: usize = 1 + OPERATION_SIZE * MAX_OPERATIONS; +const COMPONENTS: [&str; 8] = ["a", "b", "c", "d", "e", "f", "g", "h"]; +const PAYLOAD_COUNT: usize = 8; + +type ObjectDb = gix_odb::memory::Proxy; + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct Path { + len: u8, + components: [u8; 3], +} + +impl Path { + fn from_bytes(bytes: &[u8]) -> Self { + Path { + len: 1 + bytes[0] % 3, + components: [bytes[1] % 8, bytes[2] % 8, bytes[3] % 8], + } + } + + fn is_prefix_of(self, other: Path) -> bool { + self.len <= other.len && self.components[..self.len as usize] == other.components[..self.len as usize] + } + + fn below(self, source: Path, destination: Path) -> Option { + if !source.is_prefix_of(self) { + return None; + } + let suffix_len = self.len - source.len; + let len = destination.len + suffix_len; + if len > 3 { + return None; + } + + let mut components = destination.components; + components[destination.len as usize..len as usize] + .copy_from_slice(&self.components[source.len as usize..self.len as usize]); + Some(Path { len, components }) + } + + fn components(self) -> impl Iterator { + self.components + .into_iter() + .take(self.len as usize) + .map(|component| COMPONENTS[component as usize]) + } +} + +#[derive(Clone, Copy)] +enum Entry { + Blob(u8), + Executable(u8), + Link(u8), + Commit(u8), +} + +impl Entry { + fn from_bytes(mode: u8, payload: u8) -> Self { + let payload = payload % PAYLOAD_COUNT as u8; + match mode % 4 { + 0 => Entry::Blob(payload), + 1 => Entry::Executable(payload), + 2 => Entry::Link(payload), + _ => Entry::Commit(payload), + } + } + + fn mode(self) -> EntryMode { + match self { + Entry::Blob(_) => EntryKind::Blob.into(), + Entry::Executable(_) => EntryKind::BlobExecutable.into(), + Entry::Link(_) => EntryKind::Link.into(), + Entry::Commit(_) => EntryKind::Commit.into(), + } + } + + fn payload(self) -> usize { + match self { + Entry::Blob(payload) | Entry::Executable(payload) | Entry::Link(payload) | Entry::Commit(payload) => { + payload as usize + } + } + } +} + +#[derive(Clone, Copy)] +enum Action { + Set, + Remove, + Modify, + Rename, +} + +#[derive(Clone, Copy)] +struct Operation { + action: Action, + path: Path, + destination: Path, + entry: Entry, +} + +impl Operation { + fn from_bytes(bytes: &[u8]) -> Self { + Operation { + action: match bytes[1] % 4 { + 0 => Action::Set, + 1 => Action::Remove, + 2 => Action::Modify, + _ => Action::Rename, + }, + path: Path::from_bytes(&bytes[2..6]), + destination: Path::from_bytes(&bytes[6..10]), + entry: Entry::from_bytes(bytes[10], bytes[11]), + } + } +} + +#[derive(Clone, Default)] +struct State(BTreeMap); + +impl State { + fn apply(&mut self, operation: Operation) { + match operation.action { + Action::Set => self.set(operation.path, operation.entry), + Action::Remove => self.remove(operation.path), + Action::Modify => self.modify(operation.path, operation.entry), + Action::Rename => self.rename(operation.path, operation.destination), + } + } + + fn set(&mut self, path: Path, entry: Entry) { + self.0 + .retain(|existing, _| !existing.is_prefix_of(path) && !path.is_prefix_of(*existing)); + self.0.insert(path, entry); + } + + fn remove(&mut self, path: Path) { + self.0.retain(|existing, _| !path.is_prefix_of(*existing)); + } + + fn modify(&mut self, path: Path, entry: Entry) { + let target = self + .0 + .contains_key(&path) + .then_some(path) + .or_else(|| self.0.keys().copied().find(|candidate| path.is_prefix_of(*candidate))); + if let Some(target) = target { + self.0.insert(target, entry); + } + } + + fn rename(&mut self, source: Path, destination: Path) { + let moved: Vec<_> = self + .0 + .iter() + .filter_map(|(path, entry)| path.below(source, destination).map(|path| (path, *entry))) + .collect(); + if moved.is_empty() { + return; + } + + self.remove(source); + for (path, entry) in moved { + self.set(path, entry); + } + } +} + +struct Objects { + db: ObjectDb, + blobs: [ObjectId; PAYLOAD_COUNT], + commits: [ObjectId; PAYLOAD_COUNT], +} + +impl Objects { + fn new() -> Self { + let db = ObjectDb::new(gix_object::find::Never, gix_hash::Kind::Sha1); + let empty_tree = db.write(&Tree::default()).expect("the in-memory tree can be written"); + let blobs = std::array::from_fn(|index| { + let data = format!("binary payload {index}\0\n"); + db.write_buf(Kind::Blob, data.as_bytes()) + .expect("the in-memory blob can be written") + }); + let commits = std::array::from_fn(|index| { + let data = format!( + "tree {empty_tree}\nauthor A 0 +0000\ncommitter A 0 +0000\n\ncommit {index}\n" + ); + db.write_buf(Kind::Commit, data.as_bytes()) + .expect("the in-memory commit can be written") + }); + Objects { db, blobs, commits } + } + + fn id(&self, entry: Entry) -> ObjectId { + match entry { + Entry::Commit(_) => self.commits[entry.payload()], + Entry::Blob(_) | Entry::Executable(_) | Entry::Link(_) => self.blobs[entry.payload()], + } + } + + fn write_tree(&self, state: &State) -> ObjectId { + let mut editor = gix_object::tree::Editor::new(Tree::default(), &gix_object::find::Never, gix_hash::Kind::Sha1); + for (path, entry) in &state.0 { + editor + .upsert(path.components(), entry.mode().kind(), self.id(*entry)) + .expect("generated paths form a valid tree"); + } + editor + .write(|tree| self.db.write(tree)) + .expect("the generated tree can be written") + } +} + +fn fuzz(data: &[u8]) { + let Some((&configuration, operations)) = data.split_first() else { + return; + }; + + let mut base = State::default(); + let mut ours = Vec::new(); + let mut theirs = Vec::new(); + // A small component alphabet makes unrelated operations collide often. Set, remove, modify, + // and subtree rename records can thereby form additions, type changes, file/directory + // replacements, and rename interactions on either side of the same valid base tree. + for bytes in operations.chunks_exact(OPERATION_SIZE).take(MAX_OPERATIONS) { + let operation = Operation::from_bytes(bytes); + match bytes[0] % 3 { + 0 => base.set(operation.path, operation.entry), + 1 => ours.push(operation), + _ => theirs.push(operation), + } + } + + let mut ours_state = base.clone(); + for operation in ours { + ours_state.apply(operation); + } + let mut theirs_state = base.clone(); + for operation in theirs { + theirs_state.apply(operation); + } + + let objects = Objects::new(); + let base = objects.write_tree(&base); + let ours = objects.write_tree(&ours_state); + let theirs = objects.write_tree(&theirs_state); + let options = options(configuration); + let mut diff_state = gix_diff::tree::State::default(); + let mut diff_resource_cache = new_diff_resource_cache(); + let mut blob_merge = new_blob_merge_platform(); + + for (current, other) in [(ours, theirs), (theirs, ours)] { + let outcome = gix_merge::tree( + &base, + ¤t, + &other, + gix_merge::blob::builtin_driver::text::Labels { + ancestor: Some("BASE".into()), + current: Some("OURS".into()), + other: Some("THEIRS".into()), + }, + &objects.db, + |buf| objects.db.write_buf(Kind::Blob, buf), + &mut diff_state, + &mut diff_resource_cache, + &mut blob_merge, + options.clone(), + ); + let mut outcome = match outcome { + Ok(outcome) => outcome, + // Resolving a binary add/add conflict with its absent ancestor cannot + // produce a resource. This is a valid configuration-dependent error. + Err(gix_merge::tree::Error::MergeResourceNotFound) => continue, + Err(err) => panic!("generated trees and objects are valid: {err:?}"), + }; + outcome + .tree + .write(|tree| objects.db.write(tree)) + .expect("the merged tree remains valid"); + } +} + +fn options(configuration: u8) -> Options { + let binary_resolution = |value| match value % 4 { + 0 => None, + 1 => Some(binary::ResolveWith::Ancestor), + 2 => Some(binary::ResolveWith::Ours), + _ => Some(binary::ResolveWith::Theirs), + }; + let mut options = Options { + // Identity-only rewrite tracking exercises rename handling without invoking blob similarity + // diffing. Rename operations preserve object IDs so they remain discoverable. + rewrites: Some(Rewrites { + copies: None, + percentage: Some(1.0), + limit: 0, + track_empty: false, + }), + fail_on_conflict: (configuration & 0b1000_0000 != 0).then(TreatAsUnresolved::git), + marker_size_multiplier: configuration % 4, + symlink_conflicts: binary_resolution(configuration >> 2), + tree_conflicts: match (configuration >> 4) % 3 { + 0 => None, + 1 => Some(ResolveWith::Ancestor), + _ => Some(ResolveWith::Ours), + }, + ..Default::default() + }; + options.blob_merge.resolve_binary_with = binary_resolution(configuration); + // All generated blobs are binary by both the NUL-byte and size rules, so tree fuzzing never + // reaches text diffing. Keep Histogram explicit as a final guard if that invariant changes. + options.blob_merge.text.diff_algorithm = imara_diff::Algorithm::Histogram; + options +} + +fn new_diff_resource_cache() -> gix_diff::blob::Platform { + gix_diff::blob::Platform::new( + Default::default(), + gix_diff::blob::Pipeline::new(Default::default(), Default::default(), Vec::new(), Default::default()), + Default::default(), + gix_worktree::Stack::new( + FsPath::new("gix-merge-tree-fuzz-no-worktree"), + gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::default()), + Default::default(), + Vec::new(), + Vec::new(), + ), + ) +} + +fn new_blob_merge_platform() -> gix_merge::blob::Platform { + let attributes = gix_worktree::Stack::new( + FsPath::new("gix-merge-tree-fuzz-no-worktree"), + gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::new( + Default::default(), + None, + attributes::Source::WorktreeThenIdMapping, + Default::default(), + )), + gix_worktree::glob::pattern::Case::Sensitive, + Vec::new(), + Vec::new(), + ); + gix_merge::blob::Platform::new( + gix_merge::blob::Pipeline::new( + Default::default(), + gix_filter::Pipeline::default(), + gix_merge::blob::pipeline::Options { + large_file_threshold_bytes: 1, + }, + ), + gix_merge::blob::pipeline::Mode::ToGit, + attributes, + vec![], + Default::default(), + ) +} + +fuzz_target!(|data: &[u8]| -> Corpus { + if data.len() > MAX_INPUT_SIZE { + return Corpus::Reject; + } + fuzz(data); + Corpus::Keep +}); diff --git a/gix-merge/tests/merge/tree/cartesian-baseline.txt b/gix-merge/tests/merge/tree/cartesian-baseline.txt index cbbda0b1dea..7b73c088842 100644 --- a/gix-merge/tests/merge/tree/cartesian-baseline.txt +++ b/gix-merge/tests/merge/tree/cartesian-baseline.txt @@ -10,6 +10,7 @@ Git results come from `git merge-tree --write-tree`, which uses merge-ORT. The gix ours policy combines tree `ResolveWith::Ours` with text `ResolveWithOurs`, as the manual baseline does. Git's `-Xours` is not equivalent: it favors content and symlink conflicts but does not force all tree conflicts. The ours policy is directional, so both directions are checked independently rather than required to be identical. +Ancestor resolution, early exit, and disabled rename detection are evaluated independently of Git. Exact-tree inverse differences include directional conflict-marker ordering; shape, conflict, and payload symmetry distinguish those presentation differences from structural or content loss. @@ -37,6 +38,17 @@ gix ours unchanged default-clean merges: 124/124 gix ours current-side payload retention: 120/120 gix ours conflict provenance retained: 86/86 gix ours clean-oracle passes: 53/53 +gix ancestor unresolved directional merges: 32/210 +gix ancestor unchanged default-clean merges: 122/124 +gix ancestor conflict provenance retained: 86/86 +gix ancestor clean-oracle passes: 53/53 +gix early-exit conflicting directional checks: 86/86 +gix early-exit clean directional checks: 124/124 +gix no-rewrites unresolved directional merges: 54/210 +gix no-rewrites inverse path-and-mode symmetry: 105/105 +gix no-rewrites inverse conflict symmetry: 105/105 +gix no-rewrites inverse payload symmetry: 105/105 +gix no-rewrites clean-oracle passes: 53/53 ## Git/gix differences (0) - none @@ -121,3 +133,28 @@ gix ours clean-oracle passes: 53/53 ## gix ours clean-oracle failures (0) - none + +## gix ancestor changes to default-clean merges (2) +- rename-free-a + rename-modify-free-a A+B +- rename-free-a + rename-modify-free-a B+A + +## gix ancestor forgotten conflict provenance (0) +- none + +## gix ancestor clean-oracle failures (0) +- none + +## gix early-exit failures (0) +- none + +## gix no-rewrites inverse path-and-mode differences (0) +- none + +## gix no-rewrites inverse conflict differences (0) +- none + +## gix no-rewrites inverse payload differences (0) +- none + +## gix no-rewrites clean-oracle failures (0) +- none diff --git a/gix-merge/tests/merge/tree/cartesian.rs b/gix-merge/tests/merge/tree/cartesian.rs index 097e96d2813..e0953e0b8c1 100644 --- a/gix-merge/tests/merge/tree/cartesian.rs +++ b/gix-merge/tests/merge/tree/cartesian.rs @@ -29,6 +29,9 @@ struct Result { tree: Tree, conflicted: bool, conflicted_with_forced_resolution: bool, + failed_on_first_unresolved_conflict: bool, + conflict_count: usize, + last_conflict_unresolved: bool, } /// Record the current behavior of merge-ORT and gix for a finite Cartesian model of tree changes. @@ -80,6 +83,12 @@ fn records_status_quo_sha1() -> crate::Result { gix_merge::blob::builtin_driver::text::Conflict::ResolveWithOurs; let git_kind = gix_merge::tree::TreatAsUnresolved::git(); let forced_resolution = gix_merge::tree::TreatAsUnresolved::forced_resolution(); + let mut ancestor_options = options.clone(); + ancestor_options.tree_merge.tree_conflicts = Some(gix_merge::tree::ResolveWith::Ancestor); + let mut fail_fast_options = options.clone(); + fail_fast_options.tree_merge.fail_on_conflict = Some(git_kind); + let mut no_rewrites_options = options.clone(); + no_rewrites_options.tree_merge.rewrites = None; let mut commit_trees = BTreeMap::::new(); let mut exact_agreement = 0; @@ -116,6 +125,22 @@ fn records_status_quo_sha1() -> crate::Result { let mut ours_conflict_provenance_checks = 0; let mut ours_oracle_passes = 0; let mut ours_oracle_failures = Vec::new(); + let mut ancestor_unresolved = 0; + let mut ancestor_changed_clean_merge = Vec::new(); + let mut ancestor_forgot_conflict = Vec::new(); + let mut ancestor_clean_directional_merges = 0; + let mut ancestor_conflict_provenance_checks = 0; + let mut ancestor_oracle_passes = 0; + let mut ancestor_oracle_failures = Vec::new(); + let mut fail_fast_failures = Vec::new(); + let mut fail_fast_conflict_checks = 0; + let mut fail_fast_clean_checks = 0; + let mut no_rewrites_conflicted = 0; + let mut no_rewrites_inverse_shape = Vec::new(); + let mut no_rewrites_inverse_conflict = Vec::new(); + let mut no_rewrites_inverse_payload = Vec::new(); + let mut no_rewrites_oracle_passes = 0; + let mut no_rewrites_oracle_failures = Vec::new(); for case in &cases { let case_name = format!("{} + {}", case.left_operation, case.right_operation); @@ -154,12 +179,21 @@ fn records_status_quo_sha1() -> crate::Result { .tree_merge; let conflicted = outcome.has_unresolved_conflicts(git_kind); let conflicted_with_forced_resolution = outcome.has_unresolved_conflicts(forced_resolution); + let failed_on_first_unresolved_conflict = outcome.failed_on_first_unresolved_conflict; + let conflict_count = outcome.conflicts.len(); + let last_conflict_unresolved = outcome + .conflicts + .last() + .is_some_and(|conflict| conflict.is_unresolved(git_kind)); let tree_id = outcome.tree.write(|tree| objects.write(tree))?; Ok(Result { tree_id, tree: flatten_tree(tree_id, &objects)?, conflicted, conflicted_with_forced_resolution, + failed_on_first_unresolved_conflict, + conflict_count, + last_conflict_unresolved, }) }; let gix_forward = run_gix( @@ -190,6 +224,48 @@ fn records_status_quo_sha1() -> crate::Result { &format!("A-{}", case.left_operation), &force_ours_options, )?; + let ancestor_forward = run_gix( + case.left_commit, + case.right_commit, + &format!("A-{}", case.left_operation), + &format!("B-{}", case.right_operation), + &ancestor_options, + )?; + let ancestor_reverse = run_gix( + case.right_commit, + case.left_commit, + &format!("B-{}", case.right_operation), + &format!("A-{}", case.left_operation), + &ancestor_options, + )?; + let fail_fast_forward = run_gix( + case.left_commit, + case.right_commit, + &format!("A-{}", case.left_operation), + &format!("B-{}", case.right_operation), + &fail_fast_options, + )?; + let fail_fast_reverse = run_gix( + case.right_commit, + case.left_commit, + &format!("B-{}", case.right_operation), + &format!("A-{}", case.left_operation), + &fail_fast_options, + )?; + let no_rewrites_forward = run_gix( + case.left_commit, + case.right_commit, + &format!("A-{}", case.left_operation), + &format!("B-{}", case.right_operation), + &no_rewrites_options, + )?; + let no_rewrites_reverse = run_gix( + case.right_commit, + case.left_commit, + &format!("B-{}", case.right_operation), + &format!("A-{}", case.left_operation), + &no_rewrites_options, + )?; let mut difference = Vec::new(); let mut git_payload_presence = [Vec::new(), Vec::new()]; @@ -305,6 +381,69 @@ fn records_status_quo_sha1() -> crate::Result { } } + for (direction, normal, ancestor, fail_fast) in [ + ("A+B", &gix_forward, &ancestor_forward, &fail_fast_forward), + ("B+A", &gix_reverse, &ancestor_reverse, &fail_fast_reverse), + ] { + ancestor_unresolved += usize::from(ancestor.conflicted); + if !normal.conflicted { + ancestor_clean_directional_merges += 1; + if ancestor.tree_id != normal.tree_id { + ancestor_changed_clean_merge.push(format!("{case_name} {direction}")); + } + fail_fast_clean_checks += 1; + if fail_fast.failed_on_first_unresolved_conflict + || fail_fast.conflicted + || fail_fast.tree_id != normal.tree_id + { + fail_fast_failures.push(format!("{case_name} {direction}: changed a clean merge")); + } + } else { + ancestor_conflict_provenance_checks += 1; + if !ancestor.conflicted_with_forced_resolution { + ancestor_forgot_conflict.push(format!("{case_name} {direction}")); + } + fail_fast_conflict_checks += 1; + if !fail_fast.failed_on_first_unresolved_conflict + || !fail_fast.conflicted + || fail_fast.conflict_count == 0 + || !fail_fast.last_conflict_unresolved + || fail_fast.conflict_count > normal.conflict_count + { + fail_fast_failures.push(format!( + "{case_name} {direction}: did not stop with one unresolved conflict last" + )); + } + } + } + + no_rewrites_conflicted += + usize::from(no_rewrites_forward.conflicted) + usize::from(no_rewrites_reverse.conflicted); + if !same_shape(&no_rewrites_forward.tree, &no_rewrites_reverse.tree) { + no_rewrites_inverse_shape.push(case_name.clone()); + } + if no_rewrites_forward.conflicted != no_rewrites_reverse.conflicted { + no_rewrites_inverse_conflict.push(case_name.clone()); + } + let mut no_rewrites_payload_presence = [Vec::new(), Vec::new()]; + for (direction_idx, result) in [&no_rewrites_forward, &no_rewrites_reverse].into_iter().enumerate() { + for (side, operation) in [ + ("A", case.left_operation.as_str()), + ("B", case.right_operation.as_str()), + ] { + if let Some(payload) = payload(side, operation) { + no_rewrites_payload_presence[direction_idx].push(contains( + &result.tree, + payload.as_bytes(), + &objects, + )?); + } + } + } + if no_rewrites_payload_presence[0] != no_rewrites_payload_presence[1] { + no_rewrites_inverse_payload.push(case_name.clone()); + } + if let Some(ideal) = unambiguous_merge(&base_tree, &left_tree, &right_tree) { oracle_cases += 1; let git_ok = [&git_forward, &git_reverse] @@ -331,6 +470,22 @@ fn records_status_quo_sha1() -> crate::Result { } else { ours_oracle_failures.push(case_name); } + if [&ancestor_forward, &ancestor_reverse] + .into_iter() + .all(|result| !result.conflicted && result.tree == ideal) + { + ancestor_oracle_passes += 1; + } else { + ancestor_oracle_failures.push(format!("{} + {}", case.left_operation, case.right_operation)); + } + if [&no_rewrites_forward, &no_rewrites_reverse] + .into_iter() + .all(|result| !result.conflicted && result.tree == ideal) + { + no_rewrites_oracle_passes += 1; + } else { + no_rewrites_oracle_failures.push(format!("{} + {}", case.left_operation, case.right_operation)); + } } } @@ -354,6 +509,37 @@ fn records_status_quo_sha1() -> crate::Result { ours_forgot_conflict.is_empty(), "forced resolutions must remain visible to the strict conflict policy: {ours_forgot_conflict:#?}" ); + assert!( + ancestor_forgot_conflict.is_empty(), + "ancestor resolutions must remain visible to the strict conflict policy: {ancestor_forgot_conflict:#?}" + ); + assert!( + fail_fast_failures.is_empty(), + "early exit must stop exactly when the full merge encounters its first unresolved conflict: \ + {fail_fast_failures:#?}" + ); + assert!( + no_rewrites_inverse_shape.is_empty(), + "disabling rename detection must not make tree shape depend on side order: {no_rewrites_inverse_shape:#?}" + ); + assert!( + no_rewrites_inverse_conflict.is_empty(), + "disabling rename detection must not make conflict status depend on side order: \ + {no_rewrites_inverse_conflict:#?}" + ); + assert!( + no_rewrites_inverse_payload.is_empty(), + "disabling rename detection must not make payload retention depend on side order: \ + {no_rewrites_inverse_payload:#?}" + ); + assert!( + ancestor_oracle_failures.is_empty(), + "ancestor resolution must preserve every independently unambiguous merge: {ancestor_oracle_failures:#?}" + ); + assert!( + no_rewrites_oracle_failures.is_empty(), + "disabling rename detection must preserve every independently unambiguous merge: {no_rewrites_oracle_failures:#?}" + ); let directional_merges = cases.len() * 2; let mut report = String::new(); @@ -405,6 +591,10 @@ fn records_status_quo_sha1() -> crate::Result { report, "The ours policy is directional, so both directions are checked independently rather than required to be identical." )?; + writeln!( + report, + "Ancestor resolution, early exit, and disabled rename detection are evaluated independently of Git." + )?; writeln!( report, "Exact-tree inverse differences include directional conflict-marker ordering; shape, conflict, and payload" @@ -524,6 +714,62 @@ fn records_status_quo_sha1() -> crate::Result { report, "gix ours clean-oracle passes: {ours_oracle_passes}/{oracle_cases}" )?; + writeln!( + report, + "gix ancestor unresolved directional merges: {ancestor_unresolved}/{directional_merges}" + )?; + writeln!( + report, + "gix ancestor unchanged default-clean merges: {}/{}", + ancestor_clean_directional_merges - ancestor_changed_clean_merge.len(), + ancestor_clean_directional_merges + )?; + writeln!( + report, + "gix ancestor conflict provenance retained: {}/{}", + ancestor_conflict_provenance_checks - ancestor_forgot_conflict.len(), + ancestor_conflict_provenance_checks + )?; + writeln!( + report, + "gix ancestor clean-oracle passes: {ancestor_oracle_passes}/{oracle_cases}" + )?; + writeln!( + report, + "gix early-exit conflicting directional checks: {}/{}", + fail_fast_conflict_checks - fail_fast_failures.len(), + fail_fast_conflict_checks + )?; + writeln!( + report, + "gix early-exit clean directional checks: {fail_fast_clean_checks}/{fail_fast_clean_checks}" + )?; + writeln!( + report, + "gix no-rewrites unresolved directional merges: {no_rewrites_conflicted}/{directional_merges}" + )?; + writeln!( + report, + "gix no-rewrites inverse path-and-mode symmetry: {}/{}", + cases.len() - no_rewrites_inverse_shape.len(), + cases.len() + )?; + writeln!( + report, + "gix no-rewrites inverse conflict symmetry: {}/{}", + cases.len() - no_rewrites_inverse_conflict.len(), + cases.len() + )?; + writeln!( + report, + "gix no-rewrites inverse payload symmetry: {}/{}", + cases.len() - no_rewrites_inverse_payload.len(), + cases.len() + )?; + writeln!( + report, + "gix no-rewrites clean-oracle passes: {no_rewrites_oracle_passes}/{oracle_cases}" + )?; write_list(&mut report, "Git/gix differences", &implementation_differences)?; write_list(&mut report, "Git inverse exact-tree differences", &git_inverse_exact)?; @@ -555,6 +801,42 @@ fn records_status_quo_sha1() -> crate::Result { &ours_forgot_conflict, )?; write_list(&mut report, "gix ours clean-oracle failures", &ours_oracle_failures)?; + write_list( + &mut report, + "gix ancestor changes to default-clean merges", + &ancestor_changed_clean_merge, + )?; + write_list( + &mut report, + "gix ancestor forgotten conflict provenance", + &ancestor_forgot_conflict, + )?; + write_list( + &mut report, + "gix ancestor clean-oracle failures", + &ancestor_oracle_failures, + )?; + write_list(&mut report, "gix early-exit failures", &fail_fast_failures)?; + write_list( + &mut report, + "gix no-rewrites inverse path-and-mode differences", + &no_rewrites_inverse_shape, + )?; + write_list( + &mut report, + "gix no-rewrites inverse conflict differences", + &no_rewrites_inverse_conflict, + )?; + write_list( + &mut report, + "gix no-rewrites inverse payload differences", + &no_rewrites_inverse_payload, + )?; + write_list( + &mut report, + "gix no-rewrites clean-oracle failures", + &no_rewrites_oracle_failures, + )?; let baseline = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/merge/tree/cartesian-baseline.txt"); if std::env::var_os("GIX_MERGE_UPDATE_CARTESIAN_BASELINE").is_some() { @@ -603,6 +885,9 @@ fn git_result( tree: flatten_tree(tree_id, objects)?, conflicted, conflicted_with_forced_resolution: conflicted, + failed_on_first_unresolved_conflict: false, + conflict_count: usize::from(conflicted), + last_conflict_unresolved: conflicted, }) } From 1c5086ec69d5eafd2f2c4507a261d44c806a5197 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 31 Jul 2026 16:29:50 +0200 Subject: [PATCH 2/4] Refactor tree-merge change matching and resolution. The tree-merge implementation previously combined side-diff collection, path matching, scheduling, and the complete conflict-resolution matrix in a single function. It also duplicated change collection for both sides and represented pair outcomes with independent boolean flags. Keep the public tree() entry point as a small facade and move the merge engine into focused private modules. Collect each ancestor-to-side diff through one helper that constructs a SideState containing the flat change list and its matching tree together. Isolate path and rename candidate matching, including identical-change suppression, from classification into pairs that the resolution matrix understands. Represent the result of handling each paired change explicitly as a ChangeDisposition. This preserves the important distinction between a change that was merely processed and one whose effect is present in the editor, without duplicating the final state transition in the scheduler. Replace the type-specific side-picking helpers with generic pick() and pick_mut() helpers as well. These boundaries make the state machine easier to review and reduce the chance that a future rename or forced-resolution fix accidentally changes collection, matching, and application at once. They also remove duplicated setup while keeping the exhaustive resolution match in one place, where its symmetry remains visible. The public API and all recorded merge results remain unchanged. Fixes and Improvements ---------------------- Tree merging combines a flat change schedule with per-side path indexes. Valid Git operations can therefore arrive in different orders or expose structural relationships before the leaf changes that ultimately apply them. Several resolver branches treated those relationships as physical occupancy or as content changes for the same identity, leading to hangs, assertions, duplicate entries, lost siblings, or merge results that depended on diff and side order. Separate unique-path occupancy from PassedRewrittenDirectory scheduling so a side-qualified name can terminate below directory rewrites. Prune empty path nodes back to the root, and allow a deferred rewrite to insert only its new destination because its source is already indexed. Resolve the structural cases at their actual identity boundaries: - handle an added file blocking an added directory before mode-specific add/add resolution and defer early descendants until their parent deletion runs; - keep explicit file renames ahead of inferred directory renames, and keep directory replacements at their explicit sources; - treat file replacements of incompatible non-blob ancestors as additions with an empty compatible merge base; - pair shared deletions before descendants and allow file renames into paths vacated by directory renames; - preserve unrelated nested or overlapping rename destinations by keeping the directory in place and moving only the blocking file; - reject incompatible same-destination rewrites before blob merging, while collapsing identical rewrites to one clean shared destination; and - defer file-to-directory children until the parent rename/delete decision is made exactly once. Forced Ancestor and Ours resolution continues to apply only the selected side. Git-backed baselines cover both directions, forced policies, modes, symlinks, gitlinks, nested directories, and documented index-only deviations. The resulting suite contains 155 directional baseline cases, and the Cartesian model reaches 210/210 Git/gix agreement for trees and path/mode results. More Hardening -------------- Deferred tree changes may be reconsidered after another conflict has already consumed or pruned the same path-tree node. This is valid when rename detection has ambiguous identical sources, when structural conflicts overlap, or when a change follows a detected directory rename. The editor and conflict records still contain the required state, but strict bookkeeping removals and older same-path assertions turned these schedules into debug panics, hangs, or side-order-dependent duplicate content. Make cleanup idempotent wherever absence is already the required end state: add/add type conflicts, same-source rewrites, blocking conflict destinations, delete/rewrite sources, and changes deferred through directory renames. Accept cross-path structural matches from ambiguous rewrite candidates and let the existing conservative unknown-conflict fallback handle them. Preserve each rewrite input mode when blob content is identical so executable mode changes remain visible to the merge. Make unique-path selection respect childless tracked directories and qualify the first blocking file component, which guarantees termination instead of varying an ineffective descendant suffix forever. Finally, when a deferred addition is relocated to a unique conflict path, remove its temporary original path from the side index before marking it processed so a later descendant cannot relocate the same content a second time. The minimized and accumulated fuzz inputs now complete without failure. Git-backed regressions cover ambiguous sources, consumed nodes, repeated rename/delete candidates, mode-only rewrite collisions, unique paths below files, and nested rename destinations in both side orderings. The final tree baseline contains 165 directional cases with 130 intentionally skipped forced resolution checks, and reversing the nested relocation case retains exactly `a/a/a` and `a~A` without inventing `a~A_0`. Assisted-by: GPT 5.6 Co-authored-by: GPT 5.6 --- gix-merge/benches/tree.rs | 39 +- gix-merge/src/tree/function.rs | 1613 ----------------- gix-merge/src/tree/function/change.rs | 206 +++ gix-merge/src/tree/function/mod.rs | 4 + gix-merge/src/tree/utils.rs | 217 ++- .../generated-archives/tree-baseline.tar | Bin 4077568 -> 5710336 bytes .../tree-baseline_sha256.tar | Bin 4464640 -> 6298624 bytes gix-merge/tests/fixtures/tree-baseline.sh | 966 ++++++++++ gix-merge/tests/merge/tree/mod.rs | 6 +- 9 files changed, 1387 insertions(+), 1664 deletions(-) delete mode 100644 gix-merge/src/tree/function.rs create mode 100644 gix-merge/src/tree/function/change.rs create mode 100644 gix-merge/src/tree/function/mod.rs diff --git a/gix-merge/benches/tree.rs b/gix-merge/benches/tree.rs index 94a3d985f1f..3d2f403a0f1 100644 --- a/gix-merge/benches/tree.rs +++ b/gix-merge/benches/tree.rs @@ -50,23 +50,28 @@ fn tree_merge(c: &mut Criterion) { let mut group = c.benchmark_group("tree-merge/mixed-structural-cases"); group.throughput(Throughput::Elements(CASE_COUNT)); - group.bench_function("merge", |b| { - b.iter_batched( - || options.clone(), - |options| { - black_box(merge( - &scenario, - scenario.ours, - scenario.theirs, - &mut diff_state, - &mut diff_resource_cache, - &mut blob_merge, - options, - )) - }, - BatchSize::SmallInput, - ); - }); + for (name, ours, theirs) in [ + ("ours-theirs", scenario.ours, scenario.theirs), + ("theirs-ours", scenario.theirs, scenario.ours), + ] { + group.bench_function(name, |b| { + b.iter_batched( + || options.clone(), + |options| { + black_box(merge( + &scenario, + ours, + theirs, + &mut diff_state, + &mut diff_resource_cache, + &mut blob_merge, + options, + )) + }, + BatchSize::SmallInput, + ); + }); + } group.finish(); } diff --git a/gix-merge/src/tree/function.rs b/gix-merge/src/tree/function.rs deleted file mode 100644 index 6ef51f7e54c..00000000000 --- a/gix-merge/src/tree/function.rs +++ /dev/null @@ -1,1613 +0,0 @@ -use std::{borrow::Cow, convert::Infallible}; - -use bstr::{BString, ByteSlice}; -use gix_diff::{tree::recorder::Location, tree_with_rewrites::Change}; -use gix_hash::ObjectId; -use gix_object::{ - FindExt, tree, - tree::{EntryKind, EntryMode}, -}; - -use crate::tree::{ - Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, - ConflictMapping::{Original, Swapped}, - ContentMerge, Error, Options, Outcome, Resolution, ResolutionFailure, ResolveWith, - utils::{ - ChangeList, ChangeListRef, PossibleConflict, TrackedChange, TreeNodes, apply_change, perform_blob_merge, - possibly_rewritten_location, rewrite_location_with_renamed_directory, to_components, track, - unique_path_in_tree, - }, -}; - -/// Perform a merge between `our_tree` and `their_tree`, using `base_tree` as merge-base. -/// Note that `base_tree` can be an empty tree to indicate 'no common ancestor between the two sides'. -/// -/// * `labels` are relevant for text-merges and will be shown in conflicts. -/// * `objects` provides access to trees when diffing them. -/// * `write_blob_to_odb(content) -> Result` writes newly merged content into the odb to obtain an id -/// that will be used in merged trees. -/// * `diff_state` is state used for diffing trees. -/// * `diff_resource_cache` is used for similarity checks. -/// * `blob_merge` is a pre-configured platform to merge any content. -/// - Note that it shouldn't be allowed to read from the worktree, given that this is a tree-merge. -/// * `options` are used to affect how the merge is performed. -/// -/// ### Unbiased (Ours x Theirs == Theirs x Ours) -/// -/// The algorithm is implemented so that the result is the same no matter how the sides are ordered. -/// -/// ### Algorithm -/// -/// 1. Diff the ancestor against each side, including rename detection, to obtain two flat lists of tracked changes. -/// 2. Build a path tree for each list. Its nodes point back to list entries and make same-path, tree/non-tree, -/// and renamed-directory interactions discoverable. -/// 3. Start an editor at the ancestor tree and process pending changes from one list against the other list's path tree. -/// A change can be applied directly, paired with another change and merged, consumed only as part of a conflict, or -/// transformed into a deferred change at a rewritten or unique path. -/// 4. Append deferred changes as pending work, then swap the two side-lists and repeat until neither side has pending -/// changes. Swapping the roles is part of keeping the result independent of the original side ordering. -/// -/// Each tracked change therefore records both whether it still needs processing and whether its effect is actually -/// represented in the editor. This is why "processed without application" is distinct from "applied": a forced -/// ancestor resolution may consume a deletion while retaining the ancestor entry, and later path conflicts must not -/// behave as if that deletion had removed it. -/// -/// ### Differences to Merge-ORT -/// -/// Merge-ORT (Git) defines the desired outcomes where are merely mimicked here. The algorithms are different, and it's -/// clear that Merge-ORT is significantly more elaborate and general. -/// -/// It also writes out trees once it's done with them in a form of reduction process, here an editor is used -/// to keep only the changes, to be written by the caller who receives it as part of the result. -/// This may use more memory in the worst case scenario, but in average *shouldn't* perform much worse due to the -/// natural sparsity of the editor. -/// -/// Our rename-tracking also produces copy information, but we discard it and simply treat it like an addition. -/// -/// Finally, our algorithm will consider reasonable solutions to merge-conflicts as conflicts that are resolved, leaving -/// only content with conflict markers as unresolved ones. -/// -/// ### Performance -/// -/// Note that `objects` *should* have an object cache to greatly accelerate tree-retrieval. -#[expect(clippy::too_many_arguments)] -pub fn tree<'objects, E>( - base_tree: &gix_hash::oid, - our_tree: &gix_hash::oid, - their_tree: &gix_hash::oid, - mut labels: crate::blob::builtin_driver::text::Labels<'_>, - objects: &'objects impl gix_object::FindObjectOrHeader, - mut write_blob_to_odb: impl FnMut(&[u8]) -> Result, - diff_state: &mut gix_diff::tree::State, - diff_resource_cache: &mut gix_diff::blob::Platform, - blob_merge: &mut crate::blob::Platform, - options: Options, -) -> Result, Error> -where - E: Into>, -{ - let ours_needs_diff = base_tree != our_tree; - let theirs_needs_diff = base_tree != their_tree; - let _span = gix_trace::coarse!("gix_merge::tree", ?base_tree, ?our_tree, ?their_tree, ?labels); - let (mut base_buf, mut side_buf) = (Vec::new(), Vec::new()); - let ancestor_tree = objects.find_tree(base_tree, &mut base_buf)?; - let mut editor = tree::Editor::new(ancestor_tree.to_owned(), objects, base_tree.kind()); - let ancestor_tree = gix_object::TreeRefIter::from_bytes(&base_buf, base_tree.kind()); - let tree_conflicts = options.tree_conflicts; - - let mut our_changes = Vec::new(); - if ours_needs_diff { - let our_tree = objects.find_tree_iter(our_tree, &mut side_buf)?; - gix_diff::tree_with_rewrites( - ancestor_tree, - our_tree, - diff_resource_cache, - diff_state, - objects, - |change| -> Result<_, Infallible> { - track(change, &mut our_changes); - Ok(std::ops::ControlFlow::Continue(())) - }, - gix_diff::tree_with_rewrites::Options { - location: Some(Location::Path), - rewrites: options.rewrites, - }, - )?; - } - - let mut our_tree = TreeNodes::new(); - for (idx, change) in our_changes.iter().enumerate() { - our_tree.track_change(&change.inner, idx); - } - - let mut their_changes = Vec::new(); - if theirs_needs_diff { - let their_tree = objects.find_tree_iter(their_tree, &mut side_buf)?; - gix_diff::tree_with_rewrites( - ancestor_tree, - their_tree, - diff_resource_cache, - diff_state, - objects, - |change| -> Result<_, Infallible> { - track(change, &mut their_changes); - Ok(std::ops::ControlFlow::Continue(())) - }, - gix_diff::tree_with_rewrites::Options { - location: Some(Location::Path), - rewrites: options.rewrites, - }, - )?; - } - - let mut their_tree = TreeNodes::new(); - for (idx, change) in their_changes.iter().enumerate() { - their_tree.track_change(&change.inner, idx); - } - - let mut conflicts = Vec::new(); - let mut failed_on_first_conflict = false; - let mut should_fail_on_conflict = |mut conflict: Conflict| -> bool { - if tree_conflicts.is_some() { - if let Err(failure) = conflict.resolution { - conflict.resolution = Ok(Resolution::Forced(failure)); - } - } - if let Some(how) = options.fail_on_conflict { - if conflict.resolution.is_err() || conflict.is_unresolved(how) { - failed_on_first_conflict = true; - } - } - conflicts.push(conflict); - failed_on_first_conflict - }; - - let ((mut our_changes, mut our_tree), (mut their_changes, mut their_tree)) = - ((&mut our_changes, &mut our_tree), (&mut their_changes, &mut their_tree)); - let mut outer_side = Original; - if their_changes.is_empty() { - ((our_changes, our_tree), (their_changes, their_tree)) = ((their_changes, their_tree), (our_changes, our_tree)); - (labels.current, labels.other) = (labels.other, labels.current); - outer_side = outer_side.swapped(); - } - - #[derive(Debug)] - enum MatchKind { - /// A tree is supposed to be superseded by something else. - EraseTree, - /// A leaf node is superseded by a tree - EraseLeaf, - } - - 'outer: while their_changes.iter().rev().any(TrackedChange::is_pending) { - let mut segment_start = 0; - let mut last_seen_len = their_changes.len(); - - while segment_start != last_seen_len { - for theirs_idx in segment_start..last_seen_len { - // `their` can be a tree, and it could be used to efficiently prune child-changes as these - // trees are always rewrites with parent ids (of course we validate), so child-changes could be handled - // quickly. However, for now the benefit of having these trees is to have them as part of the match-tree - // on *our* side so that it's clear that we passed a renamed directory (by identity). - let TrackedChange { - inner: theirs, - needs_tree_insertion, - rewritten_location, - .. - } = &their_changes[theirs_idx]; - if theirs.entry_mode().is_tree() || !their_changes[theirs_idx].is_pending() { - continue; - } - - if needs_tree_insertion.is_some() { - their_tree.insert(theirs, theirs_idx); - } - - let candidate = our_tree - .check_conflict( - rewritten_location - .as_ref() - .map_or_else(|| theirs.source_location(), |t| t.0.as_bstr()), - ) - .or_else(|| match theirs { - Change::Rewrite { - source_location, - location, - .. - } if source_location != location => { - our_tree.check_conflict(location.as_bstr()).filter(|candidate| { - candidate - .change_idx() - .is_some_and(|idx| matches!(our_changes[idx].inner, Change::Rewrite { .. })) - }) - } - _ => None, - }); - match candidate.filter(|ours| { - ours.change_idx() - .zip(needs_tree_insertion.flatten()) - .is_none_or(|(ours_idx, ignore_idx)| ours_idx != ignore_idx) - && ours.change_idx().is_none_or(|ours_idx| { - let ours = &mut our_changes[ours_idx]; - if ours.inner == *theirs { - // Identical changes aren't a conflict. Applying `theirs` below also consumes - // our source removal, which must not be applied a second time: a later deletion - // or rewrite could otherwise erase descendants added in the meantime. - if matches!(theirs, Change::Deletion { .. } | Change::Rewrite { .. }) { - ours.mark_applied(); - } - false - } else { - !(ours.was_applied() && matches!(ours.inner, Change::Deletion { .. })) - } - }) - }) { - None => { - if let Some((rewritten_location, ours_idx)) = rewritten_location { - // `no_entry` to the index because that's not a conflict at all, - // but somewhat advanced rename tracking. - if should_fail_on_conflict(Conflict::with_resolution( - Resolution::SourceLocationAffectedByRename { - final_location: rewritten_location.to_owned(), - }, - (&our_changes[*ours_idx].inner, theirs, Original, outer_side), - [None, None, None], - )) { - break 'outer; - } - editor.remove(to_components(theirs.location()))?; - } - apply_change(&mut editor, theirs, rewritten_location.as_ref().map(|t| &t.0))?; - their_changes[theirs_idx].mark_applied(); - } - Some(candidate) => { - use crate::tree::utils::to_components_bstring_ref as toc; - debug_assert!( - rewritten_location.is_none(), - "We should probably handle the case where a rewritten location is passed down here" - ); - - let (ours_idx, match_kind) = match candidate { - PossibleConflict::PassedRewrittenDirectory { change_idx } => { - let ours = &our_changes[change_idx]; - let location_after_passed_rename = - rewrite_location_with_renamed_directory(theirs.location(), &ours.inner); - if let Some(new_location) = location_after_passed_rename { - their_tree.remove_existing_change(theirs.location()); - push_deferred_with_rewrite( - (theirs.clone(), Some(change_idx)), - Some((new_location, change_idx)), - their_changes, - ); - } else { - apply_change(&mut editor, theirs, None)?; - their_changes[theirs_idx].mark_applied(); - } - their_changes[theirs_idx].mark_processed(); - continue; - } - PossibleConflict::TreeToNonTree { change_idx: Some(idx) } - if matches!( - our_changes[idx].inner, - Change::Deletion { .. } | Change::Addition { .. } | Change::Rewrite { .. } - ) => - { - (Some(idx), Some(MatchKind::EraseTree)) - } - PossibleConflict::NonTreeToTree { change_idx } => (change_idx, Some(MatchKind::EraseLeaf)), - PossibleConflict::Match { change_idx: ours_idx } => (Some(ours_idx), None), - _ => (None, None), - }; - - let Some(ours_idx) = ours_idx else { - let ours = match candidate { - PossibleConflict::TreeToNonTree { change_idx, .. } - | PossibleConflict::NonTreeToTree { change_idx, .. } => change_idx, - PossibleConflict::Match { change_idx } - | PossibleConflict::PassedRewrittenDirectory { change_idx } => Some(change_idx), - } - .map(|idx| &mut our_changes[idx]); - - if let Some(ours) = ours { - gix_trace::debug!( - "Turning a case we could probably handle into a conflict for now. theirs: {theirs:#?} ours: {ours:#?} kind: {match_kind:?}" - ); - let conflict = Conflict::unknown((&ours.inner, theirs, Original, outer_side)); - if let Some(ResolveWith::Ours) = tree_conflicts { - apply_our_resolution(&ours.inner, theirs, outer_side, &mut editor)?; - match outer_side { - Original => ours.mark_applied(), - Swapped => their_changes[theirs_idx].mark_applied(), - } - } - if should_fail_on_conflict(conflict) { - break 'outer; - } - } else if matches!(candidate, PossibleConflict::TreeToNonTree { .. }) { - let (mode, id) = theirs.entry_mode_and_id(); - let location = theirs.location(); - let renamed_location = unique_path_in_tree( - location.as_bstr(), - &editor, - their_tree, - labels.other.unwrap_or_default(), - )?; - match tree_conflicts { - None => { - editor.upsert(toc(&renamed_location), mode.kind(), id.to_owned())?; - } - Some(ResolveWith::Ours) => { - if outer_side.is_swapped() { - editor.upsert(to_components(location), mode.kind(), id.to_owned())?; - } - } - Some(ResolveWith::Ancestor) => { - // we found no matching node of 'ours', so nothing to apply here. - } - } - - let conflict = Conflict::without_resolution( - ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { - renamed_unique_path_of_theirs: renamed_location, - }, - (theirs, theirs, Original, outer_side), - [ - None, - None, - index_entry_at_path( - &mode.kind().into(), - &id.to_owned(), - ConflictIndexEntryPathHint::RenamedOrTheirs, - ), - ], - ); - their_changes[theirs_idx].mark_processed(); - if should_fail_on_conflict(conflict) { - break 'outer; - } - } else if matches!(candidate, PossibleConflict::NonTreeToTree { .. }) { - // We are writing on top of what was a file, a conflict we probably already saw and dealt with. - let location = theirs.location(); - let (mode, id) = theirs.entry_mode_and_id(); - editor.upsert(to_components(location), mode.kind(), id.to_owned())?; - their_changes[theirs_idx].mark_applied(); - } else { - gix_trace::debug!( - "Couldn't figure out how to handle {match_kind:?} theirs: {theirs:#?} candidate: {candidate:#?}" - ); - } - continue; - }; - - let mut ours_change_applied = false; - let mut theirs_change_applied = false; - let ours = &our_changes[ours_idx].inner; - match (ours, theirs) { - ( - Change::Modification { - previous_id, - previous_entry_mode, - id: our_id, - location: our_location, - entry_mode: our_mode, - .. - }, - Change::Rewrite { - source_id: their_source_id, - id: their_id, - location: their_location, - entry_mode: their_mode, - source_location, - .. - }, - ) - | ( - Change::Rewrite { - source_id: their_source_id, - id: their_id, - location: their_location, - entry_mode: their_mode, - source_location, - .. - }, - Change::Modification { - previous_id, - previous_entry_mode, - id: our_id, - location: our_location, - entry_mode: our_mode, - .. - }, - ) => { - let side = if matches!(ours, Change::Modification { .. }) { - Original - } else { - Swapped - }; - if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { - debug_assert_eq!( - previous_id, their_source_id, - "both refer to the same base, so should always match" - ); - let their_rewritten_location = possibly_rewritten_location( - pick_our_tree(side, our_tree, their_tree), - their_location.as_ref(), - pick_our_changes(side, our_changes, their_changes), - ); - let renamed_without_change = their_source_id == their_id; - let (merged_blob_id, resolution) = if renamed_without_change { - (*our_id, None) - } else { - let (our_location, our_id, our_mode, their_location, their_id, their_mode) = - match side { - Original => ( - our_location, - our_id, - our_mode, - their_location, - their_id, - their_mode, - ), - Swapped => ( - their_location, - their_id, - their_mode, - our_location, - our_id, - our_mode, - ), - }; - let (merged_blob_id, resolution) = perform_blob_merge( - labels, - objects, - blob_merge, - &mut diff_state.buf1, - &mut write_blob_to_odb, - (our_location, *our_id, *our_mode), - (their_location, *their_id, *their_mode), - (source_location, *previous_id, *previous_entry_mode), - (0, outer_side), - &options, - )?; - (merged_blob_id, Some(resolution)) - }; - - editor.remove(toc(our_location))?; - pick_our_tree(side, our_tree, their_tree) - .remove_existing_change(our_location.as_bstr()); - let final_location = their_rewritten_location.clone(); - let new_change = Change::Addition { - location: their_rewritten_location.unwrap_or_else(|| their_location.to_owned()), - relation: None, - entry_mode: merged_mode, - id: merged_blob_id, - }; - if should_fail_on_conflict(Conflict::with_resolution( - Resolution::OursModifiedTheirsRenamedAndChangedThenRename { - merged_mode: (merged_mode != *their_mode).then_some(merged_mode), - merged_blob: resolution.map(|resolution| ContentMerge { - resolution, - merged_blob_id, - }), - final_location, - }, - (ours, theirs, side, outer_side), - [ - index_entry(previous_entry_mode, previous_id), - index_entry(our_mode, our_id), - index_entry(their_mode, their_id), - ], - )) { - break 'outer; - } - - // The other side gets the addition, not our side. - push_deferred( - (new_change, None), - pick_our_changes_mut(side, their_changes, our_changes), - ); - } else { - match tree_conflicts { - None => { - // keep both states - 'our_location' is the previous location as well. - editor.upsert(toc(our_location), our_mode.kind(), *our_id)?; - editor.upsert(toc(their_location), their_mode.kind(), *their_id)?; - } - Some(ResolveWith::Ours) => { - editor.remove(toc(source_location))?; - if side.to_global(outer_side).is_swapped() { - editor.upsert(toc(their_location), their_mode.kind(), *their_id)?; - } else { - editor.upsert(toc(our_location), our_mode.kind(), *our_id)?; - } - } - Some(ResolveWith::Ancestor) => {} - } - - if should_fail_on_conflict(Conflict::without_resolution( - ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch, - (ours, theirs, side, outer_side), - [ - index_entry_at_path( - previous_entry_mode, - previous_id, - ConflictIndexEntryPathHint::RenamedOrTheirs, - ), - None, - index_entry_at_path( - their_mode, - their_id, - ConflictIndexEntryPathHint::RenamedOrTheirs, - ), - ], - )) { - break 'outer; - } - } - } - ( - Change::Modification { - location, - previous_id, - previous_entry_mode, - entry_mode: our_mode, - id: our_id, - .. - }, - Change::Modification { - entry_mode: their_mode, - id: their_id, - .. - }, - ) if !involves_submodule(our_mode, their_mode) - && merge_modes(*our_mode, *their_mode).is_some() - && our_id != their_id => - { - let (merged_blob_id, resolution) = perform_blob_merge( - labels, - objects, - blob_merge, - &mut diff_state.buf1, - &mut write_blob_to_odb, - (location, *our_id, *our_mode), - (location, *their_id, *their_mode), - (location, *previous_id, *previous_entry_mode), - (0, outer_side), - &options, - )?; - - let merged_mode = merge_modes_prev(*our_mode, *their_mode, *previous_entry_mode) - .expect("BUG: merge_modes() reports a valid mode, this one should do too"); - - editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; - if should_fail_on_conflict(Conflict::with_resolution( - Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { - merged_blob: ContentMerge { - resolution, - merged_blob_id, - }, - }, - (ours, theirs, Original, outer_side), - [ - index_entry(previous_entry_mode, previous_id), - index_entry(our_mode, our_id), - index_entry(their_mode, their_id), - ], - )) { - break 'outer; - } - } - ( - Change::Addition { - location, - entry_mode: our_mode, - id: our_id, - .. - }, - Change::Addition { - entry_mode: their_mode, - id: their_id, - .. - }, - ) if !involves_submodule(our_mode, their_mode) && our_id != their_id => { - let conflict = if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { - let side = if our_mode == their_mode || matches!(our_mode.kind(), EntryKind::Blob) { - outer_side - } else { - outer_side.swapped() - }; - let (merged_blob_id, resolution) = perform_blob_merge( - labels, - objects, - blob_merge, - &mut diff_state.buf1, - &mut write_blob_to_odb, - (location, *our_id, merged_mode), - (location, *their_id, merged_mode), - (location, their_id.kind().null(), merged_mode), - (0, side), - &options, - )?; - editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; - Conflict::with_resolution( - Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { - merged_blob: ContentMerge { - resolution, - merged_blob_id, - }, - }, - (ours, theirs, Original, outer_side), - [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], - ) - } else { - // Actually this has a preference, as symlinks are always left in place with the other side renamed. - let ( - logical_side, - label_of_side_to_be_moved, - (our_mode, our_id, our_path_hint), - (their_mode, their_id, their_path_hint), - ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) { - ( - Original, - labels.other.unwrap_or_default(), - (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), - (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), - ) - } else { - ( - Swapped, - labels.current.unwrap_or_default(), - (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), - (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), - ) - }; - let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree); - let renamed_location = unique_path_in_tree( - location.as_bstr(), - &editor, - tree_with_rename, - label_of_side_to_be_moved, - )?; - let mut conflict = Conflict::without_resolution( - ResolutionFailure::OursAddedTheirsAddedTypeMismatch { - their_unique_location: renamed_location.clone(), - }, - (ours, theirs, logical_side, outer_side), - [ - None, - index_entry_at_path(&our_mode, &our_id, our_path_hint), - index_entry_at_path(&their_mode, &their_id, their_path_hint), - ], - ); - match tree_conflicts { - None => { - let new_change = Change::Addition { - location: renamed_location, - entry_mode: their_mode, - id: their_id, - relation: None, - }; - editor.upsert(toc(location), our_mode.kind(), our_id)?; - tree_with_rename.remove_existing_change(location.as_bstr()); - push_deferred( - (new_change, None), - pick_our_changes_mut(logical_side, their_changes, our_changes), - ); - } - Some(resolve) => { - conflict.entries = Default::default(); - match resolve { - ResolveWith::Ours => match outer_side { - Original => { - editor.upsert(toc(location), our_mode.kind(), our_id)?; - } - Swapped => { - editor.upsert(toc(location), their_mode.kind(), their_id)?; - } - }, - ResolveWith::Ancestor => { - // Do nothing - this discards both sides. - // Note that one of these adds might be the result of a rename, which - // means we effectively loose the original and can't get it back as that information is degenerated. - } - } - } - } - conflict - }; - - if should_fail_on_conflict(conflict) { - break 'outer; - } - } - ( - Change::Modification { - location, - entry_mode, - id, - previous_entry_mode, - previous_id, - }, - Change::Deletion { .. }, - ) - | ( - Change::Deletion { .. }, - Change::Modification { - location, - entry_mode, - id, - previous_entry_mode, - previous_id, - }, - ) => { - let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) { - (labels.current.unwrap_or_default(), Original) - } else { - (labels.other.unwrap_or_default(), Swapped) - }; - let deletion_replaced_by_directory = { - // The deleted leaf is replaced by a dir added at the same location. - // Rename-tracking sort order shouldn't be dependent on here, but maybe - // could one day once rename tracking caught up with Git. - let changes = match side { - Original => &their_changes, - Swapped => &our_changes, - }; - changes.iter().any(|change| { - change.inner.entry_mode().is_tree() - && matches!(change.inner, Change::Addition { .. }) - && change.inner.location() == location - }) - }; - - let should_break = if deletion_replaced_by_directory { - let entries = [ - index_entry(previous_entry_mode, previous_id), - index_entry(entry_mode, id), - None, - ]; - match tree_conflicts { - None => { - let our_tree = pick_our_tree(side, our_tree, their_tree); - let renamed_path = unique_path_in_tree( - location.as_bstr(), - &editor, - our_tree, - label_of_side_to_be_moved, - )?; - editor.remove(toc(location))?; - our_tree.remove_existing_change(location.as_bstr()); - - let new_change = Change::Addition { - location: renamed_path.clone(), - relation: None, - entry_mode: *entry_mode, - id: *id, - }; - let should_break = should_fail_on_conflict(Conflict::without_resolution( - ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { - renamed_unique_path_to_modified_blob: renamed_path, - }, - (ours, theirs, side, outer_side), - entries, - )); - - // Since we move *our* side, our tree needs to be modified. - push_deferred( - (new_change, None), - pick_our_changes_mut(side, our_changes, their_changes), - ); - should_break - } - Some(ResolveWith::Ours) => { - match side.to_global(outer_side) { - Original => { - // ours is modification - editor.upsert(toc(location), entry_mode.kind(), *id)?; - } - Swapped => { - // ours is deletion - editor.remove(toc(location))?; - } - } - should_fail_on_conflict(Conflict::without_resolution( - ResolutionFailure::OursModifiedTheirsDeleted, - (ours, theirs, side, outer_side), - entries, - )) - } - Some(ResolveWith::Ancestor) => { - should_fail_on_conflict(Conflict::without_resolution( - ResolutionFailure::OursModifiedTheirsDeleted, - (ours, theirs, side, outer_side), - entries, - )) - } - } - } else { - let entries = [ - index_entry(previous_entry_mode, previous_id), - index_entry(entry_mode, id), - None, - ]; - match tree_conflicts { - None => { - editor.upsert(toc(location), entry_mode.kind(), *id)?; - } - Some(ResolveWith::Ours) => { - let ours = match outer_side { - Original => ours, - Swapped => theirs, - }; - - match ours { - Change::Modification { .. } => { - editor.upsert(toc(location), entry_mode.kind(), *id)?; - } - Change::Deletion { .. } => { - editor.remove(toc(location))?; - } - _ => unreachable!("parent-match assures this"), - } - } - Some(ResolveWith::Ancestor) => {} - } - should_fail_on_conflict(Conflict::without_resolution( - ResolutionFailure::OursModifiedTheirsDeleted, - (ours, theirs, side, outer_side), - entries, - )) - }; - let deletion_was_applied = match tree_conflicts { - None => deletion_replaced_by_directory, - Some(ResolveWith::Ours) => side.to_global(outer_side).is_swapped(), - Some(ResolveWith::Ancestor) => false, - }; - if deletion_was_applied { - match side { - Original => theirs_change_applied = true, - Swapped => ours_change_applied = true, - } - } - if should_break { - break 'outer; - } - } - ( - Change::Modification { .. }, - Change::Addition { - location, - entry_mode, - id, - .. - }, - ) if ours.location() != theirs.location() => { - match tree_conflicts { - None => { - unreachable!( - "modification/deletion pair should prevent modification/addition from happening" - ) - } - Some(ResolveWith::Ancestor) => {} - Some(ResolveWith::Ours) => { - if outer_side.is_swapped() { - editor.upsert(toc(location), entry_mode.kind(), *id)?; - } - // we have already taken care of the 'root' of this - - // everything that follows can safely be ignored - } - } - } - ( - Change::Rewrite { - source_location: our_source_location, - entry_mode: our_mode, - id: our_id, - location, - .. - }, - Change::Rewrite { - source_location: their_source_location, - entry_mode: their_mode, - id: their_id, - location: their_location, - .. - }, - ) if our_source_location != their_source_location - && location == their_location - && !involves_submodule(our_mode, their_mode) - && merge_modes(*our_mode, *their_mode).is_some() => - { - match tree_conflicts { - None => { - let merged_mode = merge_modes(*our_mode, *their_mode) - .expect("the match guard assures compatible modes"); - let (merged_blob_id, resolution) = perform_blob_merge( - labels, - objects, - blob_merge, - &mut diff_state.buf1, - &mut write_blob_to_odb, - (location, *our_id, merged_mode), - (location, *their_id, merged_mode), - (location, our_id.kind().null(), merged_mode), - (0, outer_side), - &options, - )?; - editor.remove(toc(our_source_location))?; - editor.remove(toc(their_source_location))?; - our_tree.remove_change(our_source_location.as_bstr()); - their_tree.remove_change(their_source_location.as_bstr()); - editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; - if should_fail_on_conflict(Conflict::with_resolution( - Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { - merged_blob: ContentMerge { - resolution, - merged_blob_id, - }, - }, - (ours, theirs, Original, outer_side), - [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], - )) { - break 'outer; - } - } - Some(resolve) => { - if matches!(resolve, ResolveWith::Ours) { - let (source, mode, id, tree) = match outer_side { - Original => (our_source_location, our_mode, our_id, &mut *our_tree), - Swapped => { - (their_source_location, their_mode, their_id, &mut *their_tree) - } - }; - editor.remove(toc(source))?; - tree.remove_change(source.as_bstr()); - editor.upsert(toc(location), mode.kind(), *id)?; - } - if should_fail_on_conflict(Conflict::unknown(( - ours, theirs, Original, outer_side, - ))) { - break 'outer; - } - } - } - } - ( - Change::Rewrite { - source_location, - entry_mode: our_mode, - id: our_id, - location, - .. - }, - Change::Addition { - id: their_id, - entry_mode: their_mode, - location: add_location, - .. - }, - ) - | ( - Change::Addition { - id: their_id, - entry_mode: their_mode, - location: add_location, - .. - }, - Change::Rewrite { - source_location, - entry_mode: our_mode, - id: our_id, - location, - .. - }, - ) if add_location - .strip_prefix(source_location.as_bytes()) - .is_some_and(|suffix| suffix.starts_with(b"/")) => - { - // The rewrite moves the file out of the way while the other side replaces it - // with a directory. The child is unrelated to the rewritten blob, so keep both - // instead of merging their contents at the rewrite destination. The preceding - // deletion/rewrite pairing already recorded the rename/delete conflict. - let side = if matches!(ours, Change::Rewrite { .. }) { - Original - } else { - Swapped - }; - match tree_conflicts { - None => { - editor.remove(toc(source_location))?; - pick_our_tree(side, our_tree, their_tree) - .remove_change(source_location.as_bstr()); - editor.upsert(toc(location), our_mode.kind(), *our_id)?; - editor.upsert(toc(add_location), their_mode.kind(), *their_id)?; - ours_change_applied = true; - theirs_change_applied = true; - } - Some(ResolveWith::Ours) => match side.to_global(outer_side) { - Original => { - editor.remove(toc(source_location))?; - editor.upsert(toc(location), our_mode.kind(), *our_id)?; - match side { - Original => ours_change_applied = true, - Swapped => theirs_change_applied = true, - } - } - Swapped => { - editor.remove(toc(source_location))?; - editor.upsert(toc(add_location), their_mode.kind(), *their_id)?; - match side { - Original => theirs_change_applied = true, - Swapped => ours_change_applied = true, - } - } - }, - Some(ResolveWith::Ancestor) => {} - } - } - ( - Change::Rewrite { - source_location, - source_entry_mode, - source_id, - entry_mode: our_mode, - id: our_id, - location: our_location, - .. - }, - Change::Rewrite { - entry_mode: their_mode, - id: their_id, - location: their_location, - .. - }, - // NOTE: renames are only tracked among these kinds of types anyway, but we make sure. - ) if our_mode.is_blob_or_symlink() && their_mode.is_blob_or_symlink() => { - let (merged_blob_id, mut resolution) = if our_id == their_id { - (*our_id, None) - } else { - let (id, resolution) = perform_blob_merge( - labels, - objects, - blob_merge, - &mut diff_state.buf1, - &mut write_blob_to_odb, - (our_location, *our_id, *our_mode), - (their_location, *their_id, *their_mode), - (source_location, *source_id, *source_entry_mode), - (u8::from(our_location != their_location), outer_side), - &options, - )?; - (id, Some(resolution)) - }; - - let merged_mode = - merge_modes(*our_mode, *their_mode).expect("this case was assured earlier"); - - if matches!(tree_conflicts, None | Some(ResolveWith::Ours)) { - editor.remove(toc(source_location))?; - our_tree.remove_existing_change(source_location.as_bstr()); - their_tree.remove_existing_change(source_location.as_bstr()); - } - - let their_location = - possibly_rewritten_location(our_tree, their_location.as_bstr(), our_changes) - .map_or(Cow::Borrowed(their_location.as_bstr()), Cow::Owned); - let our_location = - possibly_rewritten_location(their_tree, our_location.as_bstr(), their_changes) - .map_or(Cow::Borrowed(our_location.as_bstr()), Cow::Owned); - let (our_addition, their_addition) = if our_location == their_location { - ( - None, - Some(Change::Addition { - location: our_location.into_owned(), - relation: None, - entry_mode: merged_mode, - id: merged_blob_id, - }), - ) - } else { - if should_fail_on_conflict(Conflict::without_resolution( - ResolutionFailure::OursRenamedTheirsRenamedDifferently { - merged_blob: resolution.take().map(|resolution| ContentMerge { - resolution, - merged_blob_id, - }), - }, - (ours, theirs, Original, outer_side), - [ - index_entry_at_path( - source_entry_mode, - source_id, - ConflictIndexEntryPathHint::Source, - ), - index_entry_at_path( - our_mode, - &merged_blob_id, - ConflictIndexEntryPathHint::Current, - ), - index_entry_at_path( - their_mode, - &merged_blob_id, - ConflictIndexEntryPathHint::RenamedOrTheirs, - ), - ], - )) { - break 'outer; - } - match tree_conflicts { - None => { - let our_addition = Change::Addition { - location: our_location.into_owned(), - relation: None, - entry_mode: merged_mode, - id: merged_blob_id, - }; - let their_addition = Change::Addition { - location: their_location.into_owned(), - relation: None, - entry_mode: merged_mode, - id: merged_blob_id, - }; - (Some(our_addition), Some(their_addition)) - } - Some(ResolveWith::Ancestor) => (None, None), - Some(ResolveWith::Ours) => { - let our_addition = Change::Addition { - location: match outer_side { - Original => our_location, - Swapped => their_location, - } - .into_owned(), - relation: None, - entry_mode: merged_mode, - id: merged_blob_id, - }; - (Some(our_addition), None) - } - } - }; - - if let Some(resolution) = resolution { - if should_fail_on_conflict(Conflict::with_resolution( - Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { - merged_blob: ContentMerge { - resolution, - merged_blob_id, - }, - }, - (ours, theirs, Original, outer_side), - [ - index_entry_at_path( - source_entry_mode, - source_id, - ConflictIndexEntryPathHint::Source, - ), - index_entry_at_path( - our_mode, - &merged_blob_id, - ConflictIndexEntryPathHint::Current, - ), - index_entry_at_path( - their_mode, - &merged_blob_id, - ConflictIndexEntryPathHint::RenamedOrTheirs, - ), - ], - )) { - break 'outer; - } - } - if let Some(addition) = our_addition { - push_deferred((addition, Some(theirs_idx)), our_changes); - } - if let Some(addition) = their_addition { - push_deferred((addition, Some(ours_idx)), their_changes); - } - } - ( - Change::Deletion { .. }, - Change::Rewrite { - source_location, - entry_mode: rewritten_mode, - id: rewritten_id, - location, - .. - }, - ) - | ( - Change::Rewrite { - source_location, - entry_mode: rewritten_mode, - id: rewritten_id, - location, - .. - }, - Change::Deletion { .. }, - ) if !rewritten_mode.is_commit() => { - let side = if matches!(ours, Change::Deletion { .. }) { - Original - } else { - Swapped - }; - - match tree_conflicts { - None | Some(ResolveWith::Ours) => { - editor.remove(toc(source_location))?; - pick_our_tree(side, our_tree, their_tree) - .remove_existing_change(source_location.as_bstr()); - match side { - Original => ours_change_applied = true, - Swapped => theirs_change_applied = true, - } - } - Some(ResolveWith::Ancestor) => {} - } - - let their_rewritten_location = possibly_rewritten_location( - pick_our_tree(side, our_tree, their_tree), - location.as_ref(), - pick_our_changes(side, our_changes, their_changes), - ) - .unwrap_or_else(|| location.to_owned()); - let our_addition = Change::Addition { - location: their_rewritten_location, - relation: None, - entry_mode: *rewritten_mode, - id: *rewritten_id, - }; - - if should_fail_on_conflict(Conflict::without_resolution( - ResolutionFailure::OursDeletedTheirsRenamed, - (ours, theirs, side, outer_side), - [ - None, - None, - index_entry_at_path( - rewritten_mode, - rewritten_id, - ConflictIndexEntryPathHint::RenamedOrTheirs, - ), - ], - )) { - break 'outer; - } - - let ours_is_rewrite = side.is_swapped(); - if tree_conflicts.is_none() - || (matches!(tree_conflicts, Some(ResolveWith::Ours)) && ours_is_rewrite) - { - push_deferred( - (our_addition, None), - pick_our_changes_mut(side, their_changes, our_changes), - ); - } - } - ( - Change::Rewrite { - source_location, - source_entry_mode, - source_id, - entry_mode: our_mode, - id: our_id, - location, - .. - }, - Change::Addition { - id: their_id, - entry_mode: their_mode, - location: add_location, - .. - }, - ) - | ( - Change::Addition { - id: their_id, - entry_mode: their_mode, - location: add_location, - .. - }, - Change::Rewrite { - source_location, - source_entry_mode, - source_id, - entry_mode: our_mode, - id: our_id, - location, - .. - }, - ) if !involves_submodule(our_mode, their_mode) => { - let side = if matches!(ours, Change::Rewrite { .. }) { - Original - } else { - Swapped - }; - if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { - let (merged_blob_id, resolution) = if our_id == their_id { - (*our_id, None) - } else { - let (id, resolution) = perform_blob_merge( - labels, - objects, - blob_merge, - &mut diff_state.buf1, - &mut write_blob_to_odb, - (location, *our_id, *our_mode), - (location, *their_id, *their_mode), - (source_location, source_id.kind().null(), *source_entry_mode), - (0, outer_side), - &options, - )?; - (id, Some(resolution)) - }; - - editor.remove(toc(source_location))?; - pick_our_tree(side, our_tree, their_tree).remove_change(source_location.as_bstr()); - - if let Some(resolution) = resolution { - if should_fail_on_conflict(Conflict::with_resolution( - Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { - merged_blob: ContentMerge { - resolution, - merged_blob_id, - }, - }, - (ours, theirs, Original, outer_side), - [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], - )) { - break 'outer; - } - } - - // Because this constellation can only be found by the lookup tree, there is - // no need to put it as addition, we know it's not going to intersect on the other side. - editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; - } else { - // We always remove the source from the tree - it might be re-added later. - let ours_is_rename = - tree_conflicts == Some(ResolveWith::Ours) && side == outer_side; - let remove_rename_source = - tree_conflicts.is_none() || ours_is_rename || add_location != source_location; - if remove_rename_source { - editor.remove(toc(source_location))?; - pick_our_tree(side, our_tree, their_tree) - .remove_change(source_location.as_bstr()); - } - - let ( - logical_side, - label_of_side_to_be_moved, - (our_mode, our_id, our_path_hint), - (their_mode, their_id, their_path_hint), - ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) { - ( - Original, - labels.other.unwrap_or_default(), - (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), - (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), - ) - } else { - ( - Swapped, - labels.current.unwrap_or_default(), - (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), - (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), - ) - }; - let tree_with_rename = pick_our_tree(logical_side, their_tree, our_tree); - let renamed_location = unique_path_in_tree( - location.as_bstr(), - &editor, - tree_with_rename, - label_of_side_to_be_moved, - )?; - - let upsert_rename_destination = tree_conflicts.is_none() || ours_is_rename; - if upsert_rename_destination { - editor.upsert(toc(location), our_mode.kind(), our_id)?; - tree_with_rename.remove_existing_change(location.as_bstr()); - } - - let conflict = Conflict::without_resolution( - ResolutionFailure::OursAddedTheirsAddedTypeMismatch { - their_unique_location: renamed_location.clone(), - }, - (ours, theirs, side, outer_side), - [ - None, - index_entry_at_path(&our_mode, &our_id, our_path_hint), - index_entry_at_path(&their_mode, &their_id, their_path_hint), - ], - ); - - if tree_conflicts.is_none() { - let new_change_with_rename = Change::Addition { - location: renamed_location, - entry_mode: their_mode, - id: their_id, - relation: None, - }; - push_deferred( - ( - new_change_with_rename, - Some(pick_idx(logical_side, theirs_idx, ours_idx)), - ), - pick_our_changes_mut(logical_side, their_changes, our_changes), - ); - } - - if should_fail_on_conflict(conflict) { - break 'outer; - } - } - } - _unknown => { - // Ancestor resolution may retain the entry represented by a written deletion, - // so an addition below it can legitimately match from a different path. - debug_assert!( - match_kind.is_none() - || (our_changes[ours_idx].was_processed_without_application() - && matches!(ours, Change::Deletion { .. })) - || (ours.location() == theirs.location() - || ours.source_location() == theirs.source_location()), - "BUG: right now it's not known to be possible to match changes from different paths: {match_kind:?} {candidate:?}" - ); - if let Some(ResolveWith::Ours) = tree_conflicts { - apply_our_resolution(ours, theirs, outer_side, &mut editor)?; - } - if should_fail_on_conflict(Conflict::unknown((ours, theirs, Original, outer_side))) { - break 'outer; - } - } - } - if theirs_change_applied { - their_changes[theirs_idx].mark_applied(); - } else { - their_changes[theirs_idx].mark_processed(); - } - if ours_change_applied { - our_changes[ours_idx].mark_applied(); - } else { - our_changes[ours_idx].mark_processed(); - } - } - } - } - segment_start = last_seen_len; - last_seen_len = their_changes.len(); - } - - ((our_changes, our_tree), (their_changes, their_tree)) = ((their_changes, their_tree), (our_changes, our_tree)); - (labels.current, labels.other) = (labels.other, labels.current); - outer_side = outer_side.swapped(); - } - - Ok(Outcome { - tree: editor, - conflicts, - failed_on_first_unresolved_conflict: failed_on_first_conflict, - }) -} - -fn apply_our_resolution( - local_ours: &Change, - local_theirs: &Change, - outer_side: ConflictMapping, - editor: &mut gix_object::tree::Editor<'_>, -) -> Result<(), Error> { - let ours = match outer_side { - Original => local_ours, - Swapped => local_theirs, - }; - Ok(apply_change(editor, ours, None)?) -} - -fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool { - a.is_commit() || b.is_commit() -} - -/// Allows equal modes or prefers executables bits in case of blobs -/// -/// Note that this is often not correct as the previous mode of each side should be taken into account so that: -/// -/// on | on = on -/// off | off = off -/// on | off || off | on = conflict -fn merge_modes(a: EntryMode, b: EntryMode) -> Option { - match (a.kind(), b.kind()) { - (_, _) if a == b => Some(a), - (EntryKind::BlobExecutable, EntryKind::BlobExecutable | EntryKind::Blob) - | (EntryKind::Blob, EntryKind::BlobExecutable) => Some(EntryKind::BlobExecutable.into()), - _ => None, - } -} - -/// Use this version if there is a single common `prev` value for both `a` and `b` to detect -/// if the mode was turned on or off. -fn merge_modes_prev(a: EntryMode, b: EntryMode, prev: EntryMode) -> Option { - match (a.kind(), b.kind()) { - (_, _) if a == b => Some(a), - (a @ EntryKind::BlobExecutable, b @ (EntryKind::BlobExecutable | EntryKind::Blob)) - | (a @ EntryKind::Blob, b @ EntryKind::BlobExecutable) => { - let prev = prev.kind(); - let changed = if a == prev { b } else { a }; - Some( - match (prev, changed) { - (EntryKind::Blob, EntryKind::BlobExecutable) => EntryKind::BlobExecutable, - (EntryKind::BlobExecutable, EntryKind::Blob) => EntryKind::Blob, - _ => unreachable!("upper match already assured we only deal with blobs"), - } - .into(), - ) - } - _ => None, - } -} - -fn push_deferred(change_and_idx: (Change, Option), changes: &mut ChangeList) { - push_deferred_with_rewrite(change_and_idx, None, changes); -} - -fn push_deferred_with_rewrite( - (change, ours_idx): (Change, Option), - new_location: Option<(BString, usize)>, - changes: &mut ChangeList, -) { - changes.push(TrackedChange::new(change, Some(ours_idx), new_location)); -} - -fn pick_our_tree<'a>(side: ConflictMapping, ours: &'a mut TreeNodes, theirs: &'a mut TreeNodes) -> &'a mut TreeNodes { - match side { - Original => ours, - Swapped => theirs, - } -} - -fn pick_our_changes<'a>( - side: ConflictMapping, - ours: &'a ChangeListRef, - theirs: &'a ChangeListRef, -) -> &'a ChangeListRef { - match side { - Original => ours, - Swapped => theirs, - } -} - -fn pick_idx(side: ConflictMapping, ours: usize, theirs: usize) -> usize { - match side { - Original => ours, - Swapped => theirs, - } -} - -fn pick_our_changes_mut<'a>( - side: ConflictMapping, - ours: &'a mut ChangeList, - theirs: &'a mut ChangeList, -) -> &'a mut ChangeList { - match side { - Original => ours, - Swapped => theirs, - } -} - -fn index_entry(mode: &gix_object::tree::EntryMode, id: &gix_hash::ObjectId) -> Option { - Some(ConflictIndexEntry { - mode: *mode, - id: *id, - path_hint: None, - }) -} - -fn index_entry_at_path( - mode: &gix_object::tree::EntryMode, - id: &gix_hash::ObjectId, - hint: ConflictIndexEntryPathHint, -) -> Option { - Some(ConflictIndexEntry { - mode: *mode, - id: *id, - path_hint: Some(hint), - }) -} diff --git a/gix-merge/src/tree/function/change.rs b/gix-merge/src/tree/function/change.rs new file mode 100644 index 00000000000..c193abf806d --- /dev/null +++ b/gix-merge/src/tree/function/change.rs @@ -0,0 +1,206 @@ +//! Change discovery and structural matching for tree merges. +//! +//! This module turns each base-to-side tree diff into a [`SideState`]: a flat +//! [`ChangeList`] containing scheduling state, paired with a [`TreeNodes`] path +//! index whose entries point back into that list. It also finds path and rename +//! interactions between sides and classifies the pairs consumed by the resolver. +//! +//! Semantic conflict resolution and edits to the result tree belong to the +//! sibling `resolve` module; this module only prepares, indexes, and matches work. + +use std::convert::Infallible; + +use bstr::{BString, ByteSlice}; +use gix_diff::{tree::recorder::Location, tree_with_rewrites::Change}; +use gix_object::FindExt; + +use crate::tree::{ + Error, + utils::{ChangeList, ChangeListRef, PossibleConflict, TreeNodes, track}, +}; + +pub(super) struct SideState { + changes: ChangeList, + tree: TreeNodes, +} + +/// Lifecycle +impl SideState { + fn from_changes(changes: ChangeList) -> Self { + let mut tree = TreeNodes::new(); + for (idx, change) in changes.iter().enumerate() { + tree.track_change(&change.inner, idx); + } + SideState { changes, tree } + } +} + +impl SideState { + /// Borrow the change schedule and its path index separately for resolution. + /// + /// The resolver swaps these pairs between "ours" and "theirs", appends deferred + /// changes to the list, and updates the corresponding index while processing them. + /// Every index stored in `TreeNodes` continues to refer into the returned `ChangeList`. + pub(super) fn parts_mut(&mut self) -> (&mut ChangeList, &mut TreeNodes) { + (&mut self.changes, &mut self.tree) + } + /// Return whether unrelated rewrite destinations can claim the same source identity. + /// + /// Rewrites derived from a parent directory rename have a relation and don't represent + /// ambiguous identity pairing themselves. + pub(super) fn has_ambiguous_rewrite_sources(&self) -> bool { + let mut sources = std::collections::HashSet::new(); + self.changes.iter().any(|change| match &change.inner { + Change::Rewrite { + source_id, + relation: None, + .. + } => !sources.insert(*source_id), + _ => false, + }) + } + + /// Compare both sides by their ordered `(source, destination)` paths. + /// + /// When unrelated rewrites share an object ID, whichever side is scheduled first can + /// otherwise decide how those identities are paired. The resolver uses this ordering + /// to choose the same first side after the caller reverses ours and theirs. Paths are + /// used instead of object IDs so the choice remains stable across hash kinds. + pub(super) fn cmp_for_scheduling(&self, other: &Self) -> std::cmp::Ordering { + self.changes + .iter() + .map(|change| (change.inner.source_location(), change.inner.location())) + .cmp( + other + .changes + .iter() + .map(|change| (change.inner.source_location(), change.inner.location())), + ) + } +} + +#[derive(Debug)] +pub(super) enum MatchKind { + /// A tree is supposed to be superseded by something else. + EraseTree, + /// A leaf node is superseded by a tree. + EraseLeaf, +} + +/// Collect one side's changes relative to the base and build their path index. +/// +/// `base_buf` contains `base_tree`, while `side_buf` is reused to load `side_tree`. +/// Equal tree IDs produce an empty state without diffing. Otherwise, the path-aware +/// diff, including the requested rewrite tracking, is normalized through [`track`] +/// into a [`ChangeList`] and indexed by [`TreeNodes`] in the returned [`SideState`]. +#[expect(clippy::too_many_arguments)] +pub(super) fn collect( + base_tree: &gix_hash::oid, + side_tree: &gix_hash::oid, + base_buf: &[u8], + side_buf: &mut Vec, + objects: &impl gix_object::FindObjectOrHeader, + diff_resource_cache: &mut gix_diff::blob::Platform, + diff_state: &mut gix_diff::tree::State, + rewrites: Option, +) -> Result { + let mut changes = Vec::new(); + if base_tree != side_tree { + let side_tree = objects.find_tree_iter(side_tree, side_buf)?; + gix_diff::tree_with_rewrites( + gix_object::TreeRefIter::from_bytes(base_buf, base_tree.kind()), + side_tree, + diff_resource_cache, + diff_state, + objects, + |change| -> Result<_, Infallible> { + track(change, &mut changes); + Ok(std::ops::ControlFlow::Continue(())) + }, + gix_diff::tree_with_rewrites::Options { + location: Some(Location::Path), + rewrites, + }, + )?; + } + Ok(SideState::from_changes(changes)) +} + +/// Find an eligible change on our indexed side that structurally interacts with `theirs`. +/// +/// `rewritten_location` belongs to a deferred change whose path passed through a directory +/// rewrite on our side. For example, if our side renamed `a` to `b`, their change at `a/x` +/// must be retried at `b/x`. The tuple contains that effective path and the index of the +/// directory rewrite which produced it, so matching starts where the change will actually +/// be applied. A rewrite is also checked at its destination, but only for a rewrite on our +/// side, to expose rewrite/rewrite interactions which are not visible at the source path. +/// +/// `needs_tree_insertion` identifies a clone appended for a later retry. The resolver inserts +/// such a deferred change into its own side's [`TreeNodes`] immediately before processing it, +/// keeping future work from seeing it prematurely. Its inner index, when present, identifies +/// the opposite-side change which caused the deferral; that candidate is ignored on retry so +/// the same pair cannot defer or resolve each other again. +/// +/// Identical changes need no conflict resolution. For a deletion, applying `theirs` removes +/// the same base entry our deletion would remove. An identical rewrite additionally creates +/// the same destination. Marking our deletion or rewrite applied records that one editor +/// update represents both sides and prevents a later scheduling pass from removing the shared +/// source after descendant entries have been added there. +/// +/// Applied deletions are otherwise ignored because their indexed path remains available for +/// structural lookup even though the entry itself has already been removed from the editor. +/// A deletion processed without application remains eligible: conflict resolution may have +/// retained the ancestor entry, so it can still block a tree or descendant at that path. +pub(super) fn matching( + theirs: &Change, + needs_tree_insertion: Option>, + rewritten_location: Option<&(BString, usize)>, + our_tree: &TreeNodes, + our_changes: &mut ChangeList, +) -> Option { + let candidate = our_tree + .check_conflict(rewritten_location.map_or_else(|| theirs.source_location(), |(location, _)| location.as_bstr())) + .or_else(|| match theirs { + Change::Rewrite { location, .. } => our_tree.check_conflict(location.as_bstr()).filter(|candidate| { + candidate + .change_idx() + .is_some_and(|idx| matches!(our_changes[idx].inner, Change::Rewrite { .. })) + }), + _ => None, + }); + + candidate.filter(|ours| { + ours.change_idx() + .zip(needs_tree_insertion.flatten()) + .is_none_or(|(ours_idx, ignore_idx)| ours_idx != ignore_idx) + && ours.change_idx().is_none_or(|ours_idx| { + let ours = &mut our_changes[ours_idx]; + if ours.inner == *theirs { + // Applying `theirs` also consumes an identical source removal, which must not + // run again after descendants have been added. + if matches!(theirs, Change::Deletion { .. } | Change::Rewrite { .. }) { + ours.mark_applied(); + } + false + } else { + !(ours.was_applied() && matches!(ours.inner, Change::Deletion { .. })) + } + }) + }) +} + +pub(super) fn pair(candidate: &PossibleConflict, our_changes: &ChangeListRef) -> (Option, Option) { + match *candidate { + PossibleConflict::TreeToNonTree { change_idx: Some(idx) } + if matches!( + our_changes[idx].inner, + Change::Deletion { .. } | Change::Addition { .. } | Change::Rewrite { .. } + ) => + { + (Some(idx), Some(MatchKind::EraseTree)) + } + PossibleConflict::NonTreeToTree { change_idx } => (change_idx, Some(MatchKind::EraseLeaf)), + PossibleConflict::Match { change_idx } => (Some(change_idx), None), + _ => (None, None), + } +} diff --git a/gix-merge/src/tree/function/mod.rs b/gix-merge/src/tree/function/mod.rs new file mode 100644 index 00000000000..2d77846e9fe --- /dev/null +++ b/gix-merge/src/tree/function/mod.rs @@ -0,0 +1,4 @@ +mod change; +mod resolve; + +pub use resolve::tree; diff --git a/gix-merge/src/tree/utils.rs b/gix-merge/src/tree/utils.rs index f706613a88c..9b1d6639846 100644 --- a/gix-merge/src/tree/utils.rs +++ b/gix-merge/src/tree/utils.rs @@ -58,18 +58,19 @@ pub fn rewrite_location_with_renamed_directory(their_location: &BStr, passed_cha } } -/// Produce a unique path within the directory that contains the file at `file_path` like `a/b`, using `editor` -/// and `tree` to assure unique names, to obtain the tree at `a/` and `side_name` to more clearly signal -/// where the file is coming from. +/// Produce a side-qualified path for `file_path` like `a/b`, using `editor` and `tree` to assure uniqueness. +/// +/// This normally keeps the file in its directory, as in `a/b~side`. If a non-tree component blocks that directory, +/// the blocker itself is qualified instead, as in `a~side/b`, because changing only the child name could never make +/// the path available. pub fn unique_path_in_tree( file_path: &BStr, editor: &tree::Editor<'_>, tree: &TreeNodes, side_name: &BStr, ) -> Result { - let mut buf = file_path.to_owned(); - buf.push(b'~'); - buf.extend( + let mut qualifier = BString::from("~"); + qualifier.extend( side_name .as_bytes() .iter() @@ -77,15 +78,35 @@ pub fn unique_path_in_tree( .map(|b| if b == b'/' { b'_' } else { b }), ); - // We could use a cursor here, but clashes are so unlikely that this wouldn't be meaningful for performance. - let base_len = buf.len(); - let mut suffix = 0; - while editor.get(to_components_bstring_ref(&buf)).is_some() || tree.check_conflict(buf.as_bstr()).is_some() { - buf.truncate(base_len); - buf.push_str(format!("_{suffix}")); - suffix += 1; + let mut component_end = file_path.len(); + loop { + let at_root = !file_path[..component_end].contains(&b'/'); + let mut suffix = None; + loop { + let mut buf = file_path[..component_end].to_owned(); + buf.extend_from_slice(&qualifier); + if let Some(suffix) = suffix { + buf.push_str(format!("_{suffix}")); + } + buf.extend_from_slice(&file_path[component_end..]); + + let conflict = tree.check_conflict(buf.as_bstr()); + if !at_root && matches!(conflict, Some(PossibleConflict::NonTreeToTree { .. })) { + break; + } + if editor.get(to_components_bstring_ref(&buf)).is_none() + && conflict.is_none_or(|conflict| matches!(conflict, PossibleConflict::PassedRewrittenDirectory { .. })) + { + return Ok(buf); + } + suffix = Some(suffix.map_or(0, |suffix| suffix + 1)); + } + + component_end = file_path[..component_end] + .iter() + .rposition(|byte| *byte == b'/') + .expect("a non-root component always has a preceding slash"); } - Ok(buf) } /// Perform a merge between two blobs and return the result of its object id. @@ -153,17 +174,20 @@ where buf } - if outer_side.is_swapped() { + let (current_location, other_location) = if outer_side.is_swapped() { (labels.current, labels.other) = (labels.other, labels.current); - } + (their_location, our_location) + } else { + (our_location, their_location) + }; let (ancestor, current, other); let labels = if our_location == their_location { labels } else { ancestor = labels.ancestor.map(|side| combined(side, previous_location)); - current = labels.current.map(|side| combined(side, our_location)); - other = labels.other.map(|side| combined(side, their_location)); + current = labels.current.map(|side| combined(side, current_location)); + other = labels.other.map(|side| combined(side, other_location)); crate::blob::builtin_driver::text::Labels { ancestor: ancestor.as_ref().map(|n| n.as_bstr()), current: current.as_ref().map(|n| n.as_bstr()), @@ -243,6 +267,15 @@ enum ChangeState { Applied, } +/// How handling a change affected the output editor. +#[derive(Debug, Clone, Copy)] +pub(super) enum ChangeDisposition { + /// The change was consumed without applying its effect. + Processed, + /// The change's effect is represented in the editor. + Applied, +} + impl TrackedChange { pub(super) fn new( inner: Change, @@ -283,6 +316,14 @@ impl TrackedChange { pub(super) fn mark_applied(&mut self) { self.state = ChangeState::Applied; } + + /// Record the final disposition chosen while resolving this change. + pub(super) fn mark(&mut self, disposition: ChangeDisposition) { + match disposition { + ChangeDisposition::Processed => self.mark_processed(), + ChangeDisposition::Applied => self.mark_applied(), + } + } } pub type ChangeList = Vec; @@ -507,10 +548,12 @@ impl TreeNodes { } } - /// Search the tree with `our` changes for `theirs` by [`source_location()`](Change::source_location())). - /// If there is an entry but both are the same, or if there is no entry, return `None`. + /// Search our indexed change paths for a structural overlap with `theirs_location`. + /// + /// Return the kind of exact-path or tree/non-tree overlap found, including passage through + /// a rewritten directory, or `None` if the path does not interact with our indexed changes. pub fn check_conflict(&self, theirs_location: &BStr) -> Option { - if self.0.len() == 1 { + if self.0[0].children.is_empty() { return None; } let components = to_components(theirs_location); @@ -524,7 +567,7 @@ impl TreeNodes { match cursor.children.get(component).copied() { // *their* change is outside *our* tree None => { - let res = if cursor.is_leaf_node() { + let res = if cursor.is_leaf_node() && !cursor.change_is_tree { Some(PossibleConflict::NonTreeToTree { change_idx: cursor.change_idx, }) @@ -573,6 +616,7 @@ impl TreeNodes { fn remove_change_inner(&mut self, location: &BStr, must_exist: bool) { let mut components = to_components(location).peekable(); let mut cursor_idx = 0; + let mut ancestry = Vec::new(); while let Some(component) = components.next() { match self.0[cursor_idx].children.get(component).copied() { None => { @@ -581,11 +625,9 @@ impl TreeNodes { return; } Some(existing_idx) => { + ancestry.push((cursor_idx, component.to_owned(), existing_idx)); let is_last = components.peek().is_none(); if is_last { - if self.0[existing_idx].is_leaf_node() { - self.0[cursor_idx].children.remove(component); - } let node = &mut self.0[existing_idx]; debug_assert!(!must_exist || node.change_idx.is_some(), "no change at '{location}'"); node.change_idx = None; @@ -596,10 +638,21 @@ impl TreeNodes { } } } + + while let Some((parent_idx, component, child_idx)) = ancestry.pop() { + let child = &self.0[child_idx]; + if child.change_idx.is_some() || !child.children.is_empty() { + break; + } + self.0[parent_idx].children.remove(component.as_bstr()); + } } - /// Insert `new_change` which affects this tree into it and put it into `storage` to obtain the index. - /// Panic if that change already exists as it must be made so that it definitely doesn't overlap with this tree. + /// Insert the current location of a newly deferred change into this tree. + /// + /// A rewrite may arrive here after directory-rename handling deferred it to a relocated + /// destination. Its source is already represented by the original change tree; only the + /// rescheduled destination must become visible now. pub fn insert(&mut self, new_change: &Change, new_change_idx: usize) { let mut next_index = self.0.len(); let mut cursor = &mut self.0[0]; @@ -617,10 +670,6 @@ impl TreeNodes { } } - debug_assert!( - !matches!(new_change, Change::Rewrite { .. }), - "BUG: we thought we wouldn't do that current.location is related?" - ); cursor.change_idx = Some(new_change_idx); cursor.change_is_tree = new_change.entry_mode().is_tree(); cursor.location = ChangeLocation::CurrentLocation; @@ -713,4 +762,110 @@ mod tree_nodes_tests { "a missing `a` prefix must stop removal before an unrelated root-level `b`" ); } + + #[test] + fn removing_a_change_prunes_empty_parent_nodes() { + let mut tree = TreeNodes::new(); + tree.track_change( + &Change::Addition { + location: "e/e".into(), + relation: None, + entry_mode: EntryKind::Blob.into(), + id: gix_hash::Kind::Sha1.null(), + }, + 0, + ); + + tree.remove_existing_change("e/e".into()); + assert!( + tree.check_conflict("e".into()).is_none(), + "an empty former parent isn't a leaf change or a path conflict" + ); + } + + #[test] + fn passing_a_rewritten_directory_does_not_occupy_every_path_below_it() { + let mut tree = TreeNodes::new(); + tree.track_change( + &Change::Rewrite { + source_location: "old".into(), + source_entry_mode: EntryKind::Tree.into(), + source_relation: None, + source_id: gix_hash::Kind::Sha1.null(), + diff: None, + entry_mode: EntryKind::Tree.into(), + id: gix_hash::Kind::Sha1.null(), + location: "new".into(), + relation: None, + copy: false, + }, + 0, + ); + tree.track_change( + &Change::Modification { + location: "old/existing".into(), + previous_entry_mode: EntryKind::Blob.into(), + previous_id: gix_hash::Kind::Sha1.null(), + entry_mode: EntryKind::Blob.into(), + id: gix_hash::Kind::Sha1.null(), + }, + 1, + ); + + assert!( + matches!( + tree.check_conflict("old/file~side".into()), + Some(PossibleConflict::PassedRewrittenDirectory { change_idx: 0 }) + ), + "the path still has to follow the directory rename" + ); + } + + #[test] + fn a_tracked_tree_without_tracked_children_does_not_occupy_paths_below_it() { + let mut tree = TreeNodes::new(); + tree.track_change( + &Change::Addition { + location: "dir".into(), + relation: None, + entry_mode: EntryKind::Tree.into(), + id: gix_hash::Kind::Sha1.null(), + }, + 0, + ); + + assert!( + !matches!( + tree.check_conflict("dir/file~side".into()), + Some(PossibleConflict::NonTreeToTree { .. }) + ), + "a tracked tree permits children even if no child change is currently tracked" + ); + } + + #[test] + fn unique_path_qualifies_a_non_tree_parent_instead_of_looping_over_child_names() -> Result<(), Error> { + let mut tree = TreeNodes::new(); + tree.track_change( + &Change::Addition { + location: "dir".into(), + relation: None, + entry_mode: EntryKind::Blob.into(), + id: gix_hash::Kind::Sha1.null(), + }, + 0, + ); + let editor = tree::Editor::new( + gix_object::Tree::default(), + &gix_object::find::Never, + gix_hash::Kind::Sha1, + ); + + assert_eq!( + unique_path_in_tree("dir/file".into(), &editor, &tree, "OURS".into())?, + "dir~OURS/file", + "the blocking path component itself must be moved aside" + ); + Ok(()) + } } diff --git a/gix-merge/tests/fixtures/generated-archives/tree-baseline.tar b/gix-merge/tests/fixtures/generated-archives/tree-baseline.tar index 82723b40c470c2c37c6c42462d5be4385ce1f4d8..ccbad3fd040dd9d6bbc5a36e8d5be5116ce461be 100644 GIT binary patch delta 177543 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

GTT+XXyd;=gxuKj?sk)K5`EMB(9*XPfS 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

    q{YKx|Ex0{?Pp~Y# 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 delta 2507 zcmcIm`%_a_9?y+|5S}4il59xeCf@R>1(J|J1&San5?&gRQfSxGvV&1rw_8D0oGDs% ziLLVL>OBN0;G@w#)E2WGY?ZRrRTtaQwN+bX1LdI>p}MvO71`Ny?!E2!5A=u6d}i`F z=ktA>@BL1;o*ou%y?dYN5i*|1CT145YOCm^J z@)-4C!o?&|+{?)W3l9h}usw=2K!;FV9fV?xJpFcd7m9(HNZ?|0u`$9`FA2Ec=T>rj zSi$QBIEQf6%UB;Jp6-2~<1O#=`Yv!lE;g{{T}|=q<*vdFyjX}YTgb^Ru8)*k+f2Ii z5qlc%H{RQKXw@qKBG zT5^q7_R8!OpfyFLgEE)oOD5Yt8c;)20?q;0zVg%C+cW(U0$_Wlv}P}|9+i|Qy@4y2 zygVPwS$C&!onbqNbaEu_*oc+D0s5 z=EXap+DleRc9#!dqO)EKxYTB^(}h+XJ0q#{!K33xF#a+W!dr$Agg_z$Bgi4;t(f^J zq+)zH!qSN!by&Oe<}S{)#Uc=Fn*n#_Vjajnp;$~lDBqdjsxx<-Jd`)NaO7tFzQNcD zY+p|>zgKF3;z&R`*Vw^igWWNpKu{w5208{*)%a0IB)X6ooH}McbrGO z{PEy30huLV%-28A)QPY7} zDgXJ2ig>3ZEq|E2!9Y4N}a!g16)$A5FE#kZSIfWi=h=SCRb_TDG5v@8a z3AwP$xpb}r^r*IyymP*I+lGfeWgDL#`m{lxw(sDT{ub==JA#?kDuH@+corxd_VWTZ zHqkMy-7%(vjHGk&te^G;zF$eb!OC&k3!WaM zlZiG;jlOajAP*(20{?qJc-n2oaD)hiNQ5Y8Gd|h=FQz|Q3*@tb{3x|X7-7eO=#7E6 zeAQI*5H24xSN@(`fbNT~Bo}`l{%p(bf>TG8Jxy;Mk=#C!XR&bQH}*$2_Qy2#$2$A< zCzw`~0xD8Xv)FFfICA@+$PV!-8)w&A4yp=FXb~5feDR4m@2I=@M4V)npVwA2QUxZ1 z9Q_z8se6dUSV{g4{yMmgRw($U!1NUP_o2&YpL|_W(!C<-^vv#L=h^N*#Q^jmH&G56 zyJocsYF3-a#Y|Vc44{nieiy{^Z#o-#%QkzvzDM9((IIxT-lOKUEH*r#%wdC*Ze7a3Ne2w6jYlmTgcFc%{SYw{(J9I)j9LkY-g_k91p>+RERy~&1@L*X3o9Ms4XK2aYG*XwJ~#T>5_4)%33(= zRtcOD`gEhNhS`2`3{I=@GoV$Kn2RRaGZ!HbYCQADit7h3+ndlp#Lc6GiLp#ZA$3nxBtx{E~q2D8~5 fk9OQrb9Bvybs%smq7b6L2;D^EM3Hmi*_8hS*c|Mo diff --git a/gix-merge/tests/fixtures/generated-archives/tree-baseline_sha256.tar b/gix-merge/tests/fixtures/generated-archives/tree-baseline_sha256.tar index d7174d431be69d5fa0256986d821b0e83dd41d71..b8f93bad606e28e5e762c6dd7a415101cb31767b 100644 GIT binary patch delta 217507 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 Q75s`ck;|+;tT{= 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

    *=NRhiFq)exePCqkH)VYfc<(-UJwq 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⪻0jV2G=y46 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 diff --git a/gix-merge/tests/fixtures/tree-baseline.sh b/gix-merge/tests/fixtures/tree-baseline.sh index 0fa7def5471..4f415d171a6 100755 --- a/gix-merge/tests/fixtures/tree-baseline.sh +++ b/gix-merge/tests/fixtures/tree-baseline.sh @@ -184,6 +184,29 @@ git init deleted-file-added-dir git add to-be-deleted/a && git commit -m "replace file with directory" ) +git init deleted-file-added-gitlink-directory +(cd deleted-file-added-gitlink-directory + write_lines original >a + git add a + git commit -m "file base" + base=$(git rev-parse HEAD) + + git branch A + git branch B + + # Both sides delete `a`; B additionally replaces it with a directory containing + # a gitlink. The shared deletion and descendant addition are compatible even + # though the gitlink takes a different structural merge path than a blob. + git checkout A + git rm a + git commit -m "delete a" + + git checkout B + git rm a + git update-index --add --cacheinfo 160000,$base,a/a + git commit -m "replace a with a gitlink directory" +) + git init tree-to-non-tree (cd tree-to-non-tree mkdir -p a/sub @@ -518,6 +541,59 @@ git init renames-to-same-destination git commit -m "rename two to target" ) +git init identical-renames-to-same-destination +(cd identical-renames-to-same-destination + write_lines same >one + cp one two + git add . + git commit -m "two identical files" + + git branch A + git branch B + + # Both sides rename a different source to `target`, but the entries have the + # same mode and object ID. There is nothing to content-merge and the two + # operations collapse cleanly to the shared destination in either direction. + git checkout A + git mv one target + git commit -m "rename one to target" + + git checkout B + git mv two target + git commit -m "rename two to target" +) + +git init identical-renames-to-same-destination-with-mode-change +(cd identical-renames-to-same-destination-with-mode-change + write_lines same >one + cp one two + git add . + git commit -m "two identical files" + + git branch A + git branch B + + # The destinations have the same blob ID but differ in executable mode. + # Content merging must see those original modes even though the final mode + # has already been selected. + git checkout A + git mv one target + chmod +x target + # For this to work on windows, we need explicit executable bit handling. + git update-index --chmod=+x target + git commit -m "rename one to executable target" + + git checkout B + git mv two target + git commit -m "rename two to target" + + # gix treats the identical content as clean and carries the executable mode + # selected from A to the shared destination in both merge directions. + git checkout -b expected A + git rm two + git commit -m "expected gix merge" +) + git init deleted-file-added-dir-with-rename (cd deleted-file-added-dir-with-rename # Regression for a deletion that is processed but not applied: @@ -1265,6 +1341,32 @@ EOF make_conflict_index submodule-both-modify-A-B-reversed ) +git init gitlink-replaced-by-files +(cd gitlink-replaced-by-files + git commit --allow-empty -m "seed commit" + seed=$(git rev-parse HEAD) + git update-index --add --cacheinfo 160000,$seed,item + git commit -m "gitlink base" + + git branch A + git branch B + + # Both sides replace the gitlink with regular files. Their contents must be + # merged as an add/add pair with an empty blob ancestor; the commit named by + # the base entry is not a valid blob-merge resource. + git checkout A + git rm item + write_lines changed-by-A >item + git add item + git commit -m "replace gitlink with A's file" + + git checkout B + git rm item + write_lines changed-by-B >item + git add item + git commit -m "replace gitlink with B's file" +) + git init both-modify-union-attr (cd both-modify-union-attr mkdir a && write_lines original 1 2 3 4 5 >a/x.f @@ -1431,6 +1533,484 @@ git init symlink-addition git commit -m "new link to point to 'b'" ) +git init added-file-vs-added-directory +(cd added-file-vs-added-directory + git commit --allow-empty -m "empty base" + + git branch A + git branch B + + # A adds a file at `e`, while B adds a file below the directory `e`. Resolving + # this tree/non-tree pair removes B's `e/e` path-tree leaf; its now-empty `e` + # parent must not remain visible as a change during the inverse scheduling pass. + git checkout A + write_lines file >e + git add e + git commit -m "add file e" + + git checkout B + mkdir e + write_lines nested >e/e + git add e/e + git commit -m "add directory e" +) + +git init added-symlink-blocks-gitlink-directory +(cd added-symlink-blocks-gitlink-directory + git commit --allow-empty -m "empty base" + base="$(git rev-parse HEAD)" + + git branch A + git branch B + + # A adds a non-tree at `d`, while B adds a directory at the same path. The nested + # gitlink is deliberate: tree/non-tree handling must not accidentally route this + # structural conflict through the blob or submodule merge cases. + git checkout A + ln -s target d + git add d + git commit -m "add symlink d" + + git checkout B + git update-index --index-info <a/b + git add a/b + git commit -m "replace the symlink directory" + + git checkout B + git mv a/a/a moved-link + git update-index --force-remove h + mkdir -p h/b + git mv moved-link h/a + write_lines sibling >h/b/a + git add . + git commit -m "replace the gitlink with a renamed symlink and sibling" + + # gix follows the detected `a` -> `h` directory rename and relocates A's + # added `a/b` to `h/b~A`. Git leaves the addition at `a/b`; this fixture + # records the existing semantic difference while guarding the path cleanup. + git checkout -b expected B + git update-index --add --cacheinfo "100644,$(git rev-parse A:a/b),h/b~A" + git commit -m "expected gix merge" + + # FIXME: merge symmetry: only when B is ours, gix also preserves A's blocking + # gitlink at its unique path. + git checkout -b expected-reversed + git update-index --add --cacheinfo "160000,$root,h~B" + git commit -m "expected reversed gix merge" +) + +git init same-source-rewrites-after-consumed-path +(cd same-source-rewrites-after-consumed-path + mkdir -p a/a + printf payload >a/a/a + printf payload >b + git add . + git commit -m "identical files at nested and root paths" + + git branch A + git branch B + + # Every non-directory entry deliberately has the same object ID. This gives + # rewrite detection several equally valid source/destination pairings. A + # removes `a/a/a` and reuses its payload at unrelated paths. + git checkout A + git rm a/a/a + mkdir -p e/a g/e + ln -s payload e/a/g + printf payload >g/e/a + git add . + git commit -m "remove the nested source and add identical entries" + + # B retains `a/a/a`, adds an executable sibling, replaces `b` with a nested + # copy, and adds another copy. Resolving one ambiguous rewrite can consume + # the shared source before a later same-source rewrite cleans it up. + git checkout B + mkdir -p a/e h/e + printf payload >a/e/a + chmod +x a/e/a + git rm b + mkdir -p b/b + printf payload >b/b/f + printf payload >h/e/a + git add . + git update-index --chmod=+x a/e/a + git commit -m "retain and multiply the identical payload" + + # gix pairs both sides with the same ambiguous base source and carries B's + # executable mode to A's destination. Git leaves both additions in place. + git checkout -b expected B + git update-index --force-remove a/a/a + git update-index --force-remove a/e/a + git update-index --add --cacheinfo "120000,$(git rev-parse A:e/a/g),e/a/g" + git update-index --add --cacheinfo "100755,$(git rev-parse B:a/e/a),g/e/a" + git commit -m "expected gix merge" +) + +git init rename-delete-after-consumed-path +(cd rename-delete-after-consumed-path + mkdir -p a h/d f e/f + write_lines shared >a/a + chmod +x a/a + write_lines four >b + write_lines seven >e/f/a + chmod +x e/f/a + write_lines shared >f/a + write_lines shared >h/d/a + git add . + git update-index --chmod=+x a/a e/f/a + git commit -m "base with repeated rename candidates" + + git branch A + git branch B + + # A moves `h/d/a` below `a`, turns `f/a` into `f`, and replaces `e/`. + # The repeated `shared` payload deliberately gives rename detection several + # possible sources, matching the scheduling ambiguity found by the fuzzer. + git checkout A + git rm -r a e + mkdir -p a/a a/d + write_lines four >a/a/a + git mv h/d/a a/d/a + mv f/a moved + rmdir f + mv moved f + write_lines five >d + write_lines shared >e + git add -A + git commit -m "rename repeated payloads and replace directories" + + # B deletes A's rename source and replaces `b`, `e`, and `f` with opposite + # file/directory shapes. Resolving another rename/delete pair can consume the + # path node for `h/d/a` before that pending deletion follows a directory rename. + git checkout B + git rm -r a b e f h + mkdir -p b/a + write_lines shared >b/a/a + write_lines four >e + write_lines four >f + git add . + git commit -m "delete rename sources and replace directories" + + # Ambiguous identity-only rename pairing makes gix keep a smaller tree than + # Git and merge the repeated payloads at the surviving paths. + shared_four=$( + printf '%s\n' \ + '<<<<<<< A' \ + shared \ + ======= \ + four \ + '>>>>>>> B' | + git hash-object -w --stdin + ) + four_shared=$( + printf '%s\n' \ + '<<<<<<< A' \ + four \ + ======= \ + shared \ + '>>>>>>> B' | + git hash-object -w --stdin + ) + git checkout -b expected B + git read-tree --empty + git update-index --add --cacheinfo "100644,$four_shared,b/a/a" + git update-index --add --cacheinfo "100644,$(git rev-parse A:a/d/a),b/d/a" + git update-index --add --cacheinfo "100644,$(git rev-parse A:d),d" + git update-index --add --cacheinfo "100644,$shared_four,e" + git update-index --add --cacheinfo "100644,$shared_four,f" + git commit -m "expected gix merge" + + reversed_four_shared=$( + printf '%s\n' \ + '<<<<<<< B' \ + four \ + ======= \ + shared \ + '>>>>>>> A' | + git hash-object -w --stdin + ) + reversed_shared_four=$( + printf '%s\n' \ + '<<<<<<< B' \ + shared \ + ======= \ + four \ + '>>>>>>> A' | + git hash-object -w --stdin + ) + # Reversing the merge keeps the same paths and pairings, with directional + # conflict-marker labels. + git checkout -f -b expected-reversed B + git read-tree --empty + git update-index --add --cacheinfo "100644,$reversed_shared_four,b/a/a" + git update-index --add --cacheinfo "100644,$(git rev-parse A:a/d/a),b/d/a" + git update-index --add --cacheinfo "100644,$(git rev-parse A:d),d" + git update-index --add --cacheinfo "100644,$reversed_four_shared,e" + git update-index --add --cacheinfo "100644,$reversed_four_shared,f" + git commit -m "expected reversed gix merge" +) + +git init modified-file-vs-gitlink-directory +(cd modified-file-vs-gitlink-directory + ln -s target a + git add a + git commit -m "symlink base" + base="$(git rev-parse HEAD)" + + git branch A + git branch B + + # A replaces the symlink with an executable file while B replaces it with a + # directory. The nested gitlink makes its addition sort before the base-file + # deletion, exercising merge scheduling independently of diff order. + git checkout A + rm a + write_lines modified >a + chmod +x a + git add --chmod=+x a + git commit -m "replace a with an executable" + + git checkout B + git rm a + git update-index --index-info <c/c + git add . + git commit -m "file in c" + + git branch A + git branch B + + # A's exact file rename also implies the directory rename `c` -> `a`. B adds + # `c/a/c`, so directory-rename handling relocates it to `a/a/c`, where A's + # renamed file at `a/a` blocks the required directory. + git checkout A + mkdir -p a + git mv c/c a/a + rmdir c + git commit -m "rename c/c to a/a" + + git checkout B + mkdir -p c/a + write_lines added >c/a/c + git add . + git commit -m "add below renamed directory" +) + +git init nested-rename-blocks-relocated-addition +(cd nested-rename-blocks-relocated-addition + mkdir a + write_lines base >a/a + git add . + git commit -m "file in a" + + git branch A + git branch B + + # Both sides rename the same file. A puts it where its containing directory + # used to be, while B nests it another level below that directory. + git checkout A + git mv a/a moved + rmdir a + git mv moved a + git commit -m "move a/a to a" + + git checkout B + git mv a/a moved + mkdir -p a/a + git mv moved a/a/a + git commit -m "move a/a to a/a/a" +) + +git init directory-rename-vs-directory-to-file +(cd directory-rename-vs-directory-to-file + mkdir a + write_lines same >a/a + git add . + git commit -m "file in a" + + git branch A + git branch B + + # A renames the containing directory. B moves its only file to the directory's + # former path, replacing the directory with that file. This is a different-renames + # conflict whose tree/non-tree handling must update the rename side's path tree. + git checkout A + git mv a e + git commit -m "rename a to e" + + git checkout B + git mv a/a moved + rmdir a + git mv moved a + git commit -m "replace a directory with its file" +) + +git init directory-rename-vs-renamed-file-replacement +(cd directory-rename-vs-renamed-file-replacement + mkdir -p h/h + write_lines nested >h/h/a + write_lines outside >a + git add . + git commit -m "directory and outside file" + + git branch A + git branch B + + # A moves the directory away. B deletes its contents and moves an unrelated + # file onto the vacated directory path. Unlike the contained-file variant + # above, the replacement rename has a distinct source, so it can meet the + # structural directory rewrite before the nested rename/delete pair does. + git checkout A + git mv h/h f + rmdir h + git commit -m "rename the directory" + + git checkout B + git rm h/h/a + mkdir -p h + git mv a h/h + git commit -m "replace the directory with a renamed file" +) + +git init unrelated-renames-overlapping-destinations +(cd unrelated-renames-overlapping-destinations + mkdir -p a/a h/b + write_lines first >a/a/a + write_lines second >h/b/a + git add . + git commit -m "two files in separate directories" + + git branch A + git branch B + + # A's directory rename places `h/b/a` below `c`. B independently renames + # `a/a/a` to the non-tree `c` and renames `h/b/a` elsewhere. The two rename + # destinations therefore overlap even though their source files are unrelated. + git checkout A + git mv h c + git commit -m "rename h to c" + + git checkout B + git mv h/b/a moved-h + git mv a/a/a moved-a + rmdir a/a + mkdir -p a + git mv moved-h a/a + git mv moved-a c + git commit -m "rename both files to crossing destinations" +) + +git init renamed-file-inside-renamed-directory +(cd renamed-file-inside-renamed-directory + mkdir -p a/a h/b + write_lines first >a/a/a + write_lines second >h/b/a + git add . + git commit -m "two files in separate directories" + + git branch A + git branch B + + # A renames `h` to `c`. B replaces the contents of `h` with a file renamed + # from elsewhere while moving the original `h/b/a` to `a/a`. Directory-rename + # handling must therefore defer and relocate a rewrite, not just an addition. + git checkout A + git mv h c + git commit -m "rename h to c" + + git checkout B + git mv h/b/a moved-h + git mv a/a/a moved-a + rmdir a/a h/b h + mkdir -p a h + git mv moved-h a/a + git mv moved-a h/h + git commit -m "replace a renamed directory with an outside file" +) + +git init unrelated-renames-to-same-path-with-type-mismatch +(cd unrelated-renames-to-same-path-with-type-mismatch + write_lines payload >file-source + ln -s payload link-source + git add . + git commit -m "regular file and symlink" + + git branch A + git branch B + + # Both sides rename a different base entry to `target`. A's entry is a + # symlink while B's is a regular file, so their destination cannot be + # content-merged. Git keeps the symlink at `target` and relocates the regular + # file to the side-qualified `target~B`, independently of merge direction. + git checkout A + git mv link-source target + git commit -m "rename the symlink to target" + + git checkout B + git mv file-source target + git commit -m "rename the regular file to target" +) + +git init renamed-file-vs-file-to-directory-with-siblings +(cd renamed-file-vs-file-to-directory-with-siblings + write_lines base >a + git add a + git commit -m "base file" + + git branch A + git branch B + + # A renames the base file away while B replaces its old path with a directory + # containing two children. Both children encounter the same rename/delete + # conflict; handling the first must not make the second try to remove an + # already-pruned rename-destination node. + git checkout A + git mv a h + git commit -m "rename the file" + + git checkout B + git rm a + mkdir a + write_lines first >a/a + write_lines second >a/c + git add . + git commit -m "replace the file with two children" +) + git init type-change-to-symlink (cd type-change-to-symlink touch a b link @@ -1454,6 +2034,7 @@ git init type-change-to-symlink baseline non-tree-to-tree A-B A B baseline deleted-file-added-dir A-B A B +baseline deleted-file-added-gitlink-directory A-B A B baseline tree-to-non-tree A-B A B baseline tree-to-non-tree-with-rename A-B A B baseline non-tree-to-tree-with-rename A-B A B @@ -1479,6 +2060,8 @@ baseline rename-change-matrix A-B A B baseline same-rename-with-content A-B A B baseline same-rename-and-file-to-directory A-B A B baseline renames-to-same-destination A-B A B +baseline identical-renames-to-same-destination A-B A B +baseline identical-renames-to-same-destination-with-mode-change A-B A B "gix resolves the identical content and mode change cleanly, while Git leaves an add/add mode conflict" baseline deleted-file-added-dir-with-rename A-B A B baseline rename-add A-B A B baseline rename-add A-B-diff3 A B @@ -1506,6 +2089,7 @@ baseline added-file-changed-content-and-mode A-B A B "We improve on executable b baseline type-change-and-renamed A-B A B baseline change-and-delete A-B A B 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." +baseline gitlink-replaced-by-files A-B A B baseline both-modify-union-attr A-B A B baseline both-modify-union-attr A-B-diff3 A B baseline both-modify-binary A-B A B @@ -1522,6 +2106,20 @@ baseline multiple-merge-bases A-B-diff3 A B baseline rename-and-modification A-B A B baseline symlink-modification A-B A B baseline symlink-addition A-B A B +baseline added-file-vs-added-directory A-B A B +baseline added-symlink-blocks-gitlink-directory A-B A B +baseline gitlink-vs-renamed-symlink-directory-with-siblings A-B A B "gix relocates A's addition through the detected directory rename, while Git keeps it at its original path; FIXME: merge symmetry: reversing gix also preserves A's blocking gitlink" +baseline same-source-rewrites-after-consumed-path A-B A B "ambiguous identical blobs make gix pair both sides with one base source, while Git retains both additions" +baseline rename-delete-after-consumed-path A-B A B "ambiguous identical blobs make gix pair rename sources differently than Git" +baseline modified-file-vs-gitlink-directory A-B A B +baseline relocated-addition-blocked-by-rename A-B A B +baseline nested-rename-blocks-relocated-addition A-B A B +baseline directory-rename-vs-directory-to-file A-B A B +baseline directory-rename-vs-renamed-file-replacement A-B A B +baseline unrelated-renames-overlapping-destinations A-B A B +baseline renamed-file-inside-renamed-directory A-B A B +baseline unrelated-renames-to-same-path-with-type-mismatch A-B A B +baseline renamed-file-vs-file-to-directory-with-siblings A-B A B baseline type-change-to-symlink A-B A B ## @@ -1529,6 +2127,327 @@ baseline type-change-to-symlink A-B A B ## when making tree-conflict resolution expectations. It's important ## to get these right. ## +(cd added-file-vs-added-directory + # The ancestor is empty, so choosing it keeps neither addition. + git read-tree main + make_resolve_tree ancestor A B + make_resolve_tree ancestor B A + + # Choosing ours keeps precisely the side selected by the merge direction. + git read-tree A + make_resolve_tree ours A B + git read-tree B + make_resolve_tree ours B A +) + +(cd deleted-file-added-gitlink-directory + # Both operations are compatible, so forced conflict resolution changes nothing: + # the shared deletion applies and B's directory remains in both directions. + git read-tree B + make_resolve_tree ancestor A B + make_resolve_tree ancestor B A + make_resolve_tree ours A B + make_resolve_tree ours B A +) + +(cd added-symlink-blocks-gitlink-directory + # The ancestor is empty, so choosing it keeps neither addition in either direction. + git read-tree main + make_resolve_tree ancestor A B + make_resolve_tree ancestor B A + + # Choosing ours keeps precisely the side named first: either A's symlink or B's + # directory containing the nested gitlink. + git read-tree A + make_resolve_tree ours A B + git read-tree B + make_resolve_tree ours B A +) + +(cd modified-file-vs-gitlink-directory + # Ancestor resolution restores the original symlink in both directions. + git read-tree main + make_resolve_tree ancestor A B + make_resolve_tree ancestor B A + + # Choosing ours keeps precisely the selected replacement. + git read-tree A + make_resolve_tree ours A B + git read-tree B + make_resolve_tree ours B A +) + +(cd relocated-addition-blocked-by-rename + # The explicit file rename prevents the inferred directory rename from relocating + # B's addition through a non-tree. The changes are therefore compatible, and forced + # conflict resolution has nothing to discard in either direction. + IFS= read -r -d '' merged_tree_id crate::Result { if actual_id != expected_tree_id { baseline::show_diff_trees_and_fail(&case_name, actual_id, &actual, expected_tree_id, &basename, &odb); } - if resolve_with_ours { + if resolve_with_ours && merge_info.conflicts.is_some() { assert!( !actual.has_unresolved_conflicts(conflicts_like_in_git), "We have forcefully resolved all conflicts, as far as Git would be concerned\n{:#?}", @@ -202,11 +202,11 @@ fn run_baseline() -> crate::Result { } assert_eq!( - actual_cases, 129, + actual_cases, 165, "BUG: update this number, and don't forget to remove a filter in the end" ); assert_eq!( - skipped_tree_resolve_cases, 118, + skipped_tree_resolve_cases, 130, "this is done when no case is skipped, and we don't want to accidentally skip them.\ Some don't actually have conflicts.\ The ones we skipped don't have irreconcilable conflicts" From bce0968305f95f366c3f0fe4bd7ad5748d6a0cde Mon Sep 17 00:00:00 2001 From: Byron Date: Thu, 6 Aug 2026 13:24:31 +0200 Subject: [PATCH 3/4] DEL SPILL --- gix-merge/src/tree/function/resolve.rs | 1917 ++++++++++++++++++++++++ 1 file changed, 1917 insertions(+) create mode 100644 gix-merge/src/tree/function/resolve.rs diff --git a/gix-merge/src/tree/function/resolve.rs b/gix-merge/src/tree/function/resolve.rs new file mode 100644 index 00000000000..bc8226a7d68 --- /dev/null +++ b/gix-merge/src/tree/function/resolve.rs @@ -0,0 +1,1917 @@ +//! Tree-merge scheduling and conflict resolution. +//! +//! See [`tree()`] for the main entrypoint and how it works. + +use std::borrow::Cow; + +use bstr::{BString, ByteSlice}; +use gix_diff::tree_with_rewrites::Change; +use gix_hash::ObjectId; +use gix_object::{ + FindExt, tree, + tree::{EntryKind, EntryMode}, +}; + +use crate::tree::{ + Conflict, ConflictIndexEntry, ConflictIndexEntryPathHint, ConflictMapping, + ConflictMapping::{Original, Swapped}, + ContentMerge, Error, Options, Outcome, Resolution, ResolutionFailure, ResolveWith, + utils::{ + ChangeDisposition, ChangeList, PossibleConflict, TrackedChange, apply_change, perform_blob_merge, + possibly_rewritten_location, rewrite_location_with_renamed_directory, to_components, unique_path_in_tree, + }, +}; + +use super::change::{MatchKind, collect as collect_changes, matching as matching_change, pair as pair_candidate}; + +/// Perform a merge between `our_tree` and `their_tree`, using `base_tree` as merge-base. +/// Note that `base_tree` can be an empty tree to indicate 'no common ancestor between the two sides'. +/// +/// * `labels` are relevant for text-merges and will be shown in conflicts. +/// * `objects` provides access to trees when diffing them. +/// * `write_blob_to_odb(content) -> Result` writes newly merged content into the odb to obtain an id +/// that will be used in merged trees. +/// * `diff_state` is state used for diffing trees. +/// * `diff_resource_cache` is used for similarity checks. +/// * `blob_merge` is a pre-configured platform to merge any content. +/// - Note that it shouldn't be allowed to read from the worktree, given that this is a tree-merge. +/// * `options` are used to affect how the merge is performed. +/// +/// ### Side handling +/// +/// The scheduler swaps the sides to share resolution logic instead of generally privileging one input. +/// Exact merge symmetry is not guaranteed: ambiguous rename candidates and some overlapping structural +/// conflicts can currently produce different trees when ours and theirs are reversed. Conflict-marker +/// content and forced "ours" resolution are directional by definition. +/// +/// ### Algorithm +/// +/// 1. Diff the ancestor against each side, including rename detection, to obtain two flat lists of tracked changes. +/// 2. Build a path tree for each list. Its nodes point back to list entries and make same-path, tree/non-tree, +/// and renamed-directory interactions discoverable. +/// 3. Start an editor at the ancestor tree and process pending changes from one list against the other list's path tree. +/// A change can be applied directly, paired with another change and merged, consumed only as part of a conflict, or +/// transformed into a deferred change at a rewritten or unique path. +/// 4. Append deferred changes as pending work, then swap the two side-lists and repeat until neither side has pending +/// changes. Swapping roles lets the same scheduling and resolution code process both inputs. +/// +/// Each tracked change therefore records both whether it still needs processing and whether its effect is actually +/// represented in the editor. This is why "processed without application" is distinct from "applied": a forced +/// ancestor resolution may consume a deletion while retaining the ancestor entry, and later path conflicts must not +/// behave as if that deletion had removed it. +/// +/// ### Differences to Merge-ORT +/// +/// Merge-ORT (Git) defines the desired outcomes where are merely mimicked here. The algorithms are different, and it's +/// clear that Merge-ORT is significantly more elaborate and general. +/// +/// It also writes out trees once it's done with them in a form of reduction process, here an editor is used +/// to keep only the changes, to be written by the caller who receives it as part of the result. +/// This may use more memory in the worst case scenario, but in average *shouldn't* perform much worse due to the +/// natural sparsity of the editor. +/// +/// Our rename-tracking also produces copy information, but we discard it and simply treat it like an addition. +/// +/// Finally, our algorithm will consider reasonable solutions to merge-conflicts as conflicts that are resolved, leaving +/// only content with conflict markers as unresolved ones. +/// +/// ### Performance +/// +/// Note that `objects` *should* have an object cache to greatly accelerate tree-retrieval. +#[expect(clippy::too_many_arguments)] +pub fn tree<'objects, E>( + base_tree: &gix_hash::oid, + our_tree: &gix_hash::oid, + their_tree: &gix_hash::oid, + mut labels: crate::blob::builtin_driver::text::Labels<'_>, + objects: &'objects impl gix_object::FindObjectOrHeader, + mut write_blob_to_odb: impl FnMut(&[u8]) -> Result, + diff_state: &mut gix_diff::tree::State, + diff_resource_cache: &mut gix_diff::blob::Platform, + blob_merge: &mut crate::blob::Platform, + options: Options, +) -> Result, Error> +where + E: Into>, +{ + let _span = gix_trace::coarse!("gix_merge::tree", ?base_tree, ?our_tree, ?their_tree, ?labels); + let (mut base_buf, mut side_buf) = (Vec::new(), Vec::new()); + let ancestor_tree = objects.find_tree(base_tree, &mut base_buf)?; + let mut editor = tree::Editor::new(ancestor_tree.to_owned(), objects, base_tree.kind()); + let tree_conflicts = options.tree_conflicts; + + let mut ours = collect_changes( + base_tree, + our_tree, + &base_buf, + &mut side_buf, + objects, + diff_resource_cache, + diff_state, + options.rewrites, + )?; + let mut theirs = collect_changes( + base_tree, + their_tree, + &base_buf, + &mut side_buf, + objects, + diff_resource_cache, + diff_state, + options.rewrites, + )?; + let mut conflicts = Vec::new(); + let mut failed_on_first_conflict = false; + let mut should_fail_on_conflict = |mut conflict: Conflict| -> bool { + if tree_conflicts.is_some() { + if let Err(failure) = conflict.resolution { + conflict.resolution = Ok(Resolution::Forced(failure)); + } + } + if let Some(how) = options.fail_on_conflict { + if conflict.resolution.is_err() || conflict.is_unresolved(how) { + failed_on_first_conflict = true; + } + } + conflicts.push(conflict); + failed_on_first_conflict + }; + + // Ambiguous rewrite identities otherwise make the side processed first decide + // which repeated additions are paired. Give both directions the same schedule. + let canonicalize_schedule = ours.has_ambiguous_rewrite_sources() || theirs.has_ambiguous_rewrite_sources(); + let swap_sides_for_schedule = canonicalize_schedule && theirs.cmp_for_scheduling(&ours).is_gt(); + let ((mut our_changes, mut our_tree), (mut their_changes, mut their_tree)) = (ours.parts_mut(), theirs.parts_mut()); + let mut outer_side = Original; + if their_changes.is_empty() || (!our_changes.is_empty() && swap_sides_for_schedule) { + ((our_changes, our_tree), (their_changes, their_tree)) = ((their_changes, their_tree), (our_changes, our_tree)); + (labels.current, labels.other) = (labels.other, labels.current); + outer_side = outer_side.swapped(); + } + + 'outer: while their_changes.iter().rev().any(TrackedChange::is_pending) { + let mut segment_start = 0; + let mut last_seen_len = their_changes.len(); + + while segment_start != last_seen_len { + for theirs_idx in segment_start..last_seen_len { + // `their` can be a tree, and it could be used to efficiently prune child-changes as these + // trees are always rewrites with parent ids (of course we validate), so child-changes could be handled + // quickly. However, for now the benefit of having these trees is to have them as part of the match-tree + // on *our* side so that it's clear that we passed a renamed directory (by identity). + let TrackedChange { + inner: theirs, + needs_tree_insertion, + rewritten_location, + .. + } = &their_changes[theirs_idx]; + if theirs.entry_mode().is_tree() || !their_changes[theirs_idx].is_pending() { + continue; + } + + if needs_tree_insertion.is_some() { + their_tree.insert(theirs, theirs_idx); + } + + match matching_change( + theirs, + *needs_tree_insertion, + rewritten_location.as_ref(), + our_tree, + our_changes, + ) { + None => { + if let Some((rewritten_location, ours_idx)) = rewritten_location { + // `no_entry` to the index because that's not a conflict at all, + // but somewhat advanced rename tracking. + if should_fail_on_conflict(Conflict::with_resolution( + Resolution::SourceLocationAffectedByRename { + final_location: rewritten_location.to_owned(), + }, + (&our_changes[*ours_idx].inner, theirs, Original, outer_side), + [None, None, None], + )) { + break 'outer; + } + editor.remove(to_components(theirs.location()))?; + } + apply_change(&mut editor, theirs, rewritten_location.as_ref().map(|t| &t.0))?; + their_changes[theirs_idx].mark_applied(); + } + Some(candidate) => { + use crate::tree::utils::to_components_bstring_ref as toc; + + if let PossibleConflict::PassedRewrittenDirectory { change_idx } = candidate { + let ours = &our_changes[change_idx]; + let location_after_passed_rename = + rewrite_location_with_renamed_directory(theirs.location(), &ours.inner); + if let Some(new_location) = location_after_passed_rename { + // Another structural conflict may already have consumed this scheduling node. + their_tree.remove_change(theirs.location()); + push_deferred_with_rewrite( + (theirs.clone(), Some(change_idx)), + Some((new_location, change_idx)), + their_changes, + ); + } else { + apply_change(&mut editor, theirs, None)?; + their_changes[theirs_idx].mark_applied(); + } + their_changes[theirs_idx].mark_processed(); + continue; + } + + let (ours_idx, match_kind) = pair_candidate(&candidate, our_changes); + let Some(ours_idx) = ours_idx else { + let ours = match candidate { + PossibleConflict::TreeToNonTree { change_idx, .. } + | PossibleConflict::NonTreeToTree { change_idx, .. } => change_idx, + PossibleConflict::Match { change_idx } + | PossibleConflict::PassedRewrittenDirectory { change_idx } => Some(change_idx), + } + .map(|idx| &mut our_changes[idx]); + + if let Some(ours) = ours { + gix_trace::debug!( + "Turning a case we could probably handle into a conflict for now. theirs: {theirs:#?} ours: {ours:#?} kind: {match_kind:?}" + ); + let conflict = Conflict::unknown((&ours.inner, theirs, Original, outer_side)); + if let Some(ResolveWith::Ours) = tree_conflicts { + apply_our_resolution(&ours.inner, theirs, outer_side, &mut editor)?; + match outer_side { + Original => ours.mark_applied(), + Swapped => their_changes[theirs_idx].mark_applied(), + } + } + if should_fail_on_conflict(conflict) { + break 'outer; + } + } else if matches!(candidate, PossibleConflict::TreeToNonTree { .. }) { + let (mode, id) = theirs.entry_mode_and_id(); + let location = theirs.location(); + if needs_tree_insertion.is_some() { + their_tree.remove_change(location); + } + let renamed_location = unique_path_in_tree( + location.as_bstr(), + &editor, + their_tree, + labels.other.unwrap_or_default(), + )?; + match tree_conflicts { + None => { + editor.upsert(toc(&renamed_location), mode.kind(), id.to_owned())?; + } + Some(ResolveWith::Ours) => { + if outer_side.is_swapped() { + editor.upsert(to_components(location), mode.kind(), id.to_owned())?; + } + } + Some(ResolveWith::Ancestor) => { + // we found no matching node of 'ours', so nothing to apply here. + } + } + + let conflict = Conflict::without_resolution( + ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { + renamed_unique_path_of_theirs: renamed_location, + }, + (theirs, theirs, Original, outer_side), + [ + None, + None, + index_entry_at_path( + &mode.kind().into(), + &id.to_owned(), + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], + ); + their_changes[theirs_idx].mark_processed(); + if should_fail_on_conflict(conflict) { + break 'outer; + } + } else if matches!(candidate, PossibleConflict::NonTreeToTree { .. }) { + // We are writing on top of what was a file, a conflict we probably already saw and dealt with. + let location = theirs.location(); + let (mode, id) = theirs.entry_mode_and_id(); + editor.upsert(to_components(location), mode.kind(), id.to_owned())?; + their_changes[theirs_idx].mark_applied(); + } else { + gix_trace::debug!( + "Couldn't figure out how to handle {match_kind:?} theirs: {theirs:#?} candidate: {candidate:#?}" + ); + } + continue; + }; + + let mut ours_disposition = ChangeDisposition::Processed; + let mut theirs_disposition = ChangeDisposition::Processed; + let ours = &our_changes[ours_idx].inner; + match (ours, theirs) { + ( + Change::Modification { + previous_id, + previous_entry_mode, + id: our_id, + location: our_location, + entry_mode: our_mode, + .. + }, + Change::Rewrite { + source_id: their_source_id, + id: their_id, + location: their_location, + entry_mode: their_mode, + source_location, + .. + }, + ) + | ( + Change::Rewrite { + source_id: their_source_id, + id: their_id, + location: their_location, + entry_mode: their_mode, + source_location, + .. + }, + Change::Modification { + previous_id, + previous_entry_mode, + id: our_id, + location: our_location, + entry_mode: our_mode, + .. + }, + ) => { + let side = if matches!(ours, Change::Modification { .. }) { + Original + } else { + Swapped + }; + if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { + debug_assert_eq!( + previous_id, their_source_id, + "both refer to the same base, so should always match" + ); + let their_rewritten_location = possibly_rewritten_location( + pick_mut(side, our_tree, their_tree), + their_location.as_ref(), + pick(side, our_changes, their_changes), + ); + let renamed_without_change = their_source_id == their_id; + let (merged_blob_id, resolution) = if renamed_without_change { + (*our_id, None) + } else { + let (our_location, our_id, our_mode, their_location, their_id, their_mode) = + match side { + Original => ( + our_location, + our_id, + our_mode, + their_location, + their_id, + their_mode, + ), + Swapped => ( + their_location, + their_id, + their_mode, + our_location, + our_id, + our_mode, + ), + }; + let (merged_blob_id, resolution) = perform_blob_merge( + labels, + objects, + blob_merge, + &mut diff_state.buf1, + &mut write_blob_to_odb, + (our_location, *our_id, *our_mode), + (their_location, *their_id, *their_mode), + (source_location, *previous_id, *previous_entry_mode), + (0, outer_side), + &options, + )?; + (merged_blob_id, Some(resolution)) + }; + + editor.remove(toc(our_location))?; + pick_mut(side, our_tree, their_tree).remove_existing_change(our_location.as_bstr()); + let final_location = their_rewritten_location.clone(); + let new_change = Change::Addition { + location: their_rewritten_location.unwrap_or_else(|| their_location.to_owned()), + relation: None, + entry_mode: merged_mode, + id: merged_blob_id, + }; + if should_fail_on_conflict(Conflict::with_resolution( + Resolution::OursModifiedTheirsRenamedAndChangedThenRename { + merged_mode: (merged_mode != *their_mode).then_some(merged_mode), + merged_blob: resolution.map(|resolution| ContentMerge { + resolution, + merged_blob_id, + }), + final_location, + }, + (ours, theirs, side, outer_side), + [ + index_entry(previous_entry_mode, previous_id), + index_entry(our_mode, our_id), + index_entry(their_mode, their_id), + ], + )) { + break 'outer; + } + + // The other side gets the addition, not our side. + push_deferred((new_change, None), pick_mut(side, their_changes, our_changes)); + } else { + match tree_conflicts { + None => { + // keep both states - 'our_location' is the previous location as well. + editor.upsert(toc(our_location), our_mode.kind(), *our_id)?; + editor.upsert(toc(their_location), their_mode.kind(), *their_id)?; + } + Some(ResolveWith::Ours) => { + editor.remove(toc(source_location))?; + if side.to_global(outer_side).is_swapped() { + editor.upsert(toc(their_location), their_mode.kind(), *their_id)?; + } else { + editor.upsert(toc(our_location), our_mode.kind(), *our_id)?; + } + } + Some(ResolveWith::Ancestor) => {} + } + + if should_fail_on_conflict(Conflict::without_resolution( + ResolutionFailure::OursModifiedTheirsRenamedTypeMismatch, + (ours, theirs, side, outer_side), + [ + index_entry_at_path( + previous_entry_mode, + previous_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + None, + index_entry_at_path( + their_mode, + their_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], + )) { + break 'outer; + } + } + } + ( + Change::Modification { + location, + previous_id, + previous_entry_mode, + entry_mode: our_mode, + id: our_id, + .. + }, + Change::Modification { + entry_mode: their_mode, + id: their_id, + .. + }, + ) if !involves_submodule(our_mode, their_mode) + && merge_modes(*our_mode, *their_mode).is_some() + && our_id != their_id => + { + let previous_is_compatible = merge_modes(*our_mode, *previous_entry_mode).is_some() + && merge_modes(*their_mode, *previous_entry_mode).is_some(); + let merged_mode = if previous_is_compatible { + merge_modes_prev(*our_mode, *their_mode, *previous_entry_mode) + } else { + merge_modes(*our_mode, *their_mode) + } + .expect("the match guard assures compatible current modes"); + let (merge_base_id, merge_base_mode) = if previous_is_compatible { + (*previous_id, *previous_entry_mode) + } else { + (previous_id.kind().null(), merged_mode) + }; + let (merged_blob_id, resolution) = perform_blob_merge( + labels, + objects, + blob_merge, + &mut diff_state.buf1, + &mut write_blob_to_odb, + (location, *our_id, *our_mode), + (location, *their_id, *their_mode), + (location, merge_base_id, merge_base_mode), + (0, outer_side), + &options, + )?; + + editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; + if should_fail_on_conflict(Conflict::with_resolution( + Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { + merged_blob: ContentMerge { + resolution, + merged_blob_id, + }, + }, + (ours, theirs, Original, outer_side), + [ + index_entry(previous_entry_mode, previous_id), + index_entry(our_mode, our_id), + index_entry(their_mode, their_id), + ], + )) { + break 'outer; + } + } + (Change::Deletion { .. }, Change::Addition { .. }) + if matches!(match_kind, Some(MatchKind::EraseLeaf)) + && !our_changes[ours_idx].was_processed_without_application() => + { + // Let the shared parent deletion pair with the other side's deletion first. + // Applying it after this descendant would remove the newly created directory. + push_deferred((theirs.clone(), Some(ours_idx)), their_changes); + } + (Change::Rewrite { .. }, Change::Addition { relation: Some(_), .. }) + if matches!(match_kind, Some(MatchKind::EraseLeaf)) + && needs_tree_insertion.is_none() + && !matches!(tree_conflicts, Some(ResolveWith::Ancestor)) => + { + // Let the replacement's parent deletion resolve the rename first. + // The deferred child then ignores this already-handled rewrite. + push_deferred((theirs.clone(), Some(ours_idx)), their_changes); + } + (Change::Rewrite { .. }, Change::Addition { .. }) + if matches!(match_kind, Some(MatchKind::EraseLeaf)) && rewritten_location.is_some() => + { + // An explicit file rename blocks the inferred directory-rename destination. + // Keep the explicit rename and apply the addition at its original location. + apply_change(&mut editor, ours, None)?; + apply_change(&mut editor, theirs, None)?; + ours_disposition = ChangeDisposition::Applied; + theirs_disposition = ChangeDisposition::Applied; + } + ( + Change::Rewrite { + source_location, + entry_mode: blocking_mode, + id: blocking_id, + location: blocking_location, + .. + }, + Change::Addition { .. }, + ) if matches!(match_kind, Some(MatchKind::EraseLeaf)) => { + let renamed_location = unique_path_in_tree( + blocking_location.as_bstr(), + &editor, + our_tree, + labels.current.unwrap_or_default(), + )?; + let conflict = Conflict::without_resolution( + ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { + renamed_unique_path_of_theirs: renamed_location.clone(), + }, + (ours, theirs, Swapped, outer_side), + [ + None, + None, + index_entry_at_path( + blocking_mode, + blocking_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], + ); + + match tree_conflicts { + None => { + editor.remove(toc(source_location))?; + editor.remove(toc(blocking_location))?; + our_tree.remove_change(blocking_location.as_bstr()); + editor.upsert(toc(&renamed_location), blocking_mode.kind(), *blocking_id)?; + apply_change(&mut editor, theirs, None)?; + ours_disposition = ChangeDisposition::Applied; + theirs_disposition = ChangeDisposition::Applied; + } + Some(ResolveWith::Ours) => match outer_side { + Original => { + apply_change(&mut editor, ours, None)?; + ours_disposition = ChangeDisposition::Applied; + } + Swapped => { + apply_change(&mut editor, theirs, None)?; + theirs_disposition = ChangeDisposition::Applied; + } + }, + Some(ResolveWith::Ancestor) => {} + } + + if should_fail_on_conflict(conflict) { + break 'outer; + } + } + ( + Change::Addition { + location: blocking_location, + entry_mode: blocking_mode, + id: blocking_id, + .. + }, + Change::Addition { .. }, + ) if matches!(match_kind, Some(MatchKind::EraseLeaf)) => { + // `ours` is the non-tree prefix of `theirs`, whose parent directories + // are represented only by already-applied structural changes. Preserve + // the directory at its intended path and move the blocking addition. + let renamed_location = unique_path_in_tree( + blocking_location.as_bstr(), + &editor, + our_tree, + labels.current.unwrap_or_default(), + )?; + let conflict = Conflict::without_resolution( + ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { + renamed_unique_path_of_theirs: renamed_location.clone(), + }, + (ours, theirs, Swapped, outer_side), + [ + None, + None, + index_entry_at_path( + blocking_mode, + blocking_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], + ); + + match tree_conflicts { + None => { + editor.remove(toc(blocking_location))?; + our_tree.remove_change(blocking_location.as_bstr()); + editor.upsert(toc(&renamed_location), blocking_mode.kind(), *blocking_id)?; + apply_change(&mut editor, theirs, None)?; + ours_disposition = ChangeDisposition::Applied; + theirs_disposition = ChangeDisposition::Applied; + } + Some(ResolveWith::Ours) => match outer_side { + Original => { + apply_change(&mut editor, ours, None)?; + ours_disposition = ChangeDisposition::Applied; + } + Swapped => { + editor.remove(toc(blocking_location))?; + our_tree.remove_change(blocking_location.as_bstr()); + apply_change(&mut editor, theirs, None)?; + theirs_disposition = ChangeDisposition::Applied; + } + }, + Some(ResolveWith::Ancestor) => {} + } + + if should_fail_on_conflict(conflict) { + break 'outer; + } + } + ( + Change::Addition { + location, + entry_mode: our_mode, + id: our_id, + .. + }, + Change::Addition { + entry_mode: their_mode, + id: their_id, + .. + }, + ) if !involves_submodule(our_mode, their_mode) && our_id != their_id => { + let conflict = if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { + let side = if our_mode == their_mode || matches!(our_mode.kind(), EntryKind::Blob) { + outer_side + } else { + outer_side.swapped() + }; + let (merged_blob_id, resolution) = perform_blob_merge( + labels, + objects, + blob_merge, + &mut diff_state.buf1, + &mut write_blob_to_odb, + (location, *our_id, merged_mode), + (location, *their_id, merged_mode), + (location, their_id.kind().null(), merged_mode), + (0, side), + &options, + )?; + editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; + Conflict::with_resolution( + Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { + merged_blob: ContentMerge { + resolution, + merged_blob_id, + }, + }, + (ours, theirs, Original, outer_side), + [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], + ) + } else { + // Actually this has a preference, as symlinks are always left in place with the other side renamed. + let ( + logical_side, + label_of_side_to_be_moved, + (our_mode, our_id, our_path_hint), + (their_mode, their_id, their_path_hint), + ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) { + ( + Original, + labels.other.unwrap_or_default(), + (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), + (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), + ) + } else { + ( + Swapped, + labels.current.unwrap_or_default(), + (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), + (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), + ) + }; + let tree_with_rename = pick_mut(logical_side, their_tree, our_tree); + let renamed_location = unique_path_in_tree( + location.as_bstr(), + &editor, + tree_with_rename, + label_of_side_to_be_moved, + )?; + let mut conflict = Conflict::without_resolution( + ResolutionFailure::OursAddedTheirsAddedTypeMismatch { + their_unique_location: renamed_location.clone(), + }, + (ours, theirs, logical_side, outer_side), + [ + None, + index_entry_at_path(&our_mode, &our_id, our_path_hint), + index_entry_at_path(&their_mode, &their_id, their_path_hint), + ], + ); + match tree_conflicts { + None => { + let new_change = Change::Addition { + location: renamed_location, + entry_mode: their_mode, + id: their_id, + relation: None, + }; + editor.upsert(toc(location), our_mode.kind(), our_id)?; + tree_with_rename.remove_change(location.as_bstr()); + push_deferred( + (new_change, None), + pick_mut(logical_side, their_changes, our_changes), + ); + } + Some(resolve) => { + conflict.entries = Default::default(); + match resolve { + ResolveWith::Ours => match outer_side { + Original => { + editor.upsert(toc(location), our_mode.kind(), our_id)?; + } + Swapped => { + editor.upsert(toc(location), their_mode.kind(), their_id)?; + } + }, + ResolveWith::Ancestor => { + // Do nothing - this discards both sides. + // Note that one of these adds might be the result of a rename, which + // means we effectively loose the original and can't get it back as that information is degenerated. + } + } + } + } + conflict + }; + + if should_fail_on_conflict(conflict) { + break 'outer; + } + } + ( + Change::Modification { + location, + entry_mode, + id, + previous_entry_mode, + previous_id, + }, + Change::Deletion { .. }, + ) + | ( + Change::Deletion { .. }, + Change::Modification { + location, + entry_mode, + id, + previous_entry_mode, + previous_id, + }, + ) => { + let (label_of_side_to_be_moved, side) = if matches!(ours, Change::Modification { .. }) { + (labels.current.unwrap_or_default(), Original) + } else { + (labels.other.unwrap_or_default(), Swapped) + }; + let deletion_replaced_by_directory = { + // The deleted leaf is replaced by a dir added at the same location. + // Rename-tracking sort order shouldn't be dependent on here, but maybe + // could one day once rename tracking caught up with Git. + let changes = match side { + Original => &their_changes, + Swapped => &our_changes, + }; + changes.iter().any(|change| { + change.inner.entry_mode().is_tree() + && matches!(change.inner, Change::Addition { .. }) + && change.inner.location() == location + }) + }; + + let should_break = if deletion_replaced_by_directory { + let entries = [ + index_entry(previous_entry_mode, previous_id), + index_entry(entry_mode, id), + None, + ]; + match tree_conflicts { + None => { + let our_tree = pick_mut(side, our_tree, their_tree); + let renamed_path = unique_path_in_tree( + location.as_bstr(), + &editor, + our_tree, + label_of_side_to_be_moved, + )?; + editor.remove(toc(location))?; + our_tree.remove_existing_change(location.as_bstr()); + + let new_change = Change::Addition { + location: renamed_path.clone(), + relation: None, + entry_mode: *entry_mode, + id: *id, + }; + let should_break = should_fail_on_conflict(Conflict::without_resolution( + ResolutionFailure::OursModifiedTheirsDirectoryThenOursRenamed { + renamed_unique_path_to_modified_blob: renamed_path, + }, + (ours, theirs, side, outer_side), + entries, + )); + + // Since we move *our* side, our tree needs to be modified. + push_deferred( + (new_change, None), + pick_mut(side, our_changes, their_changes), + ); + should_break + } + Some(ResolveWith::Ours) => { + match side.to_global(outer_side) { + Original => { + // ours is modification + editor.upsert(toc(location), entry_mode.kind(), *id)?; + } + Swapped => { + // ours is deletion + editor.remove(toc(location))?; + } + } + should_fail_on_conflict(Conflict::without_resolution( + ResolutionFailure::OursModifiedTheirsDeleted, + (ours, theirs, side, outer_side), + entries, + )) + } + Some(ResolveWith::Ancestor) => { + should_fail_on_conflict(Conflict::without_resolution( + ResolutionFailure::OursModifiedTheirsDeleted, + (ours, theirs, side, outer_side), + entries, + )) + } + } + } else { + let entries = [ + index_entry(previous_entry_mode, previous_id), + index_entry(entry_mode, id), + None, + ]; + match tree_conflicts { + None => { + editor.upsert(toc(location), entry_mode.kind(), *id)?; + } + Some(ResolveWith::Ours) => { + let ours = match outer_side { + Original => ours, + Swapped => theirs, + }; + + match ours { + Change::Modification { .. } => { + editor.upsert(toc(location), entry_mode.kind(), *id)?; + } + Change::Deletion { .. } => { + editor.remove(toc(location))?; + } + _ => unreachable!("parent-match assures this"), + } + } + Some(ResolveWith::Ancestor) => {} + } + should_fail_on_conflict(Conflict::without_resolution( + ResolutionFailure::OursModifiedTheirsDeleted, + (ours, theirs, side, outer_side), + entries, + )) + }; + let deletion_was_applied = match tree_conflicts { + None => deletion_replaced_by_directory, + Some(ResolveWith::Ours) => side.to_global(outer_side).is_swapped(), + Some(ResolveWith::Ancestor) => false, + }; + if deletion_was_applied { + match side { + Original => theirs_disposition = ChangeDisposition::Applied, + Swapped => ours_disposition = ChangeDisposition::Applied, + } + } + if should_break { + break 'outer; + } + } + ( + Change::Modification { .. }, + Change::Addition { + location, + entry_mode, + id, + .. + }, + ) if ours.location() != theirs.location() => { + match tree_conflicts { + None => { + // A file-to-directory diff can yield the descendant addition + // before the deletion of the blocking base file. Defer it so + // the modification/deletion pair can relocate the modification + // and remove this structural match first. + push_deferred((theirs.clone(), Some(ours_idx)), their_changes); + } + Some(ResolveWith::Ancestor) => {} + Some(ResolveWith::Ours) => { + if outer_side.is_swapped() { + editor.upsert(toc(location), entry_mode.kind(), *id)?; + } + // we have already taken care of the 'root' of this - + // everything that follows can safely be ignored + } + } + } + ( + Change::Rewrite { + entry_mode: tree_mode, + location: tree_location, + .. + }, + Change::Rewrite { + source_location, + entry_mode, + id, + location, + .. + }, + ) if tree_mode.is_tree() && tree_location == location => { + let renamed_location = unique_path_in_tree( + location.as_bstr(), + &editor, + our_tree, + labels.other.unwrap_or_default(), + )?; + let conflict = Conflict::without_resolution( + ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { + renamed_unique_path_of_theirs: renamed_location.clone(), + }, + (ours, theirs, Original, outer_side), + [ + None, + None, + index_entry_at_path( + entry_mode, + id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], + ); + + match tree_conflicts { + None => { + editor.remove(toc(source_location))?; + editor.upsert(toc(&renamed_location), entry_mode.kind(), *id)?; + their_tree.remove_existing_change(location.as_bstr()); + ours_disposition = ChangeDisposition::Applied; + theirs_disposition = ChangeDisposition::Applied; + } + Some(ResolveWith::Ours) => { + apply_our_resolution(ours, theirs, outer_side, &mut editor)?; + match outer_side { + Original => { + their_tree.remove_existing_change(location.as_bstr()); + ours_disposition = ChangeDisposition::Applied; + } + Swapped => { + our_tree.remove_existing_change(tree_location.as_bstr()); + theirs_disposition = ChangeDisposition::Applied; + } + } + } + Some(ResolveWith::Ancestor) => {} + } + + if should_fail_on_conflict(conflict) { + break 'outer; + } + } + ( + Change::Rewrite { + source_location, + entry_mode: tree_mode, + .. + }, + Change::Rewrite { location, .. }, + ) if tree_mode.is_tree() + && location == source_location + && matches!(match_kind, Some(MatchKind::EraseTree)) => + { + // The leaf rename occupies a path vacated by the directory rename. + // Descendant changes resolve the actual rename/delete conflict. + match tree_conflicts { + None => { + apply_change(&mut editor, theirs, None)?; + theirs_disposition = ChangeDisposition::Applied; + } + Some(ResolveWith::Ours) => { + apply_our_resolution(ours, theirs, outer_side, &mut editor)?; + match outer_side { + Original => ours_disposition = ChangeDisposition::Applied, + Swapped => theirs_disposition = ChangeDisposition::Applied, + } + } + Some(ResolveWith::Ancestor) => {} + } + } + ( + Change::Rewrite { + source_location: our_source_location, + entry_mode: our_mode, + id: our_id, + location, + .. + }, + Change::Rewrite { + source_location: their_source_location, + entry_mode: their_mode, + id: their_id, + location: their_location, + .. + }, + ) if our_source_location != their_source_location + && location == their_location + && our_mode == their_mode + && our_id == their_id => + { + editor.remove(toc(our_source_location))?; + editor.remove(toc(their_source_location))?; + our_tree.remove_change(our_source_location.as_bstr()); + their_tree.remove_change(their_source_location.as_bstr()); + editor.upsert(toc(location), our_mode.kind(), *our_id)?; + ours_disposition = ChangeDisposition::Applied; + theirs_disposition = ChangeDisposition::Applied; + } + ( + Change::Rewrite { + source_location: our_source_location, + entry_mode: our_mode, + id: our_id, + location, + .. + }, + Change::Rewrite { + source_location: their_source_location, + entry_mode: their_mode, + id: their_id, + location: their_location, + .. + }, + ) if our_source_location != their_source_location + && location == their_location + && !involves_submodule(our_mode, their_mode) => + { + match tree_conflicts { + None => { + editor.remove(toc(our_source_location))?; + editor.remove(toc(their_source_location))?; + our_tree.remove_change(our_source_location.as_bstr()); + their_tree.remove_change(their_source_location.as_bstr()); + let conflict = if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { + let (merged_blob_id, resolution) = perform_blob_merge( + labels, + objects, + blob_merge, + &mut diff_state.buf1, + &mut write_blob_to_odb, + (location, *our_id, *our_mode), + (location, *their_id, *their_mode), + (location, our_id.kind().null(), merged_mode), + (0, outer_side), + &options, + )?; + editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; + Conflict::with_resolution( + Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { + merged_blob: ContentMerge { + resolution, + merged_blob_id, + }, + }, + (ours, theirs, Original, outer_side), + [ + None, + index_entry(our_mode, our_id), + index_entry(their_mode, their_id), + ], + ) + } else { + // Like add/add type conflicts, retain the symlink at the contested path and + // move the regular file to a side-qualified path. + let ( + logical_side, + label_of_side_to_be_moved, + (our_mode, our_id, our_path_hint), + (their_mode, their_id, their_path_hint), + moved_tree, + ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) { + ( + Original, + labels.other.unwrap_or_default(), + (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), + ( + *their_mode, + *their_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + &mut *their_tree, + ) + } else { + ( + Swapped, + labels.current.unwrap_or_default(), + (*their_mode, *their_id, ConflictIndexEntryPathHint::Current), + (*our_mode, *our_id, ConflictIndexEntryPathHint::RenamedOrTheirs), + &mut *our_tree, + ) + }; + let renamed_location = unique_path_in_tree( + location.as_bstr(), + &editor, + moved_tree, + label_of_side_to_be_moved, + )?; + editor.upsert(toc(location), our_mode.kind(), our_id)?; + editor.upsert(toc(&renamed_location), their_mode.kind(), their_id)?; + Conflict::without_resolution( + ResolutionFailure::OursAddedTheirsAddedTypeMismatch { + their_unique_location: renamed_location, + }, + (ours, theirs, logical_side, outer_side), + [ + None, + index_entry_at_path(&our_mode, &our_id, our_path_hint), + index_entry_at_path(&their_mode, &their_id, their_path_hint), + ], + ) + }; + if should_fail_on_conflict(conflict) { + break 'outer; + } + } + Some(resolve) => { + if matches!(resolve, ResolveWith::Ours) { + let (source, mode, id, tree) = match outer_side { + Original => (our_source_location, our_mode, our_id, &mut *our_tree), + Swapped => { + (their_source_location, their_mode, their_id, &mut *their_tree) + } + }; + editor.remove(toc(source))?; + tree.remove_change(source.as_bstr()); + editor.upsert(toc(location), mode.kind(), *id)?; + } + if should_fail_on_conflict(Conflict::unknown(( + ours, theirs, Original, outer_side, + ))) { + break 'outer; + } + } + } + } + ( + Change::Rewrite { + source_location, + entry_mode: our_mode, + id: our_id, + location, + .. + }, + Change::Addition { + id: their_id, + entry_mode: their_mode, + location: add_location, + .. + }, + ) + | ( + Change::Addition { + id: their_id, + entry_mode: their_mode, + location: add_location, + .. + }, + Change::Rewrite { + source_location, + entry_mode: our_mode, + id: our_id, + location, + .. + }, + ) if add_location + .strip_prefix(source_location.as_bytes()) + .is_some_and(|suffix| suffix.starts_with(b"/")) => + { + // The rewrite moves the file out of the way while the other side replaces it + // with a directory. The child is unrelated to the rewritten blob, so keep both + // instead of merging their contents at the rewrite destination. The preceding + // deletion/rewrite pairing already recorded the rename/delete conflict. + let side = if matches!(ours, Change::Rewrite { .. }) { + Original + } else { + Swapped + }; + match tree_conflicts { + None => { + editor.remove(toc(source_location))?; + pick_mut(side, our_tree, their_tree).remove_change(source_location.as_bstr()); + editor.upsert(toc(location), our_mode.kind(), *our_id)?; + editor.upsert(toc(add_location), their_mode.kind(), *their_id)?; + ours_disposition = ChangeDisposition::Applied; + theirs_disposition = ChangeDisposition::Applied; + } + Some(ResolveWith::Ours) => match side.to_global(outer_side) { + Original => { + editor.remove(toc(source_location))?; + editor.upsert(toc(location), our_mode.kind(), *our_id)?; + match side { + Original => ours_disposition = ChangeDisposition::Applied, + Swapped => theirs_disposition = ChangeDisposition::Applied, + } + } + Swapped => { + editor.remove(toc(source_location))?; + editor.upsert(toc(add_location), their_mode.kind(), *their_id)?; + match side { + Original => theirs_disposition = ChangeDisposition::Applied, + Swapped => ours_disposition = ChangeDisposition::Applied, + } + } + }, + Some(ResolveWith::Ancestor) => {} + } + } + ( + Change::Rewrite { + source_location: blocking_source, + entry_mode: blocking_mode, + id: blocking_id, + location: blocking_location, + .. + }, + Change::Rewrite { + source_location: nested_source, + .. + }, + ) if blocking_source != nested_source + && matches!(match_kind, Some(MatchKind::EraseLeaf)) => + { + // These are unrelated renames whose destinations form a file/directory + // conflict. Keep the directory at its intended path and move the blocking file. + let renamed_location = unique_path_in_tree( + blocking_location.as_bstr(), + &editor, + our_tree, + labels.current.unwrap_or_default(), + )?; + let conflict = Conflict::without_resolution( + ResolutionFailure::OursDirectoryTheirsNonDirectoryTheirsRenamed { + renamed_unique_path_of_theirs: renamed_location.clone(), + }, + (ours, theirs, Swapped, outer_side), + [ + None, + None, + index_entry_at_path( + blocking_mode, + blocking_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], + ); + + match tree_conflicts { + None => { + editor.remove(toc(blocking_source))?; + editor.remove(toc(blocking_location))?; + our_tree.remove_change(blocking_location.as_bstr()); + editor.upsert(toc(&renamed_location), blocking_mode.kind(), *blocking_id)?; + apply_change(&mut editor, theirs, None)?; + ours_disposition = ChangeDisposition::Applied; + theirs_disposition = ChangeDisposition::Applied; + } + Some(ResolveWith::Ours) => match outer_side { + Original => { + apply_change(&mut editor, ours, None)?; + ours_disposition = ChangeDisposition::Applied; + } + Swapped => { + apply_change(&mut editor, theirs, None)?; + theirs_disposition = ChangeDisposition::Applied; + } + }, + Some(ResolveWith::Ancestor) => {} + } + + if should_fail_on_conflict(conflict) { + break 'outer; + } + } + ( + Change::Rewrite { + source_location, + source_entry_mode, + source_id, + entry_mode: our_mode, + id: our_id, + location: our_location, + .. + }, + Change::Rewrite { + entry_mode: their_mode, + id: their_id, + location: their_location, + .. + }, + // NOTE: renames are only tracked among these kinds of types anyway, but we make sure. + ) if our_mode.is_blob_or_symlink() + && their_mode.is_blob_or_symlink() + && merge_modes(*our_mode, *their_mode).is_some() => + { + let (merged_blob_id, mut resolution) = if our_id == their_id { + (*our_id, None) + } else { + let (id, resolution) = perform_blob_merge( + labels, + objects, + blob_merge, + &mut diff_state.buf1, + &mut write_blob_to_odb, + (our_location, *our_id, *our_mode), + (their_location, *their_id, *their_mode), + (source_location, *source_id, *source_entry_mode), + (u8::from(our_location != their_location), outer_side), + &options, + )?; + (id, Some(resolution)) + }; + + let merged_mode = + merge_modes(*our_mode, *their_mode).expect("this case was assured earlier"); + + if matches!(tree_conflicts, None | Some(ResolveWith::Ours)) { + editor.remove(toc(source_location))?; + our_tree.remove_change(source_location.as_bstr()); + their_tree.remove_change(source_location.as_bstr()); + } + + let their_location = + possibly_rewritten_location(our_tree, their_location.as_bstr(), our_changes) + .map_or(Cow::Borrowed(their_location.as_bstr()), Cow::Owned); + let our_location = + possibly_rewritten_location(their_tree, our_location.as_bstr(), their_changes) + .map_or(Cow::Borrowed(our_location.as_bstr()), Cow::Owned); + let (our_addition, their_addition) = if our_location == their_location { + ( + None, + Some(Change::Addition { + location: our_location.into_owned(), + relation: None, + entry_mode: merged_mode, + id: merged_blob_id, + }), + ) + } else { + if should_fail_on_conflict(Conflict::without_resolution( + ResolutionFailure::OursRenamedTheirsRenamedDifferently { + merged_blob: resolution.take().map(|resolution| ContentMerge { + resolution, + merged_blob_id, + }), + }, + (ours, theirs, Original, outer_side), + [ + index_entry_at_path( + source_entry_mode, + source_id, + ConflictIndexEntryPathHint::Source, + ), + index_entry_at_path( + our_mode, + &merged_blob_id, + ConflictIndexEntryPathHint::Current, + ), + index_entry_at_path( + their_mode, + &merged_blob_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], + )) { + break 'outer; + } + match tree_conflicts { + None => { + let our_addition = Change::Addition { + location: our_location.into_owned(), + relation: None, + entry_mode: merged_mode, + id: merged_blob_id, + }; + let their_addition = Change::Addition { + location: their_location.into_owned(), + relation: None, + entry_mode: merged_mode, + id: merged_blob_id, + }; + (Some(our_addition), Some(their_addition)) + } + Some(ResolveWith::Ancestor) => (None, None), + Some(ResolveWith::Ours) => { + let our_addition = Change::Addition { + location: match outer_side { + Original => our_location, + Swapped => their_location, + } + .into_owned(), + relation: None, + entry_mode: merged_mode, + id: merged_blob_id, + }; + (Some(our_addition), None) + } + } + }; + + if let Some(resolution) = resolution { + if should_fail_on_conflict(Conflict::with_resolution( + Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { + merged_blob: ContentMerge { + resolution, + merged_blob_id, + }, + }, + (ours, theirs, Original, outer_side), + [ + index_entry_at_path( + source_entry_mode, + source_id, + ConflictIndexEntryPathHint::Source, + ), + index_entry_at_path( + our_mode, + &merged_blob_id, + ConflictIndexEntryPathHint::Current, + ), + index_entry_at_path( + their_mode, + &merged_blob_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], + )) { + break 'outer; + } + } + if let Some(addition) = our_addition { + push_deferred((addition, Some(theirs_idx)), our_changes); + } + if let Some(addition) = their_addition { + push_deferred((addition, Some(ours_idx)), their_changes); + } + } + ( + Change::Deletion { .. }, + Change::Rewrite { + source_location, + entry_mode: rewritten_mode, + id: rewritten_id, + location, + .. + }, + ) + | ( + Change::Rewrite { + source_location, + entry_mode: rewritten_mode, + id: rewritten_id, + location, + .. + }, + Change::Deletion { .. }, + ) if !rewritten_mode.is_commit() => { + let side = if matches!(ours, Change::Deletion { .. }) { + Original + } else { + Swapped + }; + + match tree_conflicts { + None | Some(ResolveWith::Ours) => { + editor.remove(toc(source_location))?; + pick_mut(side, our_tree, their_tree).remove_change(source_location.as_bstr()); + match side { + Original => ours_disposition = ChangeDisposition::Applied, + Swapped => theirs_disposition = ChangeDisposition::Applied, + } + } + Some(ResolveWith::Ancestor) => {} + } + + let their_rewritten_location = possibly_rewritten_location( + pick_mut(side, our_tree, their_tree), + location.as_ref(), + pick(side, our_changes, their_changes), + ) + .unwrap_or_else(|| location.to_owned()); + let our_addition = Change::Addition { + location: their_rewritten_location, + relation: None, + entry_mode: *rewritten_mode, + id: *rewritten_id, + }; + + if should_fail_on_conflict(Conflict::without_resolution( + ResolutionFailure::OursDeletedTheirsRenamed, + (ours, theirs, side, outer_side), + [ + None, + None, + index_entry_at_path( + rewritten_mode, + rewritten_id, + ConflictIndexEntryPathHint::RenamedOrTheirs, + ), + ], + )) { + break 'outer; + } + + let ours_is_rewrite = side.is_swapped(); + if tree_conflicts.is_none() + || (matches!(tree_conflicts, Some(ResolveWith::Ours)) && ours_is_rewrite) + { + push_deferred((our_addition, None), pick_mut(side, their_changes, our_changes)); + } + } + ( + Change::Rewrite { + source_location, + source_entry_mode, + source_id, + entry_mode: our_mode, + id: our_id, + location, + .. + }, + Change::Addition { + id: their_id, + entry_mode: their_mode, + location: add_location, + .. + }, + ) + | ( + Change::Addition { + id: their_id, + entry_mode: their_mode, + location: add_location, + .. + }, + Change::Rewrite { + source_location, + source_entry_mode, + source_id, + entry_mode: our_mode, + id: our_id, + location, + .. + }, + ) if !involves_submodule(our_mode, their_mode) => { + let side = if matches!(ours, Change::Rewrite { .. }) { + Original + } else { + Swapped + }; + if our_mode.is_tree() && add_location == source_location { + // The leaf changes already represent the directory rename and its replacement. + // Keep the replacement at the explicit source instead of relocating it with + // the inferred directory rename and reporting a second conflict. + match tree_conflicts { + None => { + editor.upsert(toc(add_location), their_mode.kind(), *their_id)?; + ours_disposition = ChangeDisposition::Applied; + theirs_disposition = ChangeDisposition::Applied; + } + Some(ResolveWith::Ours) => { + apply_our_resolution(ours, theirs, outer_side, &mut editor)?; + match outer_side { + Original => ours_disposition = ChangeDisposition::Applied, + Swapped => theirs_disposition = ChangeDisposition::Applied, + } + } + Some(ResolveWith::Ancestor) => {} + } + } else if let Some(merged_mode) = merge_modes(*our_mode, *their_mode) { + let (merged_blob_id, resolution) = if our_id == their_id { + (*our_id, None) + } else { + let (id, resolution) = perform_blob_merge( + labels, + objects, + blob_merge, + &mut diff_state.buf1, + &mut write_blob_to_odb, + (location, *our_id, *our_mode), + (location, *their_id, *their_mode), + (source_location, source_id.kind().null(), *source_entry_mode), + (0, outer_side), + &options, + )?; + (id, Some(resolution)) + }; + + editor.remove(toc(source_location))?; + pick_mut(side, our_tree, their_tree).remove_change(source_location.as_bstr()); + + if let Some(resolution) = resolution { + if should_fail_on_conflict(Conflict::with_resolution( + Resolution::OursModifiedTheirsModifiedThenBlobContentMerge { + merged_blob: ContentMerge { + resolution, + merged_blob_id, + }, + }, + (ours, theirs, Original, outer_side), + [None, index_entry(our_mode, our_id), index_entry(their_mode, their_id)], + )) { + break 'outer; + } + } + + // Because this constellation can only be found by the lookup tree, there is + // no need to put it as addition, we know it's not going to intersect on the other side. + editor.upsert(toc(location), merged_mode.kind(), merged_blob_id)?; + } else { + // We always remove the source from the tree - it might be re-added later. + let ours_is_rename = + tree_conflicts == Some(ResolveWith::Ours) && side == outer_side; + let remove_rename_source = + tree_conflicts.is_none() || ours_is_rename || add_location != source_location; + if remove_rename_source { + editor.remove(toc(source_location))?; + pick_mut(side, our_tree, their_tree).remove_change(source_location.as_bstr()); + } + + let ( + logical_side, + label_of_side_to_be_moved, + (our_mode, our_id, our_path_hint), + (their_mode, their_id, their_path_hint), + ) = if matches!(our_mode.kind(), EntryKind::Link | EntryKind::Tree) { + ( + Original, + labels.other.unwrap_or_default(), + (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), + (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), + ) + } else { + ( + Swapped, + labels.current.unwrap_or_default(), + (*their_mode, *their_id, ConflictIndexEntryPathHint::RenamedOrTheirs), + (*our_mode, *our_id, ConflictIndexEntryPathHint::Current), + ) + }; + let tree_with_rename = pick_mut(side, our_tree, their_tree); + let renamed_location = unique_path_in_tree( + location.as_bstr(), + &editor, + tree_with_rename, + label_of_side_to_be_moved, + )?; + + let upsert_rename_destination = tree_conflicts.is_none() || ours_is_rename; + if upsert_rename_destination { + editor.upsert(toc(location), our_mode.kind(), our_id)?; + tree_with_rename.remove_existing_change(location.as_bstr()); + } + + let conflict = Conflict::without_resolution( + ResolutionFailure::OursAddedTheirsAddedTypeMismatch { + their_unique_location: renamed_location.clone(), + }, + (ours, theirs, side, outer_side), + [ + None, + index_entry_at_path(&our_mode, &our_id, our_path_hint), + index_entry_at_path(&their_mode, &their_id, their_path_hint), + ], + ); + + if tree_conflicts.is_none() { + let new_change_with_rename = Change::Addition { + location: renamed_location, + entry_mode: their_mode, + id: their_id, + relation: None, + }; + push_deferred( + ( + new_change_with_rename, + Some(pick_idx(logical_side, theirs_idx, ours_idx)), + ), + pick_mut(logical_side, their_changes, our_changes), + ); + } + + if should_fail_on_conflict(conflict) { + break 'outer; + } + } + } + _unknown => { + if let Some(ResolveWith::Ours) = tree_conflicts { + apply_our_resolution(ours, theirs, outer_side, &mut editor)?; + } + if should_fail_on_conflict(Conflict::unknown((ours, theirs, Original, outer_side))) { + break 'outer; + } + } + } + their_changes[theirs_idx].mark(theirs_disposition); + our_changes[ours_idx].mark(ours_disposition); + } + } + } + segment_start = last_seen_len; + last_seen_len = their_changes.len(); + } + + ((our_changes, our_tree), (their_changes, their_tree)) = ((their_changes, their_tree), (our_changes, our_tree)); + (labels.current, labels.other) = (labels.other, labels.current); + outer_side = outer_side.swapped(); + } + + Ok(Outcome { + tree: editor, + conflicts, + failed_on_first_unresolved_conflict: failed_on_first_conflict, + }) +} + +fn apply_our_resolution( + local_ours: &Change, + local_theirs: &Change, + outer_side: ConflictMapping, + editor: &mut gix_object::tree::Editor<'_>, +) -> Result<(), Error> { + let ours = match outer_side { + Original => local_ours, + Swapped => local_theirs, + }; + Ok(apply_change(editor, ours, None)?) +} + +fn involves_submodule(a: &EntryMode, b: &EntryMode) -> bool { + a.is_commit() || b.is_commit() +} + +/// Allows equal modes or prefers executables bits in case of blobs +/// +/// Note that this is often not correct as the previous mode of each side should be taken into account so that: +/// +/// on | on = on +/// off | off = off +/// on | off || off | on = conflict +fn merge_modes(a: EntryMode, b: EntryMode) -> Option { + match (a.kind(), b.kind()) { + (_, _) if a == b => Some(a), + (EntryKind::BlobExecutable, EntryKind::BlobExecutable | EntryKind::Blob) + | (EntryKind::Blob, EntryKind::BlobExecutable) => Some(EntryKind::BlobExecutable.into()), + _ => None, + } +} + +/// Use this version if there is a single common `prev` value for both `a` and `b` to detect +/// if the mode was turned on or off. +fn merge_modes_prev(a: EntryMode, b: EntryMode, prev: EntryMode) -> Option { + match (a.kind(), b.kind()) { + (_, _) if a == b => Some(a), + (a @ EntryKind::BlobExecutable, b @ (EntryKind::BlobExecutable | EntryKind::Blob)) + | (a @ EntryKind::Blob, b @ EntryKind::BlobExecutable) => { + let prev = prev.kind(); + let changed = if a == prev { b } else { a }; + Some( + match (prev, changed) { + (EntryKind::Blob, EntryKind::BlobExecutable) => EntryKind::BlobExecutable, + (EntryKind::BlobExecutable, EntryKind::Blob) => EntryKind::Blob, + _ => unreachable!("upper match already assured we only deal with blobs"), + } + .into(), + ) + } + _ => None, + } +} + +fn push_deferred(change_and_idx: (Change, Option), changes: &mut ChangeList) { + push_deferred_with_rewrite(change_and_idx, None, changes); +} + +fn push_deferred_with_rewrite( + (change, ours_idx): (Change, Option), + new_location: Option<(BString, usize)>, + changes: &mut ChangeList, +) { + changes.push(TrackedChange::new(change, Some(ours_idx), new_location)); +} + +fn pick<'a, T: ?Sized>(side: ConflictMapping, ours: &'a T, theirs: &'a T) -> &'a T { + match side { + Original => ours, + Swapped => theirs, + } +} + +fn pick_idx(side: ConflictMapping, ours: usize, theirs: usize) -> usize { + match side { + Original => ours, + Swapped => theirs, + } +} + +fn pick_mut<'a, T: ?Sized>(side: ConflictMapping, ours: &'a mut T, theirs: &'a mut T) -> &'a mut T { + match side { + Original => ours, + Swapped => theirs, + } +} + +fn index_entry(mode: &gix_object::tree::EntryMode, id: &gix_hash::ObjectId) -> Option { + Some(ConflictIndexEntry { + mode: *mode, + id: *id, + path_hint: None, + }) +} + +fn index_entry_at_path( + mode: &gix_object::tree::EntryMode, + id: &gix_hash::ObjectId, + hint: ConflictIndexEntryPathHint, +) -> Option { + Some(ConflictIndexEntry { + mode: *mode, + id: *id, + path_hint: Some(hint), + }) +} From 9b91c76dc1ce131ff161859748ad701f7570fc94 Mon Sep 17 00:00:00 2001 From: Byron Date: Fri, 31 Jul 2026 17:30:12 +0200 Subject: [PATCH 4/4] Add Linux-sized tree merge benchmarks The earlier focused tree-merge benchmark reports about 500k synthetic cases per second, but it does not show how the merge scales with a production-sized tree or with thousands of changes spread throughout it. Add a deterministic in-memory fixture shaped after Linux commit 8ba098e6b6ff0db8edf28528d1552be261af30d4: 94,852 files in 6,202 trees, with 24 top-level directories and a maximum depth of 11. Fixture creation stays outside the measured loop so Criterion isolates gix_merge::tree() without requiring a large checked-in repository fixture. Measure both a single conflicting edit and a merge with 10,000 logical side changes. The large case spreads 4,500 modifications and 500 exact renames across each side and retains one conflict. Run both workloads with and without rename tracking, and validate their shape and conflict count before sampling them. Criterion throughput counts logical side changes here, whereas the earlier benchmark counted conceptual structural cases. A representative local run measured about 854k changes/s without rename tracking and 808k changes/s with it, so those figures are useful for this workload but are not identical units to the earlier 500k cases/s result. --- gix-merge/benches/tree.rs | 407 +++++++++++++++++++++++++++++++++++++- 1 file changed, 406 insertions(+), 1 deletion(-) diff --git a/gix-merge/benches/tree.rs b/gix-merge/benches/tree.rs index 3d2f403a0f1..8fd7789ffe4 100644 --- a/gix-merge/benches/tree.rs +++ b/gix-merge/benches/tree.rs @@ -473,5 +473,410 @@ fn new_blob_merge_platform() -> gix_merge::blob::Platform { ) } -criterion_group!(benches, tree_merge); +mod linux { + use std::{fmt::Write as _, hint::black_box, path::Path}; + + use criterion::{BatchSize, Criterion, Throughput}; + use gix_diff::Rewrites; + use gix_hash::ObjectId; + use gix_merge::tree::{Options, Outcome, TreatAsUnresolved}; + use gix_object::{ + FindExt, Kind, Tree, Write, + tree::{Editor, EntryKind}, + }; + use gix_worktree::stack::state::attributes; + + type ObjectDb = gix_odb::memory::Proxy; + + const ROOT_FILES: usize = 17; + const FILES: usize = 94_852; + const TREES: usize = 6_202; + const MAX_DEPTH: usize = 11; + const MODIFICATIONS_PER_SIDE: usize = 4_500; + const RENAMES_PER_SIDE: usize = 500; + const LARGE_SIDE_CHANGES: u64 = ((MODIFICATIONS_PER_SIDE + RENAMES_PER_SIDE) * 2) as u64; + const SPREAD_STEP: usize = 7_919; + + /// Shape of Linux commit 8ba098e6b6ff0db8edf28528d1552be261af30d4. + const LINUX_LAYOUT: &[Layout] = &[ + Layout::new("Documentation", 11_301, 736, 8), + Layout::new("LICENSES", 23, 5, 3), + Layout::new("arch", 18_521, 931, 7), + Layout::new("block", 103, 2, 3), + Layout::new("certs", 12, 1, 2), + Layout::new("crypto", 184, 4, 3), + Layout::new("drivers", 37_497, 2_466, 11), + Layout::new("fs", 2_369, 99, 5), + Layout::new("include", 6_675, 347, 6), + Layout::new("init", 17, 1, 2), + Layout::new("io_uring", 89, 1, 2), + Layout::new("ipc", 13, 1, 2), + Layout::new("kernel", 722, 46, 6), + Layout::new("lib", 905, 67, 5), + Layout::new("mm", 201, 7, 4), + Layout::new("net", 1_906, 88, 4), + Layout::new("rust", 585, 56, 5), + Layout::new("samples", 292, 49, 4), + Layout::new("scripts", 694, 75, 6), + Layout::new("security", 308, 24, 4), + Layout::new("sound", 2_981, 188, 6), + Layout::new("tools", 9_394, 1_000, 10), + Layout::new("usr", 23, 5, 4), + Layout::new("virt", 20, 3, 3), + ]; + + #[derive(Clone, Copy)] + struct Layout { + name: &'static str, + files: usize, + trees: usize, + depth: usize, + } + + impl Layout { + const fn new(name: &'static str, files: usize, trees: usize, depth: usize) -> Self { + Layout { + name, + files, + trees, + depth, + } + } + } + + struct File { + path: String, + id: ObjectId, + } + + #[derive(Clone, Copy)] + struct Scenario { + base: ObjectId, + ours: ObjectId, + theirs: ObjectId, + changes: u64, + } + + struct Fixture { + objects: ObjectDb, + small: Scenario, + large: Scenario, + } + + pub(super) fn tree_merge(c: &mut Criterion) { + let fixture = fixture(); + for (name, scenario) in [("small-conflict", fixture.small), ("large-change", fixture.large)] { + let mut group = c.benchmark_group(format!("tree-merge/linux-sized/{name}")); + group.throughput(Throughput::Elements(scenario.changes)); + for (name, rewrites) in [("without-renames", None), ("with-renames", Some(Rewrites::default()))] { + validate(&fixture.objects, scenario, rewrites); + let mut diff_state = gix_diff::tree::State::default(); + let mut diff_resource_cache = new_diff_resource_cache(); + let mut blob_merge = new_blob_merge_platform(); + group.bench_function(name, |b| { + b.iter_batched( + || options(rewrites), + |options| { + black_box(merge( + &fixture.objects, + scenario, + &mut diff_state, + &mut diff_resource_cache, + &mut blob_merge, + options, + )) + }, + BatchSize::SmallInput, + ); + }); + } + group.finish(); + } + } + + fn validate(objects: &ObjectDb, scenario: Scenario, rewrites: Option) { + let mut diff_state = gix_diff::tree::State::default(); + let mut diff_resource_cache = new_diff_resource_cache(); + let mut blob_merge = new_blob_merge_platform(); + let outcome = merge( + objects, + scenario, + &mut diff_state, + &mut diff_resource_cache, + &mut blob_merge, + options(rewrites), + ); + assert_eq!(outcome.conflicts.len(), 1, "the workload has exactly one conflict"); + assert!( + outcome.conflicts[0].is_unresolved(TreatAsUnresolved::git()), + "the conflicting text edit remains unresolved" + ); + } + + fn merge<'objects>( + objects: &'objects ObjectDb, + scenario: Scenario, + diff_state: &mut gix_diff::tree::State, + diff_resource_cache: &mut gix_diff::blob::Platform, + blob_merge: &mut gix_merge::blob::Platform, + options: Options, + ) -> Outcome<'objects> { + gix_merge::tree( + &scenario.base, + &scenario.ours, + &scenario.theirs, + gix_merge::blob::builtin_driver::text::Labels { + ancestor: Some("BASE".into()), + current: Some("OURS".into()), + other: Some("THEIRS".into()), + }, + objects, + |buf| objects.write_buf(Kind::Blob, buf), + diff_state, + diff_resource_cache, + blob_merge, + options, + ) + .expect("the synthetic tree merge succeeds") + } + + fn fixture() -> Fixture { + assert_eq!( + ROOT_FILES + LINUX_LAYOUT.iter().map(|layout| layout.files).sum::(), + FILES, + "the layout has the Linux file count" + ); + assert_eq!( + LINUX_LAYOUT.iter().map(|layout| layout.trees).sum::(), + TREES, + "the layout has the Linux tree count" + ); + assert_eq!( + LINUX_LAYOUT.iter().map(|layout| layout.depth).max(), + Some(MAX_DEPTH), + "the layout has the Linux maximum depth" + ); + + let objects = ObjectDb::new(gix_object::find::Never, gix_hash::Kind::Sha1); + let (base, files) = base_tree(&objects); + let conflict_idx = files + .iter() + .enumerate() + .max_by_key(|(_, file)| depth(&file.path)) + .map(|(idx, _)| idx) + .expect("the fixture contains files"); + let small = small_scenario(&objects, base, &files[conflict_idx]); + let large = large_scenario(&objects, base, &files, conflict_idx); + Fixture { objects, small, large } + } + + fn base_tree(objects: &ObjectDb) -> (ObjectId, Vec) { + let mut editor = Editor::new(Tree::default(), &gix_object::find::Never, gix_hash::Kind::Sha1); + let mut files = Vec::with_capacity(FILES); + for idx in 0..ROOT_FILES { + add_base_file(objects, &mut editor, &mut files, format!("root-{idx:02}.txt")); + } + for layout in LINUX_LAYOUT { + let directories = directories(*layout); + assert_eq!( + directories.len(), + layout.trees, + "{} has the requested tree count", + layout.name + ); + for idx in 0..layout.files { + let directory = &directories[idx % directories.len()]; + add_base_file(objects, &mut editor, &mut files, format!("{directory}/file-{idx:05}.c")); + } + } + assert_eq!(files.len(), FILES, "the generated fixture has the requested file count"); + assert_eq!( + files.iter().map(|file| depth(&file.path)).max(), + Some(MAX_DEPTH), + "the generated fixture has the requested maximum depth" + ); + let id = editor + .write(|tree| objects.write(tree)) + .expect("the base tree can be written"); + (id, files) + } + + fn directories(layout: Layout) -> Vec { + let mut directories = Vec::with_capacity(layout.trees); + directories.push(layout.name.to_owned()); + + let mut deepest = layout.name.to_owned(); + for level in 2..layout.depth { + write!(deepest, "/deep-{level:02}").expect("writing to a string succeeds"); + directories.push(deepest.clone()); + } + + while directories.len() < layout.trees { + let idx = directories.len(); + let mut parent = idx.wrapping_mul(37) % directories.len(); + while depth(&directories[parent]) >= layout.depth - 1 { + parent = (parent + 1) % directories.len(); + } + directories.push(format!("{}/dir-{idx:04}", directories[parent])); + } + directories + } + + fn add_base_file(objects: &ObjectDb, editor: &mut Editor<'_>, files: &mut Vec, path: String) { + let id = blob(objects, &path, "base"); + editor + .upsert(path.split('/'), EntryKind::Blob, id) + .expect("generated paths are valid"); + files.push(File { path, id }); + } + + fn small_scenario(objects: &ObjectDb, base: ObjectId, conflict: &File) -> Scenario { + let mut ours = editor(objects, base); + let mut theirs = editor(objects, base); + modify(objects, &mut ours, conflict, "ours"); + modify(objects, &mut theirs, conflict, "theirs"); + Scenario { + base, + ours: write_tree(objects, &mut ours), + theirs: write_tree(objects, &mut theirs), + changes: 2, + } + } + + fn large_scenario(objects: &ObjectDb, base: ObjectId, files: &[File], conflict_idx: usize) -> Scenario { + let mut ours = editor(objects, base); + let mut theirs = editor(objects, base); + modify(objects, &mut ours, &files[conflict_idx], "ours"); + modify(objects, &mut theirs, &files[conflict_idx], "theirs"); + + let mut indices = (0..files.len()) + .map(|idx| idx * SPREAD_STEP % files.len()) + .filter(|idx| *idx != conflict_idx); + for _ in 1..MODIFICATIONS_PER_SIDE { + modify( + objects, + &mut ours, + &files[indices.next().expect("enough files for our modifications")], + "ours", + ); + } + for _ in 1..MODIFICATIONS_PER_SIDE { + modify( + objects, + &mut theirs, + &files[indices.next().expect("enough files for their modifications")], + "theirs", + ); + } + for idx in 0..RENAMES_PER_SIDE { + rename( + &mut ours, + &files[indices.next().expect("enough files for our renames")], + &format!("moved/ours/batch-{:02}/file-{idx:04}.c", idx / 50), + ); + } + for idx in 0..RENAMES_PER_SIDE { + rename( + &mut theirs, + &files[indices.next().expect("enough files for their renames")], + &format!("moved/theirs/batch-{:02}/file-{idx:04}.c", idx / 50), + ); + } + + Scenario { + base, + ours: write_tree(objects, &mut ours), + theirs: write_tree(objects, &mut theirs), + changes: LARGE_SIDE_CHANGES, + } + } + + fn modify(objects: &ObjectDb, editor: &mut Editor<'_>, file: &File, side: &str) { + editor + .upsert(file.path.split('/'), EntryKind::Blob, blob(objects, &file.path, side)) + .expect("generated paths are valid"); + } + + fn rename(editor: &mut Editor<'_>, file: &File, destination: &str) { + editor + .remove(file.path.split('/')) + .expect("the rename source exists") + .upsert(destination.split('/'), EntryKind::Blob, file.id) + .expect("generated paths are valid"); + } + + fn blob(objects: &ObjectDb, path: &str, state: &str) -> ObjectId { + objects + .write_buf( + Kind::Blob, + format!("path: {path}\nstate: {state}\nstable line\n").as_bytes(), + ) + .expect("in-memory blob writes succeed") + } + + fn editor(objects: &ObjectDb, tree: ObjectId) -> Editor<'_> { + let mut buf = Vec::new(); + let root = objects + .find_tree(&tree, &mut buf) + .expect("the generated base tree exists") + .to_owned(); + Editor::new(root, objects, gix_hash::Kind::Sha1) + } + + fn write_tree(objects: &ObjectDb, editor: &mut Editor<'_>) -> ObjectId { + editor + .write(|tree| objects.write(tree)) + .expect("the side tree can be written") + } + + fn depth(path: &str) -> usize { + path.bytes().filter(|byte| *byte == b'/').count() + 1 + } + + fn options(rewrites: Option) -> Options { + Options { + rewrites, + ..Default::default() + } + } + + fn new_diff_resource_cache() -> gix_diff::blob::Platform { + gix_diff::blob::Platform::new( + Default::default(), + gix_diff::blob::Pipeline::new(Default::default(), Default::default(), Vec::new(), Default::default()), + Default::default(), + gix_worktree::Stack::new( + Path::new("gix-merge-benchmark-no-worktree"), + gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::default()), + Default::default(), + Vec::new(), + Vec::new(), + ), + ) + } + + fn new_blob_merge_platform() -> gix_merge::blob::Platform { + let attributes = gix_worktree::Stack::new( + Path::new("gix-merge-benchmark-no-worktree"), + gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::new( + Default::default(), + None, + attributes::Source::WorktreeThenIdMapping, + Default::default(), + )), + gix_worktree::glob::pattern::Case::Sensitive, + Vec::new(), + Vec::new(), + ); + gix_merge::blob::Platform::new( + gix_merge::blob::Pipeline::new(Default::default(), gix_filter::Pipeline::default(), Default::default()), + gix_merge::blob::pipeline::Mode::ToGit, + attributes, + vec![], + Default::default(), + ) + } +} + +criterion_group!(benches, tree_merge, linux::tree_merge); criterion_main!(benches);