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 on the captured entries themselves rather than the parallel
437        // booleans: the `Some`/`None` shape carries the same information and
438        // binds the references directly, so there is nothing to `unwrap`.
439        match (base_entry, ours_entry, theirs_entry) {
440            (Some(b), Some(o), Some(t)) => {
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            (None, Some(o), None) => {
540                add_entry(merged, min_name, o.mode, o.object_hash);
541            }
542            (None, None, Some(t)) => {
543                add_entry(merged, min_name, t.mode, t.object_hash);
544            }
545            (None, Some(o), Some(t)) => {
546                if hash_and_mode_eq(o, t) {
547                    add_entry(merged, min_name, o.mode, o.object_hash);
548                } else if o.mode == EntryMode::Tree && t.mode == EntryMode::Tree {
549                    recurse_subtree_merge(
550                        store,
551                        None,
552                        Some(o.object_hash),
553                        Some(t.object_hash),
554                        min_name,
555                        prefix,
556                        merged,
557                        conflicts,
558                        depth,
559                    )?;
560                } else {
561                    conflicts.push(Conflict {
562                        path: join_path(prefix, min_name),
563                        kind: ConflictKind::AddAdd,
564                        base_hash: None,
565                        ours_hash: Some(o.object_hash),
566                        theirs_hash: Some(t.object_hash),
567                        ours_mode: Some(o.mode),
568                        theirs_mode: Some(t.mode),
569                    });
570                    add_entry(merged, min_name, o.mode, o.object_hash);
571                }
572            }
573            (Some(b), Some(o), None) => {
574                if hash_and_mode_eq(b, o) {
575                    // Ours unchanged, theirs deleted -> delete.
576                } else {
577                    conflicts.push(Conflict {
578                        path: join_path(prefix, min_name),
579                        kind: ConflictKind::DeleteModify,
580                        base_hash: Some(b.object_hash),
581                        ours_hash: Some(o.object_hash),
582                        theirs_hash: None,
583                        ours_mode: Some(o.mode),
584                        theirs_mode: None,
585                    });
586                    add_entry(merged, min_name, o.mode, o.object_hash);
587                }
588            }
589            (Some(b), None, Some(t)) => {
590                if hash_and_mode_eq(b, t) {
591                    // Theirs unchanged, ours deleted -> delete.
592                } else {
593                    conflicts.push(Conflict {
594                        path: join_path(prefix, min_name),
595                        kind: ConflictKind::DeleteModify,
596                        base_hash: Some(b.object_hash),
597                        ours_hash: None,
598                        theirs_hash: Some(t.object_hash),
599                        ours_mode: None,
600                        theirs_mode: Some(t.mode),
601                    });
602                    add_entry(merged, min_name, t.mode, t.object_hash);
603                }
604            }
605            (Some(_b), None, None) => {
606                // Both deleted -> delete.
607            }
608            (None, None, None) => {
609                // unreachable — `min_of_three` returns None in this case
610                // and we'd have broken out of the loop above.
611                break;
612            }
613        }
614    }
615    Ok(())
616}
617
618// =====================================================================
619// Tests
620// =====================================================================
621
622#[cfg(test)]
623#[allow(clippy::many_single_char_names)] // single-letter commit names keep the test tables compact
624mod tests {
625    use super::*;
626    use crate::object::{Blob, Commit, EntryMode, Identity, Object, Tree, TreeEntry};
627    use crate::serialize;
628    use tempfile::TempDir;
629
630    fn store() -> (TempDir, ObjectStore) {
631        let d = TempDir::new().unwrap();
632        let s = ObjectStore::init(&crate::layout::RepoLayout::single(d.path())).unwrap();
633        (d, s)
634    }
635    fn put_blob(s: &ObjectStore, data: &[u8]) -> Hash {
636        let bytes = serialize::serialize(&Object::Blob(Blob {
637            data: data.to_vec(),
638        }))
639        .unwrap();
640        s.write(&bytes).unwrap()
641    }
642    fn make_tree(s: &ObjectStore, entries: Vec<TreeEntry>) -> Hash {
643        let bytes = serialize::serialize(&Object::Tree(Tree { entries })).unwrap();
644        s.write(&bytes).unwrap()
645    }
646    fn entry(name: &[u8], mode: EntryMode, h: Hash) -> TreeEntry {
647        TreeEntry {
648            name: name.to_vec(),
649            mode,
650            object_hash: h,
651        }
652    }
653    fn make_commit(s: &ObjectStore, tree: Hash, parents: &[Hash], message: &str) -> Hash {
654        let c = Commit {
655            tree_hash: tree,
656            parents: parents.to_vec(),
657            author: Identity::ed25519([0; 32]),
658            signer: [0; 32],
659            message: message.as_bytes().to_vec(),
660            timestamp: message.len() as u64,
661            message_hash: [0; 32],
662            content_digest: [0; 32],
663            signature: [0; 64],
664        };
665        let bytes = serialize::serialize(&Object::Commit(c)).unwrap();
666        s.write(&bytes).unwrap()
667    }
668    fn tree_entries(s: &ObjectStore, h: Hash) -> Vec<TreeEntry> {
669        match s.read_object(&h).unwrap() {
670            Object::Tree(t) => t.entries,
671            other => panic!("expected tree, got {other}"),
672        }
673    }
674    fn find_entry<'a>(entries: &'a [TreeEntry], name: &[u8]) -> Option<&'a TreeEntry> {
675        entries.iter().find(|e| e.name == name)
676    }
677
678    #[test]
679    fn merge_identical_trees() {
680        let (_d, s) = store();
681        let blob_a = put_blob(&s, b"aaa");
682        let tree = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, blob_a)]);
683        let r = merge_trees(&s, Some(tree), Some(tree), Some(tree)).unwrap();
684        assert!(!r.has_conflicts());
685        assert_eq!(r.tree_hash, tree);
686    }
687
688    #[test]
689    fn merge_ours_adds_file() {
690        let (_d, s) = store();
691        let a = put_blob(&s, b"aaa");
692        let b = put_blob(&s, b"bbb");
693        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
694        let ours = make_tree(
695            &s,
696            vec![
697                entry(b"a.txt", EntryMode::Blob, a),
698                entry(b"b.txt", EntryMode::Blob, b),
699            ],
700        );
701        let r = merge_trees(&s, Some(base), Some(ours), Some(base)).unwrap();
702        assert!(!r.has_conflicts());
703        let entries = tree_entries(&s, r.tree_hash);
704        assert_eq!(entries.len(), 2);
705        assert_eq!(find_entry(&entries, b"a.txt").unwrap().object_hash, a);
706        assert_eq!(find_entry(&entries, b"b.txt").unwrap().object_hash, b);
707    }
708
709    #[test]
710    fn merge_theirs_adds_file() {
711        let (_d, s) = store();
712        let a = put_blob(&s, b"aaa");
713        let c = put_blob(&s, b"ccc");
714        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
715        let theirs = make_tree(
716            &s,
717            vec![
718                entry(b"a.txt", EntryMode::Blob, a),
719                entry(b"c.txt", EntryMode::Blob, c),
720            ],
721        );
722        let r = merge_trees(&s, Some(base), Some(base), Some(theirs)).unwrap();
723        assert!(!r.has_conflicts());
724        let entries = tree_entries(&s, r.tree_hash);
725        assert_eq!(entries.len(), 2);
726        assert_eq!(find_entry(&entries, b"c.txt").unwrap().object_hash, c);
727    }
728
729    #[test]
730    fn merge_both_add_different_files() {
731        let (_d, s) = store();
732        let a = put_blob(&s, b"aaa");
733        let b = put_blob(&s, b"bbb");
734        let c = put_blob(&s, b"ccc");
735        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
736        let ours = make_tree(
737            &s,
738            vec![
739                entry(b"a.txt", EntryMode::Blob, a),
740                entry(b"b.txt", EntryMode::Blob, b),
741            ],
742        );
743        let theirs = make_tree(
744            &s,
745            vec![
746                entry(b"a.txt", EntryMode::Blob, a),
747                entry(b"c.txt", EntryMode::Blob, c),
748            ],
749        );
750        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
751        assert!(!r.has_conflicts());
752        assert_eq!(tree_entries(&s, r.tree_hash).len(), 3);
753    }
754
755    #[test]
756    fn merge_both_add_same_file_same_content() {
757        let (_d, s) = store();
758        let a = put_blob(&s, b"aaa");
759        let b = put_blob(&s, b"bbb");
760        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
761        let twin = make_tree(
762            &s,
763            vec![
764                entry(b"a.txt", EntryMode::Blob, a),
765                entry(b"b.txt", EntryMode::Blob, b),
766            ],
767        );
768        let r = merge_trees(&s, Some(base), Some(twin), Some(twin)).unwrap();
769        assert!(!r.has_conflicts());
770        assert_eq!(
771            find_entry(&tree_entries(&s, r.tree_hash), b"b.txt")
772                .unwrap()
773                .object_hash,
774            b
775        );
776    }
777
778    #[test]
779    fn merge_both_add_same_file_different_content() {
780        let (_d, s) = store();
781        let a = put_blob(&s, b"aaa");
782        let b1 = put_blob(&s, b"bbb-ours");
783        let b2 = put_blob(&s, b"bbb-theirs");
784        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
785        let ours = make_tree(
786            &s,
787            vec![
788                entry(b"a.txt", EntryMode::Blob, a),
789                entry(b"b.txt", EntryMode::Blob, b1),
790            ],
791        );
792        let theirs = make_tree(
793            &s,
794            vec![
795                entry(b"a.txt", EntryMode::Blob, a),
796                entry(b"b.txt", EntryMode::Blob, b2),
797            ],
798        );
799        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
800        assert!(r.has_conflicts());
801        assert_eq!(r.conflicts.len(), 1);
802        let c = &r.conflicts[0];
803        assert_eq!(c.path, "b.txt");
804        assert_eq!(c.kind, ConflictKind::AddAdd);
805        assert_eq!(c.base_hash, None);
806        assert_eq!(c.ours_hash, Some(b1));
807        assert_eq!(c.theirs_hash, Some(b2));
808    }
809
810    #[test]
811    fn merge_ours_modifies() {
812        let (_d, s) = store();
813        let v1 = put_blob(&s, b"v1");
814        let v2 = put_blob(&s, b"v2");
815        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v1)]);
816        let ours = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v2)]);
817        let r = merge_trees(&s, Some(base), Some(ours), Some(base)).unwrap();
818        assert!(!r.has_conflicts());
819        assert_eq!(
820            find_entry(&tree_entries(&s, r.tree_hash), b"a.txt")
821                .unwrap()
822                .object_hash,
823            v2
824        );
825    }
826
827    #[test]
828    fn merge_theirs_modifies() {
829        let (_d, s) = store();
830        let v1 = put_blob(&s, b"v1");
831        let v2 = put_blob(&s, b"v2");
832        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v1)]);
833        let theirs = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v2)]);
834        let r = merge_trees(&s, Some(base), Some(base), Some(theirs)).unwrap();
835        assert!(!r.has_conflicts());
836        assert_eq!(
837            find_entry(&tree_entries(&s, r.tree_hash), b"a.txt")
838                .unwrap()
839                .object_hash,
840            v2
841        );
842    }
843
844    #[test]
845    fn merge_both_modify_same_way() {
846        let (_d, s) = store();
847        let v1 = put_blob(&s, b"v1");
848        let v2 = put_blob(&s, b"v2");
849        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v1)]);
850        let modified = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v2)]);
851        let r = merge_trees(&s, Some(base), Some(modified), Some(modified)).unwrap();
852        assert!(!r.has_conflicts());
853    }
854
855    #[test]
856    fn merge_both_modify_differently() {
857        let (_d, s) = store();
858        let v1 = put_blob(&s, b"v1");
859        let v2 = put_blob(&s, b"v2-ours");
860        let v3 = put_blob(&s, b"v3-theirs");
861        let base = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v1)]);
862        let ours = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v2)]);
863        let theirs = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, v3)]);
864        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
865        assert!(r.has_conflicts());
866        let c = &r.conflicts[0];
867        assert_eq!(c.path, "a.txt");
868        assert_eq!(c.kind, ConflictKind::ModifyModify);
869        assert_eq!(c.base_hash, Some(v1));
870        assert_eq!(c.ours_hash, Some(v2));
871        assert_eq!(c.theirs_hash, Some(v3));
872    }
873
874    #[test]
875    fn merge_ours_deletes() {
876        let (_d, s) = store();
877        let a = put_blob(&s, b"aaa");
878        let b = put_blob(&s, b"bbb");
879        let base = make_tree(
880            &s,
881            vec![
882                entry(b"a.txt", EntryMode::Blob, a),
883                entry(b"b.txt", EntryMode::Blob, b),
884            ],
885        );
886        let ours = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
887        let r = merge_trees(&s, Some(base), Some(ours), Some(base)).unwrap();
888        assert!(!r.has_conflicts());
889        assert_eq!(tree_entries(&s, r.tree_hash).len(), 1);
890    }
891
892    #[test]
893    fn merge_theirs_deletes() {
894        let (_d, s) = store();
895        let a = put_blob(&s, b"aaa");
896        let b = put_blob(&s, b"bbb");
897        let base = make_tree(
898            &s,
899            vec![
900                entry(b"a.txt", EntryMode::Blob, a),
901                entry(b"b.txt", EntryMode::Blob, b),
902            ],
903        );
904        let theirs = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
905        let r = merge_trees(&s, Some(base), Some(base), Some(theirs)).unwrap();
906        assert!(!r.has_conflicts());
907        assert_eq!(tree_entries(&s, r.tree_hash).len(), 1);
908    }
909
910    #[test]
911    fn merge_both_delete() {
912        let (_d, s) = store();
913        let a = put_blob(&s, b"aaa");
914        let b = put_blob(&s, b"bbb");
915        let base = make_tree(
916            &s,
917            vec![
918                entry(b"a.txt", EntryMode::Blob, a),
919                entry(b"b.txt", EntryMode::Blob, b),
920            ],
921        );
922        let both = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
923        let r = merge_trees(&s, Some(base), Some(both), Some(both)).unwrap();
924        assert!(!r.has_conflicts());
925        assert_eq!(tree_entries(&s, r.tree_hash).len(), 1);
926    }
927
928    #[test]
929    fn merge_delete_vs_modify() {
930        let (_d, s) = store();
931        let a = put_blob(&s, b"aaa");
932        let b1 = put_blob(&s, b"bbb-v1");
933        let b2 = put_blob(&s, b"bbb-v2");
934        let base = make_tree(
935            &s,
936            vec![
937                entry(b"a.txt", EntryMode::Blob, a),
938                entry(b"b.txt", EntryMode::Blob, b1),
939            ],
940        );
941        let ours = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
942        let theirs = make_tree(
943            &s,
944            vec![
945                entry(b"a.txt", EntryMode::Blob, a),
946                entry(b"b.txt", EntryMode::Blob, b2),
947            ],
948        );
949        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
950        assert!(r.has_conflicts());
951        let c = &r.conflicts[0];
952        assert_eq!(c.path, "b.txt");
953        assert_eq!(c.kind, ConflictKind::DeleteModify);
954        assert_eq!(c.base_hash, Some(b1));
955        assert_eq!(c.ours_hash, None);
956        assert_eq!(c.theirs_hash, Some(b2));
957    }
958
959    #[test]
960    fn merge_nested_tree_changes() {
961        let (_d, s) = store();
962        let main_v1 = put_blob(&s, b"fn main() {}");
963        let main_v2 = put_blob(&s, b"fn main() { run(); }");
964        let util = put_blob(&s, b"fn util() {}");
965        let base_sub = make_tree(&s, vec![entry(b"main.rs", EntryMode::Blob, main_v1)]);
966        let base = make_tree(&s, vec![entry(b"src", EntryMode::Tree, base_sub)]);
967        let ours_sub = make_tree(&s, vec![entry(b"main.rs", EntryMode::Blob, main_v2)]);
968        let ours = make_tree(&s, vec![entry(b"src", EntryMode::Tree, ours_sub)]);
969        let theirs_sub = make_tree(
970            &s,
971            vec![
972                entry(b"main.rs", EntryMode::Blob, main_v1),
973                entry(b"util.rs", EntryMode::Blob, util),
974            ],
975        );
976        let theirs = make_tree(&s, vec![entry(b"src", EntryMode::Tree, theirs_sub)]);
977        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
978        assert!(!r.has_conflicts());
979        let root = tree_entries(&s, r.tree_hash);
980        assert_eq!(root.len(), 1);
981        let src = tree_entries(&s, root[0].object_hash);
982        assert_eq!(src.len(), 2);
983        assert_eq!(find_entry(&src, b"main.rs").unwrap().object_hash, main_v2);
984        assert_eq!(find_entry(&src, b"util.rs").unwrap().object_hash, util);
985    }
986
987    #[test]
988    fn merge_nested_conflict_path_is_full() {
989        let (_d, s) = store();
990        let v1 = put_blob(&s, b"original");
991        let v2 = put_blob(&s, b"ours-change");
992        let v3 = put_blob(&s, b"theirs-change");
993        let base_sub = make_tree(&s, vec![entry(b"main.rs", EntryMode::Blob, v1)]);
994        let base = make_tree(&s, vec![entry(b"src", EntryMode::Tree, base_sub)]);
995        let ours_sub = make_tree(&s, vec![entry(b"main.rs", EntryMode::Blob, v2)]);
996        let ours = make_tree(&s, vec![entry(b"src", EntryMode::Tree, ours_sub)]);
997        let theirs_sub = make_tree(&s, vec![entry(b"main.rs", EntryMode::Blob, v3)]);
998        let theirs = make_tree(&s, vec![entry(b"src", EntryMode::Tree, theirs_sub)]);
999        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
1000        assert!(r.has_conflicts());
1001        assert_eq!(r.conflicts[0].path, "src/main.rs");
1002        assert_eq!(r.conflicts[0].kind, ConflictKind::ModifyModify);
1003        assert_eq!(r.conflicts[0].base_hash, Some(v1));
1004        assert_eq!(r.conflicts[0].ours_hash, Some(v2));
1005        assert_eq!(r.conflicts[0].theirs_hash, Some(v3));
1006    }
1007
1008    #[test]
1009    fn merge_empty_base() {
1010        let (_d, s) = store();
1011        let a = put_blob(&s, b"aaa");
1012        let b = put_blob(&s, b"bbb");
1013        let ours = make_tree(&s, vec![entry(b"a.txt", EntryMode::Blob, a)]);
1014        let theirs = make_tree(&s, vec![entry(b"b.txt", EntryMode::Blob, b)]);
1015        let r = merge_trees(&s, None, Some(ours), Some(theirs)).unwrap();
1016        assert!(!r.has_conflicts());
1017        assert_eq!(tree_entries(&s, r.tree_hash).len(), 2);
1018    }
1019
1020    // ---- find_merge_base ----
1021
1022    #[test]
1023    fn find_merge_base_linear() {
1024        let (_d, s) = store();
1025        let empty = make_tree(&s, vec![]);
1026        let a = make_commit(&s, empty, &[], "A");
1027        let b = make_commit(&s, empty, &[a], "B");
1028        let c = make_commit(&s, empty, &[b], "C");
1029        let d = make_commit(&s, empty, &[b], "D");
1030        assert_eq!(find_merge_base(&s, c, d).unwrap(), Some(b));
1031    }
1032
1033    #[test]
1034    fn find_merge_base_root() {
1035        let (_d, s) = store();
1036        let empty = make_tree(&s, vec![]);
1037        let a = make_commit(&s, empty, &[], "A");
1038        let b = make_commit(&s, empty, &[a], "B");
1039        let c = make_commit(&s, empty, &[a], "C");
1040        assert_eq!(find_merge_base(&s, b, c).unwrap(), Some(a));
1041    }
1042
1043    #[test]
1044    fn find_merge_base_no_common_ancestor() {
1045        let (_d, s) = store();
1046        let empty = make_tree(&s, vec![]);
1047        let x = make_commit(&s, empty, &[], "X");
1048        let y = make_commit(&s, empty, &[], "Y");
1049        assert_eq!(find_merge_base(&s, x, y).unwrap(), None);
1050    }
1051
1052    #[test]
1053    fn find_merge_base_same_commit() {
1054        let (_d, s) = store();
1055        let empty = make_tree(&s, vec![]);
1056        let a = make_commit(&s, empty, &[], "A");
1057        assert_eq!(find_merge_base(&s, a, a).unwrap(), Some(a));
1058    }
1059
1060    #[test]
1061    fn find_merge_base_walks_non_first_parents() {
1062        let (_d, s) = store();
1063        let empty = make_tree(&s, vec![]);
1064        let root = make_commit(&s, empty, &[], "root");
1065        let left = make_commit(&s, empty, &[root], "left");
1066        let right = make_commit(&s, empty, &[root], "right");
1067        let m = make_commit(&s, empty, &[left, right], "merge");
1068        let tip = make_commit(&s, empty, &[m], "tip");
1069        assert_eq!(find_merge_base(&s, tip, right).unwrap(), Some(right));
1070    }
1071
1072    #[test]
1073    fn is_ancestor_walks_merge_parents() {
1074        let (_d, s) = store();
1075        let empty = make_tree(&s, vec![]);
1076        let root = make_commit(&s, empty, &[], "root");
1077        let left = make_commit(&s, empty, &[root], "left");
1078        let right = make_commit(&s, empty, &[root], "right");
1079        let m = make_commit(&s, empty, &[left, right], "merge");
1080
1081        assert!(is_ancestor(&s, root, m).unwrap());
1082        assert!(is_ancestor(&s, right, m).unwrap());
1083        assert!(!is_ancestor(&s, m, right).unwrap());
1084    }
1085
1086    #[test]
1087    fn merge_both_modify_disjoint_lines_auto_merges() {
1088        let (_d, s) = store();
1089        let base_b = put_blob(&s, b"a\nb\nc\nd\ne\n");
1090        let ours_b = put_blob(&s, b"A\nb\nc\nd\ne\n"); // line 1
1091        let theirs_b = put_blob(&s, b"a\nb\nc\nd\nE\n"); // line 5
1092        let base = make_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, base_b)]);
1093        let ours = make_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, ours_b)]);
1094        let theirs = make_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, theirs_b)]);
1095        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
1096        assert!(
1097            !r.has_conflicts(),
1098            "disjoint line edits should auto-merge (#298), got {:?}",
1099            r.conflicts
1100        );
1101        let f = find_entry(&tree_entries(&s, r.tree_hash), b"f.txt")
1102            .unwrap()
1103            .object_hash;
1104        let merged = match s.read_object(&f).unwrap() {
1105            Object::Blob(b) => b.data,
1106            other => panic!("merged f.txt not a blob: {other:?}"),
1107        };
1108        assert_eq!(merged, b"A\nb\nc\nd\nE\n");
1109    }
1110
1111    #[test]
1112    fn merge_both_modify_same_line_still_conflicts() {
1113        let (_d, s) = store();
1114        let base_b = put_blob(&s, b"a\nb\nc\n");
1115        let ours_b = put_blob(&s, b"a\nOURS\nc\n"); // line 2
1116        let theirs_b = put_blob(&s, b"a\nTHEIRS\nc\n"); // line 2 — overlaps
1117        let base = make_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, base_b)]);
1118        let ours = make_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, ours_b)]);
1119        let theirs = make_tree(&s, vec![entry(b"f.txt", EntryMode::Blob, theirs_b)]);
1120        let r = merge_trees(&s, Some(base), Some(ours), Some(theirs)).unwrap();
1121        assert!(r.has_conflicts(), "same-line edits must still conflict");
1122        assert_eq!(r.conflicts[0].kind, ConflictKind::ModifyModify);
1123    }
1124}