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