Skip to main content

suture_core/dag/
graph.rs

1//! Patch DAG graph data structure.
2//!
3//! An in-memory representation of the patch DAG, supporting:
4//! - Adding patches with parent edges
5//! - Ancestor queries
6//! - Lowest Common Ancestor (LCA) computation
7//! - Acyclicity enforcement
8
9use crate::patch::types::{Patch, PatchId};
10use std::cell::RefCell;
11use std::collections::{HashMap, HashSet, VecDeque};
12use suture_common::BranchName;
13use thiserror::Error;
14
15/// Errors that can occur during DAG operations.
16#[derive(Error, Debug)]
17pub enum DagError {
18    #[error("patch already exists: {0}")]
19    DuplicatePatch(String),
20
21    #[error("patch not found: {0}")]
22    PatchNotFound(String),
23
24    #[error("parent patch not found: {0}")]
25    ParentNotFound(String),
26
27    #[error("would create a cycle: {from} -> {to}")]
28    CycleDetected { from: String, to: String },
29
30    #[error("branch not found: {0}")]
31    BranchNotFound(String),
32
33    #[error("branch already exists: {0}")]
34    BranchAlreadyExists(String),
35
36    #[error("empty branch name")]
37    EmptyBranchName,
38
39    #[error("invalid branch name: {0}")]
40    InvalidBranchName(String),
41
42    #[error("cannot create root patch with parents")]
43    RootWithParents,
44
45    #[error("{0}")]
46    Custom(String),
47}
48
49/// A node in the Patch-DAG.
50#[derive(Clone, Debug)]
51pub struct DagNode {
52    /// The patch data.
53    pub(crate) patch: Patch,
54    /// Parent patch IDs.
55    pub(crate) parent_ids: Vec<PatchId>,
56    /// Child patch IDs.
57    pub(crate) child_ids: Vec<PatchId>,
58    /// Generation number: max(parent generations) + 1, or 0 for root.
59    /// Used for O(1) depth comparisons in LCA computation.
60    pub(crate) generation: u64,
61}
62
63impl DagNode {
64    /// Returns the ID of this node's patch.
65    pub fn id(&self) -> &PatchId {
66        &self.patch.id
67    }
68}
69
70/// The Patch-DAG — a directed acyclic graph of patches.
71///
72/// Internally stores:
73/// - A HashMap of PatchId -> DagNode
74/// - A HashMap of branch name -> target PatchId
75/// - An ancestor cache (lazy, stable across mutations since adding new nodes
76///   never changes existing nodes' ancestor sets)
77pub struct PatchDag {
78    /// All nodes in the DAG, indexed by patch ID.
79    pub(crate) nodes: HashMap<PatchId, DagNode>,
80    /// Named branch pointers.
81    pub(crate) branches: HashMap<String, PatchId>,
82    /// Cache of ancestor sets, keyed by patch ID.
83    /// Populated lazily by `ancestors()`; stable because `add_patch()` only
84    /// creates new nodes (existing nodes' ancestor sets never change).
85    ancestor_cache: RefCell<HashMap<PatchId, HashSet<PatchId>>>,
86}
87
88impl Default for PatchDag {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94impl PatchDag {
95    /// Create a new empty DAG.
96    pub fn new() -> Self {
97        Self {
98            nodes: HashMap::new(),
99            branches: HashMap::new(),
100            ancestor_cache: RefCell::new(HashMap::new()),
101        }
102    }
103
104    /// Add a patch to the DAG.
105    ///
106    /// # Arguments
107    ///
108    /// * `patch` - The patch to add
109    /// * `parent_ids` - Parent patch IDs (empty for root commit)
110    ///
111    /// # Errors
112    ///
113    /// - `DuplicatePatch` if a patch with the same ID already exists
114    /// - `ParentNotFound` if a parent doesn't exist (unless it's a root commit)
115    /// - `CycleDetected` if adding this edge would create a cycle
116    pub fn add_patch(
117        &mut self,
118        patch: Patch,
119        parent_ids: Vec<PatchId>,
120    ) -> Result<PatchId, DagError> {
121        let id = patch.id;
122
123        // Check for duplicates
124        if self.nodes.contains_key(&id) {
125            return Err(DagError::DuplicatePatch(id.to_hex()));
126        }
127
128        // Validate parents exist (unless this is a root commit)
129        if !parent_ids.is_empty() {
130            for parent_id in &parent_ids {
131                if !self.nodes.contains_key(parent_id) {
132                    return Err(DagError::ParentNotFound(parent_id.to_hex()));
133                }
134            }
135
136            // Check for cycles: ensure this patch is not an ancestor of any parent
137            // (Since this patch is new, it can't be an ancestor yet, so this check
138            // is trivially satisfied for new patches. The cycle check is more relevant
139            // for branch operations.)
140        }
141
142        // Compute generation number: max(parent generations) + 1, or 0 for root
143        let generation = if parent_ids.is_empty() {
144            0
145        } else {
146            parent_ids
147                .iter()
148                .map(|pid| self.nodes.get(pid).map(|n| n.generation).unwrap_or(0))
149                .max()
150                .unwrap_or(0)
151                + 1
152        };
153
154        // Create the node
155        let node = DagNode {
156            patch,
157            parent_ids: parent_ids.clone(),
158            child_ids: Vec::new(),
159            generation,
160        };
161
162        // Add edges from parents to this node
163        for parent_id in &parent_ids {
164            if let Some(parent_node) = self.nodes.get_mut(parent_id) {
165                parent_node.child_ids.push(id);
166            }
167        }
168
169        self.nodes.insert(id, node);
170        Ok(id)
171    }
172
173    /// Get a patch by ID.
174    pub fn get_patch(&self, id: &PatchId) -> Option<&Patch> {
175        self.nodes.get(id).map(|node| &node.patch)
176    }
177
178    /// Get a node by ID.
179    pub fn get_node(&self, id: &PatchId) -> Option<&DagNode> {
180        self.nodes.get(id)
181    }
182
183    /// Check if a patch exists.
184    pub fn has_patch(&self, id: &PatchId) -> bool {
185        self.nodes.contains_key(id)
186    }
187
188    /// Get all transitive ancestors of a patch (excluding the patch itself).
189    ///
190    /// Uses BFS traversal. Results are cached: the first call for a given patch
191    /// ID computes the ancestor set via BFS; subsequent calls return the cached
192    /// result in O(1). The cache is safe without invalidation because
193    /// `add_patch()` only creates new nodes — existing nodes' ancestor sets
194    /// never change.
195    pub fn ancestors(&self, id: &PatchId) -> HashSet<PatchId> {
196        // Check cache first
197        if let Some(cached) = self.ancestor_cache.borrow().get(id) {
198            return cached.clone();
199        }
200
201        // BFS to compute ancestor set
202        let mut ancestors = HashSet::new();
203        let mut queue: VecDeque<PatchId> = VecDeque::new();
204
205        if let Some(node) = self.nodes.get(id) {
206            for parent_id in &node.parent_ids {
207                if !ancestors.contains(parent_id) {
208                    ancestors.insert(*parent_id);
209                    queue.push_back(*parent_id);
210                }
211            }
212        }
213
214        while let Some(current) = queue.pop_front() {
215            if let Some(node) = self.nodes.get(&current) {
216                for parent_id in &node.parent_ids {
217                    if ancestors.insert(*parent_id) {
218                        queue.push_back(*parent_id);
219                    }
220                }
221            }
222        }
223
224        // Store in cache
225        self.ancestor_cache
226            .borrow_mut()
227            .insert(*id, ancestors.clone());
228        ancestors
229    }
230
231    /// Find the Lowest Common Ancestor (LCA) of two patches.
232    ///
233    /// The LCA is the most recent patch that is an ancestor of both.
234    /// Returns `None` if no common ancestor exists.
235    ///
236    /// Uses generation numbers for O(1) depth comparison instead of BFS-based
237    /// `ancestor_depth()`, reducing complexity from O(n²) to O(n).
238    pub fn lca(&self, a: &PatchId, b: &PatchId) -> Option<PatchId> {
239        if a == b {
240            return Some(*a);
241        }
242
243        let ancestors_a = self.ancestors(a);
244        let ancestors_b = self.ancestors(b);
245
246        // Quick check: if a is an ancestor of b, then a is the LCA
247        if ancestors_b.contains(a) {
248            return Some(*a);
249        }
250        // If b is an ancestor of a, then b is the LCA
251        if ancestors_a.contains(b) {
252            return Some(*b);
253        }
254
255        // Find common ancestors
256        let common: Vec<PatchId> = ancestors_a.intersection(&ancestors_b).copied().collect();
257
258        if common.is_empty() {
259            return None;
260        }
261
262        // Find the most recent common ancestor (highest generation number).
263        // Uses precomputed generation field — O(1) per lookup instead of BFS.
264        let mut best: Option<PatchId> = None;
265        let mut best_gen: u64 = 0;
266
267        for candidate in &common {
268            let candidate_gen = self.nodes.get(candidate).map(|n| n.generation).unwrap_or(0);
269            if candidate_gen >= best_gen {
270                best_gen = candidate_gen;
271                best = Some(*candidate);
272            }
273        }
274
275        best
276    }
277
278    /// Compute the "depth" of a patch using its precomputed generation number.
279    ///
280    /// For a linear chain, this equals the number of ancestors.
281    /// For DAGs with merges, the generation is the length of the longest
282    /// path from root to this node.
283    #[allow(dead_code)]
284    fn ancestor_depth(&self, id: &PatchId) -> usize {
285        self.nodes
286            .get(id)
287            .map(|n| n.generation as usize)
288            .unwrap_or(0)
289    }
290
291    /// Create a new branch pointing to a patch.
292    pub fn create_branch(&mut self, name: BranchName, target: PatchId) -> Result<(), DagError> {
293        let name_str = name.as_str().to_string();
294
295        if name_str.is_empty() {
296            return Err(DagError::EmptyBranchName);
297        }
298
299        if self.branches.contains_key(&name_str) {
300            return Err(DagError::BranchAlreadyExists(name_str));
301        }
302
303        if !self.nodes.contains_key(&target) {
304            return Err(DagError::PatchNotFound(target.to_hex()));
305        }
306
307        self.branches.insert(name_str, target);
308        Ok(())
309    }
310
311    /// Get the target patch ID of a branch.
312    pub fn get_branch(&self, name: &BranchName) -> Option<PatchId> {
313        self.branches.get(name.as_str()).copied()
314    }
315
316    /// Update a branch to point to a new patch.
317    pub fn update_branch(&mut self, name: &BranchName, target: PatchId) -> Result<(), DagError> {
318        if !self.branches.contains_key(name.as_str()) {
319            return Err(DagError::BranchNotFound(name.as_str().to_string()));
320        }
321        if !self.nodes.contains_key(&target) {
322            return Err(DagError::PatchNotFound(target.to_hex()));
323        }
324        self.branches.insert(name.as_str().to_string(), target);
325        Ok(())
326    }
327
328    /// Delete a branch.
329    pub fn delete_branch(&mut self, name: &BranchName) -> Result<(), DagError> {
330        if self.branches.remove(name.as_str()).is_none() {
331            return Err(DagError::BranchNotFound(name.as_str().to_string()));
332        }
333        Ok(())
334    }
335
336    /// List all branches.
337    pub fn list_branches(&self) -> Vec<(String, PatchId)> {
338        let mut branches: Vec<_> = self
339            .branches
340            .iter()
341            .map(|(name, id)| (name.clone(), *id))
342            .collect();
343        branches.sort_by(|a, b| a.0.cmp(&b.0));
344        branches
345    }
346
347    /// Get the total number of patches in the DAG.
348    pub fn patch_count(&self) -> usize {
349        self.nodes.len()
350    }
351
352    /// Get all patch IDs in the DAG.
353    pub fn patch_ids(&self) -> Vec<PatchId> {
354        self.nodes.keys().copied().collect()
355    }
356
357    /// Get patches from a specific node back to root (inclusive).
358    pub fn patch_chain(&self, id: &PatchId) -> Vec<PatchId> {
359        let mut chain = Vec::new();
360        let mut current = Some(*id);
361
362        while let Some(curr_id) = current {
363            if chain.contains(&curr_id) {
364                break; // Safety: shouldn't happen in a valid DAG
365            }
366            chain.push(curr_id);
367            current = self
368                .nodes
369                .get(&curr_id)
370                .and_then(|n| n.parent_ids.first().copied());
371        }
372
373        chain
374    }
375
376    /// Collect all patches reachable from a given patch ID (inclusive).
377    pub fn reachable_patches(&self, id: &PatchId) -> Vec<Patch> {
378        let ancestors = self.ancestors(id);
379        let mut patches = Vec::with_capacity(ancestors.len() + 1);
380
381        if let Some(node) = self.nodes.get(id) {
382            patches.push(node.patch.clone());
383        }
384
385        for ancestor_id in ancestors {
386            if let Some(node) = self.nodes.get(&ancestor_id) {
387                patches.push(node.patch.clone());
388            }
389        }
390
391        patches
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::patch::types::{OperationType, Patch, TouchSet};
399    use suture_common::Hash;
400
401    fn make_patch(addr: &str) -> Patch {
402        Patch::new(
403            OperationType::Modify,
404            TouchSet::single(addr),
405            Some(format!("file_{}", addr)),
406            vec![],
407            vec![],
408            "test".to_string(),
409            format!("edit {}", addr),
410        )
411    }
412
413    #[test]
414    fn test_add_root_patch() {
415        let mut dag = PatchDag::new();
416        let root = make_patch("root");
417        let id = dag.add_patch(root, vec![]).unwrap();
418        assert_eq!(dag.patch_count(), 1);
419        assert!(dag.has_patch(&id));
420    }
421
422    #[test]
423    fn test_add_patch_with_parent() {
424        let mut dag = PatchDag::new();
425        let root = make_patch("root");
426        let root_id = dag.add_patch(root, vec![]).unwrap();
427
428        let child = make_patch("child");
429        let child_id = dag.add_patch(child, vec![root_id]).unwrap();
430        assert_eq!(dag.patch_count(), 2);
431
432        let ancestors = dag.ancestors(&child_id);
433        assert_eq!(ancestors.len(), 1);
434        assert!(ancestors.contains(&root_id));
435    }
436
437    #[test]
438    fn test_duplicate_patch_rejected() {
439        let mut dag = PatchDag::new();
440        let p = make_patch("dup");
441        let _id = dag.add_patch(p.clone(), vec![]).unwrap();
442        let result = dag.add_patch(p, vec![]);
443        assert!(matches!(result, Err(DagError::DuplicatePatch(_))));
444    }
445
446    #[test]
447    fn test_parent_not_found() {
448        let mut dag = PatchDag::new();
449        let child = make_patch("child");
450        let fake_parent = Hash::from_hex(&"f".repeat(64)).unwrap();
451        let result = dag.add_patch(child, vec![fake_parent]);
452        assert!(matches!(result, Err(DagError::ParentNotFound(_))));
453    }
454
455    #[test]
456    fn test_ancestors_linear_chain() {
457        let mut dag = PatchDag::new();
458        let p0 = make_patch("p0");
459        let id0 = dag.add_patch(p0, vec![]).unwrap();
460
461        let p1 = make_patch("p1");
462        let id1 = dag.add_patch(p1, vec![id0]).unwrap();
463
464        let p2 = make_patch("p2");
465        let id2 = dag.add_patch(p2, vec![id1]).unwrap();
466
467        let ancestors = dag.ancestors(&id2);
468        assert_eq!(ancestors.len(), 2);
469        assert!(ancestors.contains(&id0));
470        assert!(ancestors.contains(&id1));
471    }
472
473    #[test]
474    fn test_ancestors_diamond() {
475        let mut dag = PatchDag::new();
476        let root = make_patch("root");
477        let root_id = dag.add_patch(root, vec![]).unwrap();
478
479        let left = make_patch("left");
480        let left_id = dag.add_patch(left, vec![root_id]).unwrap();
481
482        let right = make_patch("right");
483        let right_id = dag.add_patch(right, vec![root_id]).unwrap();
484
485        let merge = make_patch("merge");
486        let merge_id = dag.add_patch(merge, vec![left_id, right_id]).unwrap();
487
488        let ancestors = dag.ancestors(&merge_id);
489        assert_eq!(ancestors.len(), 3); // root, left, right
490    }
491
492    #[test]
493    fn test_lca_linear() {
494        let mut dag = PatchDag::new();
495        let p0 = make_patch("p0");
496        let id0 = dag.add_patch(p0, vec![]).unwrap();
497
498        let p1 = make_patch("p1");
499        let id1 = dag.add_patch(p1, vec![id0]).unwrap();
500
501        let p2 = make_patch("p2");
502        let id2 = dag.add_patch(p2, vec![id1]).unwrap();
503
504        assert_eq!(dag.lca(&id1, &id2), Some(id1));
505        assert_eq!(dag.lca(&id0, &id2), Some(id0));
506    }
507
508    #[test]
509    fn test_hashset_contains() {
510        use std::collections::HashSet;
511        let h1 = suture_common::Hash::from_data(b"test1");
512        let h2 = suture_common::Hash::from_data(b"test2");
513        let mut set: HashSet<suture_common::Hash> = HashSet::new();
514        set.insert(h1);
515        set.insert(h2);
516        assert!(set.contains(&h1));
517        assert!(set.contains(&h2));
518        let h3 = suture_common::Hash::from_data(b"test1");
519        assert!(set.contains(&h3), "same-value hash should be in set");
520    }
521
522    #[test]
523    fn test_ancestors_with_hashset() {
524        let mut dag = PatchDag::new();
525        let root = make_patch("root");
526        let root_id = dag.add_patch(root, vec![]).unwrap();
527        let child = make_patch("child");
528        let child_id = dag.add_patch(child, vec![root_id]).unwrap();
529
530        let ancestors = dag.ancestors(&child_id);
531        assert_eq!(ancestors.len(), 1, "child should have 1 ancestor");
532        assert!(
533            ancestors.contains(&root_id),
534            "root should be ancestor of child"
535        );
536    }
537
538    #[test]
539    fn test_lca_diamond() {
540        let mut dag = PatchDag::new();
541        let root = make_patch("root");
542        let root_id = dag.add_patch(root, vec![]).unwrap();
543
544        let left = make_patch("left");
545        let left_id = dag.add_patch(left, vec![root_id]).unwrap();
546
547        let right = make_patch("right");
548        let right_id = dag.add_patch(right, vec![root_id]).unwrap();
549
550        let merge = make_patch("merge");
551        let merge_id = dag.add_patch(merge, vec![left_id, right_id]).unwrap();
552
553        // Debug: verify ancestors work
554        let anc_left = dag.ancestors(&left_id);
555        let anc_right = dag.ancestors(&right_id);
556        let anc_merge = dag.ancestors(&merge_id);
557        assert!(
558            anc_left.contains(&root_id),
559            "root_id should be ancestor of left_id"
560        );
561        assert!(
562            anc_right.contains(&root_id),
563            "root_id should be ancestor of right_id"
564        );
565        assert!(
566            anc_merge.contains(&left_id),
567            "left_id should be ancestor of merge_id"
568        );
569        assert!(
570            anc_merge.contains(&root_id),
571            "root_id should be ancestor of merge_id"
572        );
573        assert_eq!(
574            anc_left.len(),
575            1,
576            "left_id should have exactly 1 ancestor (root_id)"
577        );
578        assert_eq!(
579            anc_merge.len(),
580            3,
581            "merge_id should have 3 ancestors (left, right, root)"
582        );
583
584        let lca_result = dag.lca(&merge_id, &left_id);
585        assert_eq!(
586            lca_result,
587            Some(left_id),
588            "LCA of merge and left should be left"
589        );
590    }
591
592    #[test]
593    fn test_branch_operations() {
594        let mut dag = PatchDag::new();
595        let root = make_patch("root");
596        let root_id = dag.add_patch(root, vec![]).unwrap();
597
598        let main = BranchName::new("main").unwrap();
599        dag.create_branch(main.clone(), root_id).unwrap();
600
601        assert_eq!(dag.get_branch(&main), Some(root_id));
602        assert_eq!(dag.list_branches().len(), 1);
603
604        let child = make_patch("child");
605        let child_id = dag.add_patch(child, vec![root_id]).unwrap();
606        dag.update_branch(&main, child_id).unwrap();
607        assert_eq!(dag.get_branch(&main), Some(child_id));
608
609        let feat = BranchName::new("feature").unwrap();
610        dag.create_branch(feat.clone(), root_id).unwrap();
611        assert_eq!(dag.list_branches().len(), 2);
612
613        dag.delete_branch(&feat).unwrap();
614        assert_eq!(dag.list_branches().len(), 1);
615    }
616
617    #[test]
618    fn test_branch_duplicate_rejected() {
619        let mut dag = PatchDag::new();
620        let root = make_patch("root");
621        let root_id = dag.add_patch(root, vec![]).unwrap();
622
623        let main = BranchName::new("main").unwrap();
624        dag.create_branch(main.clone(), root_id).unwrap();
625        let result = dag.create_branch(main, root_id);
626        assert!(matches!(result, Err(DagError::BranchAlreadyExists(_))));
627    }
628
629    #[test]
630    fn test_patch_chain() {
631        let mut dag = PatchDag::new();
632        let p0 = make_patch("p0");
633        let id0 = dag.add_patch(p0, vec![]).unwrap();
634
635        let p1 = make_patch("p1");
636        let id1 = dag.add_patch(p1, vec![id0]).unwrap();
637
638        let p2 = make_patch("p2");
639        let id2 = dag.add_patch(p2, vec![id1]).unwrap();
640
641        let chain = dag.patch_chain(&id2);
642        assert_eq!(chain.len(), 3);
643        assert_eq!(chain[0], id2); // Most recent first
644        assert_eq!(chain[1], id1);
645        assert_eq!(chain[2], id0);
646    }
647
648    #[test]
649    fn test_generation_numbers_linear() {
650        let mut dag = PatchDag::new();
651        let p0 = make_patch("p0");
652        let id0 = dag.add_patch(p0, vec![]).unwrap();
653        assert_eq!(dag.get_node(&id0).unwrap().generation, 0);
654
655        let p1 = make_patch("p1");
656        let id1 = dag.add_patch(p1, vec![id0]).unwrap();
657        assert_eq!(dag.get_node(&id1).unwrap().generation, 1);
658
659        let p2 = make_patch("p2");
660        let id2 = dag.add_patch(p2, vec![id1]).unwrap();
661        assert_eq!(dag.get_node(&id2).unwrap().generation, 2);
662    }
663
664    #[test]
665    fn test_generation_numbers_diamond() {
666        let mut dag = PatchDag::new();
667        let root = make_patch("root");
668        let root_id = dag.add_patch(root, vec![]).unwrap();
669
670        let left = make_patch("left");
671        let left_id = dag.add_patch(left, vec![root_id]).unwrap();
672
673        let right = make_patch("right");
674        let right_id = dag.add_patch(right, vec![root_id]).unwrap();
675
676        let merge = make_patch("merge");
677        let merge_id = dag.add_patch(merge, vec![left_id, right_id]).unwrap();
678
679        assert_eq!(dag.get_node(&root_id).unwrap().generation, 0);
680        assert_eq!(dag.get_node(&left_id).unwrap().generation, 1);
681        assert_eq!(dag.get_node(&right_id).unwrap().generation, 1);
682        // Merge's generation = max(left.gen, right.gen) + 1 = 2
683        assert_eq!(dag.get_node(&merge_id).unwrap().generation, 2);
684    }
685
686    #[test]
687    fn test_generation_numbers_uneven_branches() {
688        let mut dag = PatchDag::new();
689        let root = make_patch("root");
690        let root_id = dag.add_patch(root, vec![]).unwrap();
691
692        // Short branch: root -> a
693        let a = make_patch("a");
694        let a_id = dag.add_patch(a, vec![root_id]).unwrap();
695
696        // Long branch: root -> b -> c -> d
697        let b = make_patch("b");
698        let b_id = dag.add_patch(b, vec![root_id]).unwrap();
699        let c = make_patch("c");
700        let c_id = dag.add_patch(c, vec![b_id]).unwrap();
701        let d = make_patch("d");
702        let d_id = dag.add_patch(d, vec![c_id]).unwrap();
703
704        // Merge short and long branches
705        let merge = make_patch("merge");
706        let merge_id = dag.add_patch(merge, vec![a_id, d_id]).unwrap();
707
708        assert_eq!(dag.get_node(&a_id).unwrap().generation, 1);
709        assert_eq!(dag.get_node(&d_id).unwrap().generation, 3);
710        // Merge gen = max(1, 3) + 1 = 4
711        assert_eq!(dag.get_node(&merge_id).unwrap().generation, 4);
712    }
713
714    #[test]
715    fn test_ancestor_cache() {
716        let mut dag = PatchDag::new();
717        let p0 = make_patch("p0");
718        let id0 = dag.add_patch(p0, vec![]).unwrap();
719        let p1 = make_patch("p1");
720        let id1 = dag.add_patch(p1, vec![id0]).unwrap();
721        let p2 = make_patch("p2");
722        let id2 = dag.add_patch(p2, vec![id1]).unwrap();
723
724        // First call: computes via BFS, caches result
725        let anc1 = dag.ancestors(&id2);
726        assert_eq!(anc1.len(), 2);
727
728        // Second call: should return cached result (same values)
729        let anc2 = dag.ancestors(&id2);
730        assert_eq!(anc2.len(), 2);
731        assert_eq!(anc1, anc2);
732
733        // Cache should have entries for id2
734        assert!(dag.ancestor_cache.borrow().contains_key(&id2));
735    }
736
737    #[test]
738    fn test_lca_uneven_branches() {
739        let mut dag = PatchDag::new();
740        let root = make_patch("root");
741        let root_id = dag.add_patch(root, vec![]).unwrap();
742
743        // Branch A: root -> a1 -> a2
744        let a1 = make_patch("a1");
745        let a1_id = dag.add_patch(a1, vec![root_id]).unwrap();
746        let a2 = make_patch("a2");
747        let a2_id = dag.add_patch(a2, vec![a1_id]).unwrap();
748
749        // Branch B: root -> b1
750        let b1 = make_patch("b1");
751        let b1_id = dag.add_patch(b1, vec![root_id]).unwrap();
752
753        // LCA(a2, b1) should be root
754        assert_eq!(dag.lca(&a2_id, &b1_id), Some(root_id));
755        // LCA(a1, b1) should be root
756        assert_eq!(dag.lca(&a1_id, &b1_id), Some(root_id));
757    }
758
759    #[test]
760    fn test_lca_no_common_ancestor() {
761        let mut dag = PatchDag::new();
762        // Two disconnected trees
763        let r1 = make_patch("root1");
764        let r1_id = dag.add_patch(r1, vec![]).unwrap();
765        let r2 = make_patch("root2");
766        let r2_id = dag.add_patch(r2, vec![]).unwrap();
767
768        // No common ancestor
769        assert_eq!(dag.lca(&r1_id, &r2_id), None);
770    }
771
772    mod proptests {
773        use super::*;
774        use crate::patch::types::{OperationType, Patch, TouchSet};
775        use proptest::prelude::*;
776
777        fn make_unique_patch(idx: usize) -> Patch {
778            let addr = format!("proptest_addr_{}", idx);
779            Patch::new(
780                OperationType::Modify,
781                TouchSet::single(&addr),
782                Some(format!("file_{}", addr)),
783                addr.clone().into_bytes(),
784                vec![],
785                "proptest".to_string(),
786                format!("patch {}", idx),
787            )
788        }
789
790        proptest! {
791            #[test]
792            fn add_patch_increases_count(n in 0usize..20) {
793                let mut dag = PatchDag::new();
794                let mut last_id = None;
795                for i in 0..n {
796                    let patch = make_unique_patch(i);
797                    let parents = last_id.map(|id| vec![id]).unwrap_or_default();
798                    let id = dag.add_patch(patch, parents).unwrap();
799                    last_id = Some(id);
800                }
801                prop_assert_eq!(dag.patch_count(), n);
802            }
803
804            #[test]
805            fn patch_chain_ancestry(n in 0usize..20) {
806                prop_assume!(n > 0);
807                let mut dag = PatchDag::new();
808                let mut last_id = None;
809                for i in 0..n {
810                    let patch = make_unique_patch(i);
811                    let parents = last_id.map(|id| vec![id]).unwrap_or_default();
812                    let id = dag.add_patch(patch, parents).unwrap();
813                    last_id = Some(id);
814                }
815                let tip = last_id.unwrap();
816                let chain = dag.patch_chain(&tip);
817                prop_assert_eq!(chain.len(), n);
818            }
819
820            #[test]
821            fn lca_linear_chain(n in 2usize..15) {
822                let mut dag = PatchDag::new();
823                let mut ids = Vec::new();
824                for i in 0..n {
825                    let patch = make_unique_patch(i);
826                    let parents = ids.last().map(|id| vec![*id]).unwrap_or_default();
827                    let id = dag.add_patch(patch, parents).unwrap();
828                    ids.push(id);
829                }
830                // LCA(first, last) == first
831                prop_assert_eq!(dag.lca(&ids[0], &ids[n - 1]), Some(ids[0]));
832                // LCA(second, last) == second
833                if n >= 3 {
834                    prop_assert_eq!(dag.lca(&ids[1], &ids[n - 1]), Some(ids[1]));
835                }
836                // LCA(last, last) == last
837                prop_assert_eq!(dag.lca(&ids[n - 1], &ids[n - 1]), Some(ids[n - 1]));
838            }
839
840            #[test]
841            fn ancestors_subset(n in 1usize..20) {
842                let mut dag = PatchDag::new();
843                let mut ids = Vec::new();
844                for i in 0..n {
845                    let patch = make_unique_patch(i);
846                    let parents = ids.last().map(|id| vec![*id]).unwrap_or_default();
847                    let id = dag.add_patch(patch, parents).unwrap();
848                    ids.push(id);
849                }
850                let tip = ids.last().unwrap();
851                let ancestors = dag.ancestors(tip);
852                // All predecessors should be in ancestors
853                for (i, id) in ids.iter().enumerate().take(n - 1) {
854                    prop_assert!(ancestors.contains(id),
855                        "predecessor {} should be ancestor of tip", i);
856                }
857                // Tip itself should NOT be in ancestors
858                prop_assert!(!ancestors.contains(tip));
859                // Should have exactly n-1 ancestors
860                prop_assert_eq!(ancestors.len(), n - 1);
861            }
862        }
863    }
864}