Skip to main content

giff_core/
algorithms.rs

1use crate::{FrameId, GiffError, Stack, StackFrame, StackStore};
2use std::collections::{HashMap, HashSet};
3
4impl StackStore {
5    /// Find the stack and frame for a given branch name.
6    pub fn find_stack_for_branch(&self, branch: &str) -> Option<(&Stack, &StackFrame)> {
7        for stack in &self.stacks {
8            if let Some(frame) = stack.frames.iter().find(|f| f.branch == branch) {
9                return Some((stack, frame));
10            }
11        }
12        None
13    }
14}
15
16impl Stack {
17    /// All frames whose `parent` is `None` — i.e. frames that target the trunk directly.
18    /// A well-formed linear stack has exactly one root; trees with multiple roots are valid
19    /// (e.g. "two parallel branches off main"), but `giff stack land` requires exactly one.
20    pub fn roots(&self) -> Vec<&StackFrame> {
21        self.frames.iter().filter(|f| f.parent.is_none()).collect()
22    }
23
24    /// Direct children of `id` — frames whose `parent` points at it.
25    /// Returns `Vec` because trees allow multiple children. For linear stacks this is 0 or 1.
26    pub fn children(&self, id: &FrameId) -> Vec<&StackFrame> {
27        self.frames
28            .iter()
29            .filter(|f| f.parent.as_ref() == Some(id))
30            .collect()
31    }
32
33    /// Look up a frame by id.
34    pub fn frame(&self, id: &FrameId) -> Option<&StackFrame> {
35        self.frames.iter().find(|f| &f.id == id)
36    }
37
38    /// The frame directly below `id` (its parent), or `None` if `id` is a root.
39    pub fn frame_below(&self, id: &FrameId) -> Option<&StackFrame> {
40        let frame = self.frame(id)?;
41        let parent_id = frame.parent.as_ref()?;
42        self.frame(parent_id)
43    }
44
45    /// Recursive descendants of `id` in pre-order (the subtree rooted at `id`,
46    /// excluding `id` itself).
47    pub fn descendants(&self, id: &FrameId) -> Vec<&StackFrame> {
48        let mut out = Vec::new();
49        let mut stack = vec![id.clone()];
50        while let Some(cur) = stack.pop() {
51            for child in self.children(&cur) {
52                out.push(child);
53                stack.push(child.id.clone());
54            }
55        }
56        out
57    }
58
59    /// Chain of ancestors from the parent of `id` up to a root.
60    /// First element is `id`'s parent, last element is the root.
61    pub fn ancestors(&self, id: &FrameId) -> Vec<&StackFrame> {
62        let mut out = Vec::new();
63        let mut cursor = self.frame(id).and_then(|f| f.parent.as_ref()).cloned();
64        while let Some(cur) = cursor {
65            if let Some(frame) = self.frame(&cur) {
66                out.push(frame);
67                cursor = frame.parent.clone();
68            } else {
69                break;
70            }
71        }
72        out
73    }
74
75    /// Path from a root down to `id` (inclusive). The first element is the root, the last is `id`.
76    pub fn path_to_root(&self, id: &FrameId) -> Vec<&StackFrame> {
77        let mut chain: Vec<&StackFrame> = self.ancestors(id);
78        chain.reverse();
79        if let Some(self_frame) = self.frame(id) {
80            chain.push(self_frame);
81        }
82        chain
83    }
84
85    /// Distance from `id` to its root (root has depth 0).
86    pub fn depth(&self, id: &FrameId) -> usize {
87        self.ancestors(id).len()
88    }
89
90    /// All frames in topological order — every parent appears before each of its children.
91    /// Stable for a given input (children are visited in `frames` insertion order so renders are
92    /// deterministic across runs).
93    pub fn topological_order(&self) -> Vec<&StackFrame> {
94        let mut out: Vec<&StackFrame> = Vec::with_capacity(self.frames.len());
95        let mut visited: HashSet<FrameId> = HashSet::new();
96        for root in self.roots() {
97            self.dfs_collect(&root.id, &mut visited, &mut out);
98        }
99        // Defensive: if validate() has been skipped and the stack contains a cycle, some frames
100        // may not be reachable from any root. Append them in insertion order so callers still see
101        // every frame and can detect the inconsistency (validate() catches the actual cycle).
102        for f in &self.frames {
103            if !visited.contains(&f.id) {
104                out.push(f);
105                visited.insert(f.id.clone());
106            }
107        }
108        out
109    }
110
111    fn dfs_collect<'a>(
112        &'a self,
113        id: &FrameId,
114        visited: &mut HashSet<FrameId>,
115        out: &mut Vec<&'a StackFrame>,
116    ) {
117        if !visited.insert(id.clone()) {
118            return;
119        }
120        if let Some(frame) = self.frame(id) {
121            out.push(frame);
122        }
123        for child in self.children(id) {
124            self.dfs_collect(&child.id, visited, out);
125        }
126    }
127
128    /// Topological order — was previously the linear bottom-to-top traversal. Now equivalent
129    /// to `topological_order()`. Kept for call sites still using this name.
130    pub fn ordered_frames(&self) -> Vec<&StackFrame> {
131        self.topological_order()
132    }
133
134    /// True when the stack is a single linear chain (one root, every frame has at most one child).
135    pub fn is_linear(&self) -> bool {
136        if self.roots().len() != 1 {
137            return false;
138        }
139        self.frames.iter().all(|f| self.children(&f.id).len() <= 1)
140    }
141
142    /// Compute the new parent for every non-merged frame after a set of frames are pruned.
143    ///
144    /// "Merged" here means "this PR was merged on GitHub, so we should drop the frame from the
145    /// local stack and re-parent its descendants up the chain." We walk each non-merged frame's
146    /// parent chain, skipping merged ancestors, until we hit a non-merged ancestor or `None`.
147    ///
148    /// The returned map only contains frames whose parent actually *changed*, so the caller can
149    /// push exactly that many `update_pr` calls to the forge and not waste API rate.
150    pub fn parent_updates_after_pruning(
151        &self,
152        merged: &HashSet<FrameId>,
153    ) -> HashMap<FrameId, Option<FrameId>> {
154        let by_id: HashMap<&FrameId, &StackFrame> =
155            self.frames.iter().map(|f| (&f.id, f)).collect();
156        let mut out = HashMap::new();
157        for frame in &self.frames {
158            if merged.contains(&frame.id) {
159                continue;
160            }
161            let mut p = frame.parent.clone();
162            while let Some(ref pid) = p {
163                if merged.contains(pid) {
164                    p = by_id.get(pid).and_then(|f| f.parent.clone());
165                } else {
166                    break;
167                }
168            }
169            if p != frame.parent {
170                out.insert(frame.id.clone(), p);
171            }
172        }
173        out
174    }
175
176    /// Validate the stack's tree shape. Returns Err on cycles, missing parents, duplicate ids,
177    /// or duplicate branches. Run after every mutation in `giff-cli` as a safety net.
178    pub fn validate(&self) -> Result<(), GiffError> {
179        // 1. No duplicate frame ids.
180        let mut ids: HashMap<&FrameId, ()> = HashMap::new();
181        for f in &self.frames {
182            if ids.insert(&f.id, ()).is_some() {
183                return Err(GiffError::InvalidStack(format!(
184                    "duplicate frame id `{}`",
185                    f.id.0
186                )));
187            }
188        }
189
190        // 2. No duplicate branch names.
191        let mut branches: HashMap<&str, ()> = HashMap::new();
192        for f in &self.frames {
193            if branches.insert(f.branch.as_str(), ()).is_some() {
194                return Err(GiffError::InvalidStack(format!(
195                    "duplicate branch `{}` in stack",
196                    f.branch
197                )));
198            }
199        }
200
201        // 3. Every parent_id must reference a frame in this stack.
202        for f in &self.frames {
203            if let Some(p) = f.parent.as_ref() {
204                if !ids.contains_key(p) {
205                    return Err(GiffError::InvalidStack(format!(
206                        "frame `{}` has dangling parent `{}`",
207                        f.branch, p.0
208                    )));
209                }
210            }
211        }
212
213        // 4. No cycles. Walk every frame upward; if we revisit, we have a cycle.
214        for f in &self.frames {
215            let mut seen: HashSet<&FrameId> = HashSet::new();
216            let mut cur = Some(&f.id);
217            while let Some(id) = cur {
218                if !seen.insert(id) {
219                    return Err(GiffError::InvalidStack(format!(
220                        "cycle detected at frame `{}`",
221                        f.branch
222                    )));
223                }
224                cur = self
225                    .frames
226                    .iter()
227                    .find(|x| &x.id == id)
228                    .and_then(|x| x.parent.as_ref());
229            }
230        }
231
232        Ok(())
233    }
234}