Skip to main content

rung_core/
stack.rs

1//! Stack data model representing a chain of dependent branches.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::BranchName;
7
8/// A stack of dependent branches forming a PR chain.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Stack {
11    /// Ordered list of branches from base to tip.
12    pub branches: Vec<StackBranch>,
13
14    /// Branches that have been merged (for preserving history in PR comments).
15    #[serde(default, skip_serializing_if = "Vec::is_empty")]
16    pub merged: Vec<MergedBranch>,
17}
18
19impl Stack {
20    /// Create a new empty stack.
21    #[must_use]
22    pub const fn new() -> Self {
23        Self {
24            branches: Vec::new(),
25            merged: Vec::new(),
26        }
27    }
28
29    /// Find a branch by name.
30    #[must_use]
31    pub fn find_branch(&self, name: &str) -> Option<&StackBranch> {
32        self.branches.iter().find(|b| b.name == name)
33    }
34
35    /// Find a branch by name (mutable).
36    pub fn find_branch_mut(&mut self, name: &str) -> Option<&mut StackBranch> {
37        self.branches.iter_mut().find(|b| b.name == name)
38    }
39
40    /// Add a new branch to the stack.
41    pub fn add_branch(&mut self, branch: StackBranch) {
42        self.branches.push(branch);
43    }
44
45    /// Remove a branch from the stack.
46    pub fn remove_branch(&mut self, name: &str) -> Option<StackBranch> {
47        if let Some(pos) = self.branches.iter().position(|b| b.name == name) {
48            Some(self.branches.remove(pos))
49        } else {
50            None
51        }
52    }
53
54    /// Mark a branch as merged, moving it from active to merged list.
55    ///
56    /// This preserves the branch info for stack comment history,
57    /// including the original parent for ancestry chain traversal.
58    /// Returns the removed branch if found.
59    pub fn mark_merged(&mut self, name: &str) -> Option<StackBranch> {
60        let branch = self.remove_branch(name)?;
61
62        if let Some(pr) = branch.pr {
63            self.merged.push(MergedBranch {
64                name: branch.name.clone(),
65                parent: branch.parent.clone(),
66                pr,
67                merged_at: Utc::now(),
68            });
69        }
70
71        Some(branch)
72    }
73
74    /// Find a merged branch by name.
75    #[must_use]
76    pub fn find_merged(&self, name: &str) -> Option<&MergedBranch> {
77        self.merged.iter().find(|b| b.name == name)
78    }
79
80    /// Find a merged branch by PR number.
81    #[must_use]
82    pub fn find_merged_by_pr(&self, pr: u64) -> Option<&MergedBranch> {
83        self.merged.iter().find(|b| b.pr == pr)
84    }
85
86    /// Clear merged branches when stack is empty.
87    ///
88    /// This should be called after merge operations to clean up
89    /// when the entire stack has been merged.
90    pub fn clear_merged_if_empty(&mut self) {
91        if self.branches.is_empty() {
92            self.merged.clear();
93        }
94    }
95
96    /// Get all children of a branch.
97    #[must_use]
98    pub fn children_of(&self, name: &str) -> Vec<&StackBranch> {
99        self.branches
100            .iter()
101            .filter(|b| b.parent.as_deref() == Some(name))
102            .collect()
103    }
104
105    /// Get all descendants of a branch in topological order (parents before children).
106    ///
107    /// This includes children, grandchildren, etc. The branch itself is NOT included.
108    #[must_use]
109    pub fn descendants(&self, name: &str) -> Vec<&StackBranch> {
110        let mut result = Vec::new();
111        let mut stack = vec![name];
112
113        while let Some(current_parent) = stack.pop() {
114            for branch in &self.branches {
115                if branch.parent.as_deref() == Some(current_parent) {
116                    result.push(branch);
117                    stack.push(&branch.name);
118                }
119            }
120        }
121        result
122    }
123
124    /// Get the ancestry chain for a branch (from root to the branch).
125    #[must_use]
126    pub fn ancestry(&self, name: &str) -> Vec<&StackBranch> {
127        let mut chain = vec![];
128        let mut current = name;
129
130        while let Some(branch) = self.find_branch(current) {
131            chain.push(branch);
132            match &branch.parent {
133                Some(parent) if self.find_branch(parent).is_some() => {
134                    current = parent;
135                }
136                _ => break,
137            }
138        }
139
140        chain.reverse();
141        chain
142    }
143
144    /// Check if the stack is empty.
145    #[must_use]
146    pub const fn is_empty(&self) -> bool {
147        self.branches.is_empty()
148    }
149
150    /// Get the number of branches in the stack.
151    #[must_use]
152    pub const fn len(&self) -> usize {
153        self.branches.len()
154    }
155
156    /// Check if reparenting `branch` to `new_parent` would create a cycle.
157    ///
158    /// A cycle would occur if the new parent is a descendant of the branch
159    /// (e.g., setting A's parent to B when B is already a child of A).
160    #[must_use]
161    pub fn would_create_cycle(&self, branch: &str, new_parent: &str) -> bool {
162        // Can't be your own parent
163        if branch == new_parent {
164            return true;
165        }
166
167        // Check if new_parent is a descendant of branch
168        self.descendants(branch)
169            .iter()
170            .any(|b| b.name == new_parent)
171    }
172
173    /// Reparent a branch to a new parent.
174    ///
175    /// Updates the parent field of the specified branch. Does not perform
176    /// any git operations - the caller is responsible for rebasing.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if the branch is not found or if the reparent
181    /// would create a cycle.
182    pub fn reparent(&mut self, branch: &str, new_parent: Option<&str>) -> crate::Result<()> {
183        // Check for cycle (only if new_parent is in the stack)
184        if let Some(parent) = new_parent
185            && self.would_create_cycle(branch, parent)
186        {
187            return Err(crate::error::Error::CyclicDependency(format!(
188                "reparenting '{branch}' to '{parent}' would create a cycle"
189            )));
190        }
191
192        // Find and update the branch
193        let branch_entry = self
194            .find_branch_mut(branch)
195            .ok_or_else(|| crate::error::Error::BranchNotFound(branch.to_string()))?;
196
197        // Parent comes from an existing branch name, so it should be valid
198        branch_entry.parent = new_parent.map(BranchName::new).transpose().map_err(|_| {
199            crate::error::Error::BranchNotFound(new_parent.unwrap_or_default().to_string())
200        })?;
201
202        Ok(())
203    }
204}
205
206impl Default for Stack {
207    fn default() -> Self {
208        Self::new()
209    }
210}
211
212/// A branch within a stack.
213///
214/// Branch names are validated at construction time to prevent:
215/// - Path traversal attacks (`../`)
216/// - Shell metacharacters (`$`, `;`, `|`, etc.)
217/// - Invalid git branch name characters
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct StackBranch {
220    /// Branch name (validated).
221    pub name: BranchName,
222
223    /// Parent branch name (None for root branches based on main/master).
224    pub parent: Option<BranchName>,
225
226    /// Associated PR number (if submitted).
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub pr: Option<u64>,
229
230    /// When this branch was added to the stack.
231    pub created: DateTime<Utc>,
232}
233
234impl StackBranch {
235    /// Create a new stack branch with pre-validated names.
236    #[must_use]
237    pub fn new(name: BranchName, parent: Option<BranchName>) -> Self {
238        Self {
239            name,
240            parent,
241            pr: None,
242            created: Utc::now(),
243        }
244    }
245
246    /// Create a new stack branch, validating the names.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if the branch name or parent name is invalid.
251    pub fn try_new(
252        name: impl Into<String>,
253        parent: Option<impl Into<String>>,
254    ) -> crate::Result<Self> {
255        let name = BranchName::new(name)?;
256        let parent = parent.map(BranchName::new).transpose()?;
257        Ok(Self::new(name, parent))
258    }
259}
260
261/// A branch that has been merged (for preserving history in PR comments).
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct MergedBranch {
264    /// Branch name.
265    pub name: BranchName,
266
267    /// Original parent branch name (preserved for ancestry chain).
268    #[serde(default)]
269    pub parent: Option<BranchName>,
270
271    /// PR number that was merged.
272    pub pr: u64,
273
274    /// When this branch was merged.
275    pub merged_at: DateTime<Utc>,
276}
277
278/// Synchronization state of a branch relative to its parent.
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(tag = "status", rename_all = "snake_case")]
281pub enum BranchState {
282    /// Branch is up-to-date with its parent.
283    Synced,
284
285    /// Parent has moved forward, branch needs rebase.
286    Diverged {
287        /// Number of commits the parent is ahead.
288        commits_behind: usize,
289    },
290
291    /// Rebase resulted in conflicts that need resolution.
292    Conflict {
293        /// Files with conflicts.
294        files: Vec<String>,
295    },
296
297    /// Parent branch was deleted or renamed.
298    Detached,
299}
300
301impl BranchState {
302    /// Check if the branch needs syncing.
303    #[must_use]
304    pub const fn needs_sync(&self) -> bool {
305        matches!(self, Self::Diverged { .. })
306    }
307
308    /// Check if the branch has conflicts.
309    #[must_use]
310    pub const fn has_conflicts(&self) -> bool {
311        matches!(self, Self::Conflict { .. })
312    }
313
314    /// Check if the branch is healthy (synced).
315    #[must_use]
316    pub const fn is_healthy(&self) -> bool {
317        matches!(self, Self::Synced)
318    }
319}
320
321#[cfg(test)]
322#[allow(clippy::unwrap_used)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_stack_operations() {
328        let mut stack = Stack::new();
329        assert!(stack.is_empty());
330
331        stack.add_branch(StackBranch::try_new("feature/auth", Some("main")).unwrap());
332        stack.add_branch(StackBranch::try_new("feature/auth-ui", Some("feature/auth")).unwrap());
333
334        assert_eq!(stack.len(), 2);
335        assert!(stack.find_branch("feature/auth").is_some());
336
337        let children = stack.children_of("feature/auth");
338        assert_eq!(children.len(), 1);
339        assert_eq!(children[0].name, "feature/auth-ui");
340    }
341
342    #[test]
343    fn test_ancestry() {
344        let mut stack = Stack::new();
345        stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
346        stack.add_branch(StackBranch::try_new("b", Some("a")).unwrap());
347        stack.add_branch(StackBranch::try_new("c", Some("b")).unwrap());
348
349        let ancestry = stack.ancestry("c");
350        assert_eq!(ancestry.len(), 3);
351        assert_eq!(ancestry[0].name, "a");
352        assert_eq!(ancestry[1].name, "b");
353        assert_eq!(ancestry[2].name, "c");
354    }
355
356    #[test]
357    fn test_descendants() {
358        let mut stack = Stack::new();
359        // Create tree: main → a → b → c
360        //                    ↘ d
361        stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
362        stack.add_branch(StackBranch::try_new("b", Some("a")).unwrap());
363        stack.add_branch(StackBranch::try_new("c", Some("b")).unwrap());
364        stack.add_branch(StackBranch::try_new("d", Some("a")).unwrap());
365
366        // Descendants of "a" should be b, c, d (in some order based on traversal)
367        let descendants = stack.descendants("a");
368        assert_eq!(descendants.len(), 3);
369        let names: Vec<&str> = descendants.iter().map(|b| b.name.as_str()).collect();
370        assert!(names.contains(&"b"));
371        assert!(names.contains(&"c"));
372        assert!(names.contains(&"d"));
373
374        // Descendants of "b" should only be c
375        let descendants = stack.descendants("b");
376        assert_eq!(descendants.len(), 1);
377        assert_eq!(descendants[0].name, "c");
378
379        // Descendants of "c" (leaf) should be empty
380        let descendants = stack.descendants("c");
381        assert!(descendants.is_empty());
382    }
383
384    #[test]
385    fn test_branch_state() {
386        assert!(BranchState::Synced.is_healthy());
387        assert!(!BranchState::Synced.needs_sync());
388
389        let diverged = BranchState::Diverged { commits_behind: 3 };
390        assert!(diverged.needs_sync());
391        assert!(!diverged.is_healthy());
392
393        let conflict = BranchState::Conflict {
394            files: vec!["src/main.rs".into()],
395        };
396        assert!(conflict.has_conflicts());
397    }
398
399    #[test]
400    fn test_would_create_cycle() {
401        let mut stack = Stack::new();
402        // Create tree: main → a → b → c
403        //                    ↘ d
404        stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
405        stack.add_branch(StackBranch::try_new("b", Some("a")).unwrap());
406        stack.add_branch(StackBranch::try_new("c", Some("b")).unwrap());
407        stack.add_branch(StackBranch::try_new("d", Some("a")).unwrap());
408
409        // Can't be your own parent
410        assert!(stack.would_create_cycle("a", "a"));
411        assert!(stack.would_create_cycle("b", "b"));
412
413        // Can't set parent to a descendant
414        assert!(stack.would_create_cycle("a", "b")); // b is child of a
415        assert!(stack.would_create_cycle("a", "c")); // c is grandchild of a
416        assert!(stack.would_create_cycle("a", "d")); // d is child of a
417        assert!(stack.would_create_cycle("b", "c")); // c is child of b
418
419        // These are valid reparents (no cycle)
420        assert!(!stack.would_create_cycle("c", "a")); // c can have a as parent
421        assert!(!stack.would_create_cycle("c", "d")); // c can have d as parent (sibling of b)
422        assert!(!stack.would_create_cycle("d", "b")); // d can have b as parent
423        assert!(!stack.would_create_cycle("b", "d")); // b can have d as parent (they're siblings)
424    }
425
426    #[test]
427    fn test_reparent() {
428        let mut stack = Stack::new();
429        // Create tree: main → a → b
430        //                    ↘ c
431        stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
432        stack.add_branch(StackBranch::try_new("b", Some("a")).unwrap());
433        stack.add_branch(StackBranch::try_new("c", Some("a")).unwrap());
434
435        // b's parent is a
436        assert_eq!(
437            stack
438                .find_branch("b")
439                .unwrap()
440                .parent
441                .as_ref()
442                .unwrap()
443                .as_str(),
444            "a"
445        );
446
447        // Reparent b to c
448        stack.reparent("b", Some("c")).unwrap();
449        assert_eq!(
450            stack
451                .find_branch("b")
452                .unwrap()
453                .parent
454                .as_ref()
455                .unwrap()
456                .as_str(),
457            "c"
458        );
459
460        // Verify tree is now: main → a → c → b
461        let ancestry = stack.ancestry("b");
462        assert_eq!(ancestry.len(), 3);
463        assert_eq!(ancestry[0].name, "a");
464        assert_eq!(ancestry[1].name, "c");
465        assert_eq!(ancestry[2].name, "b");
466    }
467
468    #[test]
469    fn test_reparent_to_none() {
470        let mut stack = Stack::new();
471        stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
472        stack.add_branch(StackBranch::try_new("b", Some("a")).unwrap());
473
474        // Reparent b to None (make it a root branch)
475        stack.reparent("b", None).unwrap();
476        assert!(stack.find_branch("b").unwrap().parent.is_none());
477    }
478
479    #[test]
480    fn test_reparent_cycle_error() {
481        let mut stack = Stack::new();
482        stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
483        stack.add_branch(StackBranch::try_new("b", Some("a")).unwrap());
484
485        // Should fail: can't make a's parent be b (its child)
486        let result = stack.reparent("a", Some("b"));
487        assert!(result.is_err());
488    }
489
490    #[test]
491    fn test_reparent_not_found() {
492        let mut stack = Stack::new();
493        stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
494
495        // Should fail: branch doesn't exist
496        let result = stack.reparent("nonexistent", Some("a"));
497        assert!(result.is_err());
498    }
499}