Skip to main content

mkit_core/ops/
merge.rs

1//! 3-way tree merge + commit-graph merge-base + ancestry test.
2//!
3//! `merge_trees` lockstep-walks the three sorted entry arrays
4//! (base / ours / theirs) and applies the decision matrix documented
5//! above each branch in `merge_entries_recursive`. When all three sides
6//! have a path with `EntryMode::Tree`, we recurse into the three
7//! subtrees rather than emitting a tree-level conflict; this lets
8//! per-file conflicts inside a directory be reported at their full path
9//! (`src/main.rs` rather than `src`).
10//!
11//! `find_merge_base` walks the full DAG on both sides — it is NOT
12//! limited to first-parent — so merge bases reachable only through a
13//! merge commit's secondary parent are still found. Among multiple
14//! common ancestors we pick the one with the smallest total depth:
15//! (depth in A's ancestor tree) + (depth in B's BFS).
16
17use std::collections::HashMap;
18use std::collections::HashSet;
19
20use crate::hash::Hash;
21use crate::object::{EntryMode, Object, Tree, TreeEntry};
22use crate::serialize;
23use crate::store::{MAX_TREE_DEPTH, ObjectStore, StoreError};
24
25/// Distinct conflict kinds.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum ConflictKind {
28    /// Both sides modified the same path to different content.
29    ModifyModify,
30    /// One side deleted the path while the other modified it.
31    DeleteModify,
32    /// Both sides created the path with different content.
33    AddAdd,
34}
35
36/// Single conflict report. `base_hash`, `ours_hash`, `theirs_hash` are
37/// `None` whenever the corresponding side does not contain the path
38/// (Add/Add has no base; Delete/Modify has no hash on the deleting
39/// side).
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Conflict {
42    pub path: String,
43    pub kind: ConflictKind,
44    pub base_hash: Option<Hash>,
45    pub ours_hash: Option<Hash>,
46    pub theirs_hash: Option<Hash>,
47    /// Tree mode of the ours-side entry (`None` when ours deleted the
48    /// path). Carried so a downstream resolver can stage the ours-side
49    /// with its real exec/symlink mode instead of defaulting to a plain
50    /// blob (#214).
51    pub ours_mode: Option<EntryMode>,
52    /// Tree mode of the theirs-side entry (`None` when theirs deleted
53    /// the path).
54    pub theirs_mode: Option<EntryMode>,
55}
56
57/// Result of [`merge_trees`]. The `tree_hash` is always populated —
58/// even on conflict — using "ours wins in the merged tree" as the
59/// tie-breaker. Callers must check `has_conflicts()` before treating
60/// the result as a clean merge.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct MergeResult {
63    pub tree_hash: Hash,
64    pub conflicts: Vec<Conflict>,
65}
66
67impl MergeResult {
68    #[must_use]
69    pub fn has_conflicts(&self) -> bool {
70        !self.conflicts.is_empty()
71    }
72}
73
74/// 3-way merge of three trees identified by their hashes (or `None` to
75/// represent the empty tree). Always writes the merged tree to `store`
76/// and returns its hash, even when `conflicts` is non-empty (the merged
77/// tree contains "ours" at every conflicting path so a downstream
78/// resolver can see what we picked).
79///
80/// # Errors
81///
82/// Propagates [`StoreError`] from object reads and the final tree write.
83pub fn merge_trees(
84    store: &ObjectStore,
85    base_hash: Option<Hash>,
86    ours_hash: Option<Hash>,
87    theirs_hash: Option<Hash>,
88) -> Result<MergeResult, StoreError> {
89    let base_entries = load_entries(store, base_hash)?;
90    let ours_entries = load_entries(store, ours_hash)?;
91    let theirs_entries = load_entries(store, theirs_hash)?;
92
93    let mut merged: Vec<TreeEntry> = Vec::new();
94    let mut conflicts: Vec<Conflict> = Vec::new();
95    merge_entries_recursive(
96        store,
97        &base_entries,
98        &ours_entries,
99        &theirs_entries,
100        "",
101        &mut merged,
102        &mut conflicts,
103        0,
104    )?;
105
106    let tree_hash = put_tree(store, merged)?;
107    Ok(MergeResult {
108        tree_hash,
109        conflicts,
110    })
111}
112
113/// Find the lowest-cost common ancestor of two commits. Returns
114/// `Ok(None)` when the histories share no ancestor.
115///
116/// Definition of "lowest-cost": walk B breadth-first; for every node
117/// also reachable from A (taken from a precomputed ancestor-depth map
118/// of A), the cost is `depth_in_A + depth_in_B`; take the candidate
119/// with smallest total depth, breaking ties by first-seen order in B's
120/// BFS. This is NOT necessarily a unique LCA when histories cross
121/// multiple times, but it is the behaviour the tests pin.
122///
123/// # Errors
124///
125/// Propagates [`StoreError`] from any commit-object read other than
126/// `ObjectNotFound`, which is treated as "this branch terminates here".
127pub fn find_merge_base(store: &ObjectStore, a: Hash, b: Hash) -> Result<Option<Hash>, StoreError> {
128    if a == b {
129        return Ok(Some(a));
130    }
131    let ancestors_a = collect_ancestors_with_depth(store, a)?;
132
133    // BFS over B.
134    let mut queue: Vec<(Hash, usize)> = Vec::new();
135    queue.push((b, 0));
136
137    let mut best: Option<(Hash, usize)> = None;
138    let mut head = 0usize;
139    while head < queue.len() {
140        let (node, depth) = queue[head];
141        head += 1;
142        if let Some((_, best_total)) = best
143            && depth > best_total
144        {
145            break;
146        }
147        if let Some(&ancestor_depth) = ancestors_a.get(&node) {
148            let total = ancestor_depth + depth;
149            match best {
150                None => best = Some((node, total)),
151                Some((_, t)) if total < t => best = Some((node, total)),
152                _ => {}
153            }
154        }
155        match store.read_object(&node) {
156            Ok(Object::Commit(c)) => {
157                for &p in &c.parents {
158                    queue.push((p, depth + 1));
159                }
160            }
161            Ok(_) | Err(StoreError::ObjectNotFound(_)) => {}
162            Err(e) => return Err(e),
163        }
164    }
165    Ok(best.map(|(h, _)| h))
166}
167
168/// Returns `true` when `ancestor` is reachable from `descendant` by
169/// walking parent pointers. `ancestor == descendant` returns `true`.
170///
171/// # Errors
172///
173/// Propagates [`StoreError`] from commit-object reads (other than
174/// `ObjectNotFound`, which terminates the local walk silently).
175pub fn is_ancestor(
176    store: &ObjectStore,
177    ancestor: Hash,
178    descendant: Hash,
179) -> Result<bool, StoreError> {
180    if ancestor == descendant {
181        return Ok(true);
182    }
183    let mut seen: HashSet<Hash> = HashSet::new();
184    let mut stack: Vec<Hash> = Vec::new();
185    stack.push(descendant);
186
187    while let Some(current) = stack.pop() {
188        if !seen.insert(current) {
189            continue;
190        }
191        if current == ancestor {
192            return Ok(true);
193        }
194        match store.read_object(&current) {
195            Ok(Object::Commit(c)) => {
196                for &p in &c.parents {
197                    stack.push(p);
198                }
199            }
200            Ok(_) | Err(StoreError::ObjectNotFound(_)) => {}
201            Err(e) => return Err(e),
202        }
203    }
204    Ok(false)
205}
206
207// ---------------------------------------------------------------------
208// Internals
209// ---------------------------------------------------------------------
210
211fn load_entries(store: &ObjectStore, hash: Option<Hash>) -> Result<Vec<TreeEntry>, StoreError> {
212    match hash {
213        Some(h) => match store.read_object(&h)? {
214            Object::Tree(t) => Ok(t.entries),
215            other => Err(StoreError::Decode(
216                crate::object::MkitError::InvalidObjectType(other.object_type() as u8),
217            )),
218        },
219        None => Ok(Vec::new()),
220    }
221}
222
223fn put_tree(store: &ObjectStore, entries: Vec<TreeEntry>) -> Result<Hash, StoreError> {
224    let bytes = serialize::serialize(&Object::Tree(Tree { entries }))?;
225    store.write(&bytes)
226}
227
228/// The raw bytes of a single-object `Blob`, or `None` for a `ChunkedBlob`
229/// (large file) or any non-blob — those skip the line-level merge and fall
230/// back to a conflict. Blobs that `add`/`build_tree` produce are single
231/// `Blob` objects below the chunk threshold, so this covers the source
232/// files a 3-way text merge applies to.
233fn single_blob_bytes(store: &ObjectStore, h: Hash) -> Result<Option<Vec<u8>>, StoreError> {
234    match store.read_object(&h)? {
235        Object::Blob(b) => Ok(Some(b.data)),
236        _ => Ok(None),
237    }
238}
239
240/// Attempt a line-level 3-way merge of a both-sides-modified regular-file
241/// blob (#298). Returns the hash of the merged blob when ours/theirs
242/// changed disjoint regions of base, or `None` to fall back to a
243/// modify/modify conflict (overlapping changes, a binary side, or a
244/// chunked/large blob). Genuine store I/O errors propagate.
245fn try_text_merge(
246    store: &ObjectStore,
247    base_h: Hash,
248    ours_h: Hash,
249    theirs_h: Hash,
250) -> Result<Option<Hash>, StoreError> {
251    let (Some(base), Some(ours), Some(theirs)) = (
252        single_blob_bytes(store, base_h)?,
253        single_blob_bytes(store, ours_h)?,
254        single_blob_bytes(store, theirs_h)?,
255    ) else {
256        return Ok(None);
257    };
258    match crate::ops::merge_blob_3way(&base, &ours, &theirs) {
259        Some(merged) => {
260            // Store as a single Blob — same shape (and hash) `add` produces
261            // for sub-threshold content, so the merged object dedups.
262            let bytes = serialize::serialize(&Object::Blob(crate::object::Blob { data: merged }))?;
263            Ok(Some(store.write(&bytes)?))
264        }
265        None => Ok(None),
266    }
267}
268
269fn collect_ancestors_with_depth(
270    store: &ObjectStore,
271    start: Hash,
272) -> Result<HashMap<Hash, usize>, StoreError> {
273    let mut ancestors: HashMap<Hash, usize> = HashMap::new();
274    let mut queue: Vec<(Hash, usize)> = Vec::new();
275    queue.push((start, 0));
276
277    let mut head = 0usize;
278    while head < queue.len() {
279        let (node, depth) = queue[head];
280        head += 1;
281        // First-write-wins.
282        if ancestors.contains_key(&node) {
283            continue;
284        }
285        ancestors.insert(node, depth);
286        match store.read_object(&node) {
287            Ok(Object::Commit(c)) => {
288                for &p in &c.parents {
289                    queue.push((p, depth + 1));
290                }
291            }
292            Ok(_) | Err(StoreError::ObjectNotFound(_)) => {}
293            Err(e) => return Err(e),
294        }
295    }
296    Ok(ancestors)
297}
298
299fn hash_and_mode_eq(a: &TreeEntry, b: &TreeEntry) -> bool {
300    a.mode == b.mode && a.object_hash == b.object_hash
301}
302
303fn min_of_three<'a>(
304    a: Option<&'a [u8]>,
305    b: Option<&'a [u8]>,
306    c: Option<&'a [u8]>,
307) -> Option<&'a [u8]> {
308    let mut result: Option<&'a [u8]> = a;
309    if let Some(bv) = b {
310        result = match result {
311            Some(r) if r <= bv => Some(r),
312            _ => Some(bv),
313        };
314    }
315    if let Some(cv) = c {
316        result = match result {
317            Some(r) if r <= cv => Some(r),
318            _ => Some(cv),
319        };
320    }
321    result
322}
323
324fn join_path(prefix: &str, name: &[u8]) -> String {
325    let name_str = String::from_utf8_lossy(name);
326    if prefix.is_empty() {
327        name_str.into_owned()
328    } else {
329        let mut s = String::with_capacity(prefix.len() + 1 + name_str.len());
330        s.push_str(prefix);
331        s.push('/');
332        s.push_str(&name_str);
333        s
334    }
335}
336
337fn add_entry(out: &mut Vec<TreeEntry>, name: &[u8], mode: EntryMode, object_hash: Hash) {
338    out.push(TreeEntry {
339        name: name.to_vec(),
340        mode,
341        object_hash,
342    });
343}
344
345#[allow(clippy::too_many_arguments)]
346fn recurse_subtree_merge(
347    store: &ObjectStore,
348    base_sub: Option<Hash>,
349    ours_sub: Option<Hash>,
350    theirs_sub: Option<Hash>,
351    entry_name: &[u8],
352    prefix: &str,
353    merged: &mut Vec<TreeEntry>,
354    conflicts: &mut Vec<Conflict>,
355    depth: usize,
356) -> Result<(), StoreError> {
357    let sub_prefix = join_path(prefix, entry_name);
358    let base_entries = load_entries(store, base_sub)?;
359    let ours_entries = load_entries(store, ours_sub)?;
360    let theirs_entries = load_entries(store, theirs_sub)?;
361
362    let mut sub_merged: Vec<TreeEntry> = Vec::new();
363    merge_entries_recursive(
364        store,
365        &base_entries,
366        &ours_entries,
367        &theirs_entries,
368        &sub_prefix,
369        &mut sub_merged,
370        conflicts,
371        depth + 1,
372    )?;
373
374    let sub_hash = put_tree(store, sub_merged)?;
375    add_entry(merged, entry_name, EntryMode::Tree, sub_hash);
376    Ok(())
377}
378
379#[allow(clippy::too_many_lines)]
380fn merge_entries_recursive(
381    store: &ObjectStore,
382    base_entries: &[TreeEntry],
383    ours_entries: &[TreeEntry],
384    theirs_entries: &[TreeEntry],
385    prefix: &str,
386    merged: &mut Vec<TreeEntry>,
387    conflicts: &mut Vec<Conflict>,
388    depth: usize,
389) -> Result<(), StoreError> {
390    if depth > MAX_TREE_DEPTH {
391        return Err(StoreError::TreeTooDeep);
392    }
393    let mut bi = 0usize;
394    let mut oi = 0usize;
395    let mut ti = 0usize;
396
397    while bi < base_entries.len() || oi < ours_entries.len() || ti < theirs_entries.len() {
398        let b_name: Option<&[u8]> = base_entries.get(bi).map(|e| e.name.as_slice());
399        let o_name: Option<&[u8]> = ours_entries.get(oi).map(|e| e.name.as_slice());
400        let t_name: Option<&[u8]> = theirs_entries.get(ti).map(|e| e.name.as_slice());
401
402        let Some(min_name) = min_of_three(b_name, o_name, t_name) else {
403            break;
404        };
405
406        let has_base = b_name.is_some_and(|n| n == min_name);
407        let has_ours = o_name.is_some_and(|n| n == min_name);
408        let has_theirs = t_name.is_some_and(|n| n == min_name);
409
410        let base_entry = if has_base {
411            Some(&base_entries[bi])
412        } else {
413            None
414        };
415        let ours_entry = if has_ours {
416            Some(&ours_entries[oi])
417        } else {
418            None
419        };
420        let theirs_entry = if has_theirs {
421            Some(&theirs_entries[ti])
422        } else {
423            None
424        };
425
426        if has_base {
427            bi += 1;
428        }
429        if has_ours {
430            oi += 1;
431        }
432        if has_theirs {
433            ti += 1;
434        }
435
436        match (has_base, has_ours, has_theirs) {
437            (true, true, true) => {
438                let b = base_entry.unwrap();
439                let o = ours_entry.unwrap();
440                let t = theirs_entry.unwrap();
441                let b_eq_o = hash_and_mode_eq(b, o);
442                let b_eq_t = hash_and_mode_eq(b, t);
443                let o_eq_t = hash_and_mode_eq(o, t);
444                if b_eq_o && b_eq_t {
445                    add_entry(merged, min_name, b.mode, b.object_hash);
446                } else if b_eq_t && !b_eq_o {
447                    // Theirs unchanged, ours changed -> take ours.
448                    if b.mode == EntryMode::Tree && o.mode == EntryMode::Tree {
449                        recurse_subtree_merge(
450                            store,
451                            Some(b.object_hash),
452                            Some(o.object_hash),
453                            Some(b.object_hash),
454                            min_name,
455                            prefix,
456                            merged,
457                            conflicts,
458                            depth,
459                        )?;
460                    } else {
461                        add_entry(merged, min_name, o.mode, o.object_hash);
462                    }
463                } else if b_eq_o && !b_eq_t {
464                    // Ours unchanged, theirs changed -> take theirs.
465                    if b.mode == EntryMode::Tree && t.mode == EntryMode::Tree {
466                        recurse_subtree_merge(
467                            store,
468                            Some(b.object_hash),
469                            Some(b.object_hash),
470                            Some(t.object_hash),
471                            min_name,
472                            prefix,
473                            merged,
474                            conflicts,
475                            depth,
476                        )?;
477                    } else {
478                        add_entry(merged, min_name, t.mode, t.object_hash);
479                    }
480                } else if o_eq_t {
481                    // Both changed the same way.
482                    if b.mode == EntryMode::Tree && o.mode == EntryMode::Tree {
483                        recurse_subtree_merge(
484                            store,
485                            Some(b.object_hash),
486                            Some(o.object_hash),
487                            Some(t.object_hash),
488                            min_name,
489                            prefix,
490                            merged,
491                            conflicts,
492                            depth,
493                        )?;
494                    } else {
495                        add_entry(merged, min_name, o.mode, o.object_hash);
496                    }
497                } else if b.mode == EntryMode::Tree
498                    && o.mode == EntryMode::Tree
499                    && t.mode == EntryMode::Tree
500                {
501                    // Three-way subtree change — recurse to find per-file conflicts.
502                    recurse_subtree_merge(
503                        store,
504                        Some(b.object_hash),
505                        Some(o.object_hash),
506                        Some(t.object_hash),
507                        min_name,
508                        prefix,
509                        merged,
510                        conflicts,
511                        depth,
512                    )?;
513                } else if o.mode == t.mode
514                    && matches!(o.mode, EntryMode::Blob | EntryMode::Executable)
515                    && let Some(merged_hash) =
516                        try_text_merge(store, b.object_hash, o.object_hash, t.object_hash)?
517                {
518                    // Both changed the same regular file, but on disjoint
519                    // lines — auto-merge the content (#298). Mode is shared,
520                    // so it carries over unambiguously.
521                    add_entry(merged, min_name, o.mode, merged_hash);
522                } else {
523                    // Both changed differently and the content can't be
524                    // line-merged (overlap / binary / mode mismatch / chunked)
525                    // -> modify/modify conflict.
526                    conflicts.push(Conflict {
527                        path: join_path(prefix, min_name),
528                        kind: ConflictKind::ModifyModify,
529                        base_hash: Some(b.object_hash),
530                        ours_hash: Some(o.object_hash),
531                        theirs_hash: Some(t.object_hash),
532                        ours_mode: Some(o.mode),
533                        theirs_mode: Some(t.mode),
534                    });
535                    // Ours wins in the merged tree.
536                    add_entry(merged, min_name, o.mode, o.object_hash);
537                }
538            }
539            (false, true, false) => {
540                let o = ours_entry.unwrap();
541                add_entry(merged, min_name, o.mode, o.object_hash);
542            }
543            (false, false, true) => {
544                let t = theirs_entry.unwrap();
545                add_entry(merged, min_name, t.mode, t.object_hash);
546            }
547            (false, true, true) => {
548                let o = ours_entry.unwrap();
549                let t = theirs_entry.unwrap();
550                if hash_and_mode_eq(o, t) {
551                    add_entry(merged, min_name, o.mode, o.object_hash);
552                } else if o.mode == EntryMode::Tree && t.mode == EntryMode::Tree {
553                    recurse_subtree_merge(
554                        store,
555                        None,
556                        Some(o.object_hash),
557                        Some(t.object_hash),
558                        min_name,
559                        prefix,
560                        merged,
561                        conflicts,
562                        depth,
563                    )?;
564                } else {
565                    conflicts.push(Conflict {
566                        path: join_path(prefix, min_name),
567                        kind: ConflictKind::AddAdd,
568                        base_hash: None,
569                        ours_hash: Some(o.object_hash),
570                        theirs_hash: Some(t.object_hash),
571                        ours_mode: Some(o.mode),
572                        theirs_mode: Some(t.mode),
573                    });
574                    add_entry(merged, min_name, o.mode, o.object_hash);
575                }
576            }
577            (true, true, false) => {
578                let b = base_entry.unwrap();
579                let o = ours_entry.unwrap();
580                if hash_and_mode_eq(b, o) {
581                    // Ours unchanged, theirs deleted -> delete.
582                } else {
583                    conflicts.push(Conflict {
584                        path: join_path(prefix, min_name),
585                        kind: ConflictKind::DeleteModify,
586                        base_hash: Some(b.object_hash),
587                        ours_hash: Some(o.object_hash),
588                        theirs_hash: None,
589                        ours_mode: Some(o.mode),
590                        theirs_mode: None,
591                    });
592                    add_entry(merged, min_name, o.mode, o.object_hash);
593                }
594            }
595            (true, false, true) => {
596                let b = base_entry.unwrap();
597                let t = theirs_entry.unwrap();
598                if hash_and_mode_eq(b, t) {
599                    // Theirs unchanged, ours deleted -> delete.
600                } else {
601                    conflicts.push(Conflict {
602                        path: join_path(prefix, min_name),
603                        kind: ConflictKind::DeleteModify,
604                        base_hash: Some(b.object_hash),
605                        ours_hash: None,
606                        theirs_hash: Some(t.object_hash),
607                        ours_mode: None,
608                        theirs_mode: Some(t.mode),
609                    });
610                    add_entry(merged, min_name, t.mode, t.object_hash);
611                }
612            }
613            (true, false, false) => {
614                // Both deleted -> delete.
615            }
616            (false, false, false) => {
617                // unreachable — `min_of_three` returns None in this case
618                // and we'd have broken out of the loop above.
619                break;
620            }
621        }
622    }
623    Ok(())
624}
625
626// =====================================================================
627// Tests
628// =====================================================================
629
630#[cfg(test)]
631#[allow(clippy::many_single_char_names)] // single-letter commit names keep the test tables compact
632mod tests {
633    use super::*;
634    use crate::object::{Blob, Commit, EntryMode, Identity, Object, Tree, TreeEntry};
635    use crate::serialize;
636    use tempfile::TempDir;
637
638    fn store() -> (TempDir, ObjectStore) {
639        let d = TempDir::new().unwrap();
640        let s = ObjectStore::init(d.path()).unwrap();
641        (d, s)
642    }
643    fn put_blob(s: &ObjectStore, data: &[u8]) -> Hash {
644        let bytes = serialize::serialize(&Object::Blob(Blob {
645            data: data.to_vec(),
646        }))
647        .unwrap();
648        s.write(&bytes).unwrap()
649    }
650    fn make_tree(s: &ObjectStore, entries: Vec<TreeEntry>) -> Hash {
651        let bytes = serialize::serialize(&Object::Tree(Tree { entries })).unwrap();
652        s.write(&bytes).unwrap()
653    }
654    fn entry(name: &[u8], mode: EntryMode, h: Hash) -> TreeEntry {
655        TreeEntry {
656            name: name.to_vec(),
657            mode,
658            object_hash: h,
659        }
660    }
661    fn make_commit(s: &ObjectStore, tree: Hash, parents: &[Hash], message: &str) -> Hash {
662        let c = Commit {
663            tree_hash: tree,
664            parents: parents.to_vec(),
665            author: Identity::ed25519([0; 32]),
666            signer: [0; 32],
667            message: message.as_bytes().to_vec(),
668            timestamp: message.len() as u64,
669            message_hash: [0; 32],
670            content_digest: [0; 32],
671            signature: [0; 64],
672        };
673        let bytes = serialize::serialize(&Object::Commit(c)).unwrap();
674        s.write(&bytes).unwrap()
675    }
676    fn tree_entries(s: &ObjectStore, h: Hash) -> Vec<TreeEntry> {
677        match s.read_object(&h).unwrap() {
678            Object::Tree(t) => t.entries,
679            other => panic!("expected tree, got {other}"),
680        }
681    }
682    fn find_entry<'a>(entries: &'a [TreeEntry], name: &[u8]) -> Option<&'a TreeEntry> {
683        entries.iter().find(|e| e.name == name)
684    }
685
686    #[test]
687    fn merge_identical_trees() {
688        let (_d, s) = store();
689        let blob_a = put_blob(&s, b"aaa");
690        let tree = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, blob_a)]);
691        let r = merge_trees(&s, Some(tree), Some(tree), Some(tree)).unwrap();
692        assert!(!r.has_conflicts());
693        assert_eq!(r.tree_hash, tree);
694    }
695
696    #[test]
697    fn merge_ours_adds_file() {
698        let (_d, s) = store();
699        let a = put_blob(&s, b"aaa");
700        let b = put_blob(&s, b"bbb");
701        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
702        let ours = make_tree(
703            &s,
704            vec![
705                entry(b"a.txt", EntryMode::Blob, a),
706                entry(b"b.txt", EntryMode::Blob, b),
707            ],
708        );
709        let r = merge_trees(&s, Some(base), Some(ours), Some(base)).unwrap();
710        assert!(!r.has_conflicts());
711        let entries = tree_entries(&s, r.tree_hash);
712        assert_eq!(entries.len(), 2);
713        assert_eq!(find_entry(&entries, b"a.txt").unwrap().object_hash, a);
714        assert_eq!(find_entry(&entries, b"b.txt").unwrap().object_hash, b);
715    }
716
717    #[test]
718    fn merge_theirs_adds_file() {
719        let (_d, s) = store();
720        let a = put_blob(&s, b"aaa");
721        let c = put_blob(&s, b"ccc");
722        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
723        let theirs = make_tree(
724            &s,
725            vec![
726                entry(b"a.txt", EntryMode::Blob, a),
727                entry(b"c.txt", EntryMode::Blob, c),
728            ],
729        );
730        let r = merge_trees(&s, Some(base), Some(base), Some(theirs)).unwrap();
731        assert!(!r.has_conflicts());
732        let entries = tree_entries(&s, r.tree_hash);
733        assert_eq!(entries.len(), 2);
734        assert_eq!(find_entry(&entries, b"c.txt").unwrap().object_hash, c);
735    }
736
737    #[test]
738    fn merge_both_add_different_files() {
739        let (_d, s) = store();
740        let a = put_blob(&s, b"aaa");
741        let b = put_blob(&s, b"bbb");
742        let c = put_blob(&s, b"ccc");
743        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
744        let ours = make_tree(
745            &s,
746            vec![
747                entry(b"a.txt", EntryMode::Blob, a),
748                entry(b"b.txt", EntryMode::Blob, b),
749            ],
750        );
751        let theirs = make_tree(
752            &s,
753            vec![
754                entry(b"a.txt", EntryMode::Blob, a),
755                entry(b"c.txt", EntryMode::Blob, c),
756            ],
757        );
758        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
759        assert!(!r.has_conflicts());
760        assert_eq!(tree_entries(&s, r.tree_hash).len(), 3);
761    }
762
763    #[test]
764    fn merge_both_add_same_file_same_content() {
765        let (_d, s) = store();
766        let a = put_blob(&s, b"aaa");
767        let b = put_blob(&s, b"bbb");
768        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
769        let twin = make_tree(
770            &s,
771            vec![
772                entry(b"a.txt", EntryMode::Blob, a),
773                entry(b"b.txt", EntryMode::Blob, b),
774            ],
775        );
776        let r = merge_trees(&s, Some(base), Some(twin), Some(twin)).unwrap();
777        assert!(!r.has_conflicts());
778        assert_eq!(
779            find_entry(&tree_entries(&s, r.tree_hash), b"b.txt")
780                .unwrap()
781                .object_hash,
782            b
783        );
784    }
785
786    #[test]
787    fn merge_both_add_same_file_different_content() {
788        let (_d, s) = store();
789        let a = put_blob(&s, b"aaa");
790        let b1 = put_blob(&s, b"bbb-ours");
791        let b2 = put_blob(&s, b"bbb-theirs");
792        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
793        let ours = make_tree(
794            &s,
795            vec![
796                entry(b"a.txt", EntryMode::Blob, a),
797                entry(b"b.txt", EntryMode::Blob, b1),
798            ],
799        );
800        let theirs = make_tree(
801            &s,
802            vec![
803                entry(b"a.txt", EntryMode::Blob, a),
804                entry(b"b.txt", EntryMode::Blob, b2),
805            ],
806        );
807        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
808        assert!(r.has_conflicts());
809        assert_eq!(r.conflicts.len(), 1);
810        let c = &r.conflicts[0];
811        assert_eq!(c.path, "b.txt");
812        assert_eq!(c.kind, ConflictKind::AddAdd);
813        assert_eq!(c.base_hash, None);
814        assert_eq!(c.ours_hash, Some(b1));
815        assert_eq!(c.theirs_hash, Some(b2));
816    }
817
818    #[test]
819    fn merge_ours_modifies() {
820        let (_d, s) = store();
821        let v1 = put_blob(&s, b"v1");
822        let v2 = put_blob(&s, b"v2");
823        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v1)]);
824        let ours = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v2)]);
825        let r = merge_trees(&s, Some(base), Some(ours), Some(base)).unwrap();
826        assert!(!r.has_conflicts());
827        assert_eq!(
828            find_entry(&tree_entries(&s, r.tree_hash), b"a.txt")
829                .unwrap()
830                .object_hash,
831            v2
832        );
833    }
834
835    #[test]
836    fn merge_theirs_modifies() {
837        let (_d, s) = store();
838        let v1 = put_blob(&s, b"v1");
839        let v2 = put_blob(&s, b"v2");
840        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v1)]);
841        let theirs = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v2)]);
842        let r = merge_trees(&s, Some(base), Some(base), Some(theirs)).unwrap();
843        assert!(!r.has_conflicts());
844        assert_eq!(
845            find_entry(&tree_entries(&s, r.tree_hash), b"a.txt")
846                .unwrap()
847                .object_hash,
848            v2
849        );
850    }
851
852    #[test]
853    fn merge_both_modify_same_way() {
854        let (_d, s) = store();
855        let v1 = put_blob(&s, b"v1");
856        let v2 = put_blob(&s, b"v2");
857        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v1)]);
858        let modified = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v2)]);
859        let r = merge_trees(&s, Some(base), Some(modified), Some(modified)).unwrap();
860        assert!(!r.has_conflicts());
861    }
862
863    #[test]
864    fn merge_both_modify_differently() {
865        let (_d, s) = store();
866        let v1 = put_blob(&s, b"v1");
867        let v2 = put_blob(&s, b"v2-ours");
868        let v3 = put_blob(&s, b"v3-theirs");
869        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v1)]);
870        let ours = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v2)]);
871        let theirs = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v3)]);
872        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
873        assert!(r.has_conflicts());
874        let c = &r.conflicts[0];
875        assert_eq!(c.path, "a.txt");
876        assert_eq!(c.kind, ConflictKind::ModifyModify);
877        assert_eq!(c.base_hash, Some(v1));
878        assert_eq!(c.ours_hash, Some(v2));
879        assert_eq!(c.theirs_hash, Some(v3));
880    }
881
882    #[test]
883    fn merge_ours_deletes() {
884        let (_d, s) = store();
885        let a = put_blob(&s, b"aaa");
886        let b = put_blob(&s, b"bbb");
887        let base = make_tree(
888            &s,
889            vec![
890                entry(b"a.txt", EntryMode::Blob, a),
891                entry(b"b.txt", EntryMode::Blob, b),
892            ],
893        );
894        let ours = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
895        let r = merge_trees(&s, Some(base), Some(ours), Some(base)).unwrap();
896        assert!(!r.has_conflicts());
897        assert_eq!(tree_entries(&s, r.tree_hash).len(), 1);
898    }
899
900    #[test]
901    fn merge_theirs_deletes() {
902        let (_d, s) = store();
903        let a = put_blob(&s, b"aaa");
904        let b = put_blob(&s, b"bbb");
905        let base = make_tree(
906            &s,
907            vec![
908                entry(b"a.txt", EntryMode::Blob, a),
909                entry(b"b.txt", EntryMode::Blob, b),
910            ],
911        );
912        let theirs = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
913        let r = merge_trees(&s, Some(base), Some(base), Some(theirs)).unwrap();
914        assert!(!r.has_conflicts());
915        assert_eq!(tree_entries(&s, r.tree_hash).len(), 1);
916    }
917
918    #[test]
919    fn merge_both_delete() {
920        let (_d, s) = store();
921        let a = put_blob(&s, b"aaa");
922        let b = put_blob(&s, b"bbb");
923        let base = make_tree(
924            &s,
925            vec![
926                entry(b"a.txt", EntryMode::Blob, a),
927                entry(b"b.txt", EntryMode::Blob, b),
928            ],
929        );
930        let both = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
931        let r = merge_trees(&s, Some(base), Some(both), Some(both)).unwrap();
932        assert!(!r.has_conflicts());
933        assert_eq!(tree_entries(&s, r.tree_hash).len(), 1);
934    }
935
936    #[test]
937    fn merge_delete_vs_modify() {
938        let (_d, s) = store();
939        let a = put_blob(&s, b"aaa");
940        let b1 = put_blob(&s, b"bbb-v1");
941        let b2 = put_blob(&s, b"bbb-v2");
942        let base = make_tree(
943            &s,
944            vec![
945                entry(b"a.txt", EntryMode::Blob, a),
946                entry(b"b.txt", EntryMode::Blob, b1),
947            ],
948        );
949        let ours = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
950        let theirs = make_tree(
951            &s,
952            vec![
953                entry(b"a.txt", EntryMode::Blob, a),
954                entry(b"b.txt", EntryMode::Blob, b2),
955            ],
956        );
957        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
958        assert!(r.has_conflicts());
959        let c = &r.conflicts[0];
960        assert_eq!(c.path, "b.txt");
961        assert_eq!(c.kind, ConflictKind::DeleteModify);
962        assert_eq!(c.base_hash, Some(b1));
963        assert_eq!(c.ours_hash, None);
964        assert_eq!(c.theirs_hash, Some(b2));
965    }
966
967    #[test]
968    fn merge_nested_tree_changes() {
969        let (_d, s) = store();
970        let main_v1 = put_blob(&s, b"fn main() {}");
971        let main_v2 = put_blob(&s, b"fn main() { run(); }");
972        let util = put_blob(&s, b"fn util() {}");
973        let base_sub = make_tree(&s, vec![entry(b"main.rs", EntryMode::Blob, main_v1)]);
974        let base = make_tree(&s, vec![entry(b"src", EntryMode::Tree, base_sub)]);
975        let ours_sub = make_tree(&s, vec![entry(b"main.rs", EntryMode::Blob, main_v2)]);
976        let ours = make_tree(&s, vec![entry(b"src", EntryMode::Tree, ours_sub)]);
977        let theirs_sub = make_tree(
978            &s,
979            vec![
980                entry(b"main.rs", EntryMode::Blob, main_v1),
981                entry(b"util.rs", EntryMode::Blob, util),
982            ],
983        );
984        let theirs = make_tree(&s, vec![entry(b"src", EntryMode::Tree, theirs_sub)]);
985        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
986        assert!(!r.has_conflicts());
987        let root = tree_entries(&s, r.tree_hash);
988        assert_eq!(root.len(), 1);
989        let src = tree_entries(&s, root[0].object_hash);
990        assert_eq!(src.len(), 2);
991        assert_eq!(find_entry(&src, b"main.rs").unwrap().object_hash, main_v2);
992        assert_eq!(find_entry(&src, b"util.rs").unwrap().object_hash, util);
993    }
994
995    #[test]
996    fn merge_nested_conflict_path_is_full() {
997        let (_d, s) = store();
998        let v1 = put_blob(&s, b"original");
999        let v2 = put_blob(&s, b"ours-change");
1000        let v3 = put_blob(&s, b"theirs-change");
1001        let base_sub = make_tree(&s, vec![entry(b"main.rs", EntryMode::Blob, v1)]);
1002        let base = make_tree(&s, vec![entry(b"src", EntryMode::Tree, base_sub)]);
1003        let ours_sub = make_tree(&s, vec![entry(b"main.rs", EntryMode::Blob, v2)]);
1004        let ours = make_tree(&s, vec![entry(b"src", EntryMode::Tree, ours_sub)]);
1005        let theirs_sub = make_tree(&s, vec![entry(b"main.rs", EntryMode::Blob, v3)]);
1006        let theirs = make_tree(&s, vec![entry(b"src", EntryMode::Tree, theirs_sub)]);
1007        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
1008        assert!(r.has_conflicts());
1009        assert_eq!(r.conflicts[0].path, "src/main.rs");
1010        assert_eq!(r.conflicts[0].kind, ConflictKind::ModifyModify);
1011        assert_eq!(r.conflicts[0].base_hash, Some(v1));
1012        assert_eq!(r.conflicts[0].ours_hash, Some(v2));
1013        assert_eq!(r.conflicts[0].theirs_hash, Some(v3));
1014    }
1015
1016    #[test]
1017    fn merge_empty_base() {
1018        let (_d, s) = store();
1019        let a = put_blob(&s, b"aaa");
1020        let b = put_blob(&s, b"bbb");
1021        let ours = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
1022        let theirs = make_tree(&s, vec![entry(b"b.txt", EntryMode::Blob, b)]);
1023        let r = merge_trees(&s, None, Some(ours), Some(theirs)).unwrap();
1024        assert!(!r.has_conflicts());
1025        assert_eq!(tree_entries(&s, r.tree_hash).len(), 2);
1026    }
1027
1028    // ---- find_merge_base ----
1029
1030    #[test]
1031    fn find_merge_base_linear() {
1032        let (_d, s) = store();
1033        let empty = make_tree(&s, vec![]);
1034        let a = make_commit(&s, empty, &[], "A");
1035        let b = make_commit(&s, empty, &[a], "B");
1036        let c = make_commit(&s, empty, &[b], "C");
1037        let d = make_commit(&s, empty, &[b], "D");
1038        assert_eq!(find_merge_base(&s, c, d).unwrap(), Some(b));
1039    }
1040
1041    #[test]
1042    fn find_merge_base_root() {
1043        let (_d, s) = store();
1044        let empty = make_tree(&s, vec![]);
1045        let a = make_commit(&s, empty, &[], "A");
1046        let b = make_commit(&s, empty, &[a], "B");
1047        let c = make_commit(&s, empty, &[a], "C");
1048        assert_eq!(find_merge_base(&s, b, c).unwrap(), Some(a));
1049    }
1050
1051    #[test]
1052    fn find_merge_base_no_common_ancestor() {
1053        let (_d, s) = store();
1054        let empty = make_tree(&s, vec![]);
1055        let x = make_commit(&s, empty, &[], "X");
1056        let y = make_commit(&s, empty, &[], "Y");
1057        assert_eq!(find_merge_base(&s, x, y).unwrap(), None);
1058    }
1059
1060    #[test]
1061    fn find_merge_base_same_commit() {
1062        let (_d, s) = store();
1063        let empty = make_tree(&s, vec![]);
1064        let a = make_commit(&s, empty, &[], "A");
1065        assert_eq!(find_merge_base(&s, a, a).unwrap(), Some(a));
1066    }
1067
1068    #[test]
1069    fn find_merge_base_walks_non_first_parents() {
1070        let (_d, s) = store();
1071        let empty = make_tree(&s, vec![]);
1072        let root = make_commit(&s, empty, &[], "root");
1073        let left = make_commit(&s, empty, &[root], "left");
1074        let right = make_commit(&s, empty, &[root], "right");
1075        let m = make_commit(&s, empty, &[left, right], "merge");
1076        let tip = make_commit(&s, empty, &[m], "tip");
1077        assert_eq!(find_merge_base(&s, tip, right).unwrap(), Some(right));
1078    }
1079
1080    #[test]
1081    fn is_ancestor_walks_merge_parents() {
1082        let (_d, s) = store();
1083        let empty = make_tree(&s, vec![]);
1084        let root = make_commit(&s, empty, &[], "root");
1085        let left = make_commit(&s, empty, &[root], "left");
1086        let right = make_commit(&s, empty, &[root], "right");
1087        let m = make_commit(&s, empty, &[left, right], "merge");
1088
1089        assert!(is_ancestor(&s, root, m).unwrap());
1090        assert!(is_ancestor(&s, right, m).unwrap());
1091        assert!(!is_ancestor(&s, m, right).unwrap());
1092    }
1093
1094    #[test]
1095    fn merge_both_modify_disjoint_lines_auto_merges() {
1096        let (_d, s) = store();
1097        let base_b = put_blob(&s, b"a\nb\nc\nd\ne\n");
1098        let ours_b = put_blob(&s, b"A\nb\nc\nd\ne\n"); // line 1
1099        let theirs_b = put_blob(&s, b"a\nb\nc\nd\nE\n"); // line 5
1100        let base = make_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, base_b)]);
1101        let ours = make_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, ours_b)]);
1102        let theirs = make_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, theirs_b)]);
1103        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
1104        assert!(
1105            !r.has_conflicts(),
1106            "disjoint line edits should auto-merge (#298), got {:?}",
1107            r.conflicts
1108        );
1109        let f = find_entry(&tree_entries(&s, r.tree_hash), b"f.txt")
1110            .unwrap()
1111            .object_hash;
1112        let merged = match s.read_object(&f).unwrap() {
1113            Object::Blob(b) => b.data,
1114            other => panic!("merged f.txt not a blob: {other:?}"),
1115        };
1116        assert_eq!(merged, b"A\nb\nc\nd\nE\n");
1117    }
1118
1119    #[test]
1120    fn merge_both_modify_same_line_still_conflicts() {
1121        let (_d, s) = store();
1122        let base_b = put_blob(&s, b"a\nb\nc\n");
1123        let ours_b = put_blob(&s, b"a\nOURS\nc\n"); // line 2
1124        let theirs_b = put_blob(&s, b"a\nTHEIRS\nc\n"); // line 2 — overlaps
1125        let base = make_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, base_b)]);
1126        let ours = make_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, ours_b)]);
1127        let theirs = make_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, theirs_b)]);
1128        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
1129        assert!(r.has_conflicts(), "same-line edits must still conflict");
1130        assert_eq!(r.conflicts[0].kind, ConflictKind::ModifyModify);
1131    }
1132}