1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::BranchName;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Stack {
11 pub branches: Vec<StackBranch>,
13
14 #[serde(default, skip_serializing_if = "Vec::is_empty")]
16 pub merged: Vec<MergedBranch>,
17}
18
19impl Stack {
20 #[must_use]
22 pub const fn new() -> Self {
23 Self {
24 branches: Vec::new(),
25 merged: Vec::new(),
26 }
27 }
28
29 #[must_use]
31 pub fn find_branch(&self, name: &str) -> Option<&StackBranch> {
32 self.branches.iter().find(|b| b.name == name)
33 }
34
35 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 pub fn add_branch(&mut self, branch: StackBranch) {
42 self.branches.push(branch);
43 }
44
45 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 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 #[must_use]
76 pub fn find_merged(&self, name: &str) -> Option<&MergedBranch> {
77 self.merged.iter().find(|b| b.name == name)
78 }
79
80 #[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 pub fn clear_merged_if_empty(&mut self) {
91 if self.branches.is_empty() {
92 self.merged.clear();
93 }
94 }
95
96 #[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 #[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 #[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 #[must_use]
146 pub const fn is_empty(&self) -> bool {
147 self.branches.is_empty()
148 }
149
150 #[must_use]
152 pub const fn len(&self) -> usize {
153 self.branches.len()
154 }
155
156 #[must_use]
161 pub fn would_create_cycle(&self, branch: &str, new_parent: &str) -> bool {
162 if branch == new_parent {
164 return true;
165 }
166
167 self.descendants(branch)
169 .iter()
170 .any(|b| b.name == new_parent)
171 }
172
173 pub fn reparent(&mut self, branch: &str, new_parent: Option<&str>) -> crate::Result<()> {
183 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 let branch_entry = self
194 .find_branch_mut(branch)
195 .ok_or_else(|| crate::error::Error::BranchNotFound(branch.to_string()))?;
196
197 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#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct StackBranch {
220 pub name: BranchName,
222
223 pub parent: Option<BranchName>,
225
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub pr: Option<u64>,
229
230 pub created: DateTime<Utc>,
232}
233
234impl StackBranch {
235 #[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 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#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct MergedBranch {
264 pub name: BranchName,
266
267 #[serde(default)]
269 pub parent: Option<BranchName>,
270
271 pub pr: u64,
273
274 pub merged_at: DateTime<Utc>,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(tag = "status", rename_all = "snake_case")]
281pub enum BranchState {
282 Synced,
284
285 Diverged {
287 commits_behind: usize,
289 },
290
291 Conflict {
293 files: Vec<String>,
295 },
296
297 Detached,
299}
300
301impl BranchState {
302 #[must_use]
304 pub const fn needs_sync(&self) -> bool {
305 matches!(self, Self::Diverged { .. })
306 }
307
308 #[must_use]
310 pub const fn has_conflicts(&self) -> bool {
311 matches!(self, Self::Conflict { .. })
312 }
313
314 #[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 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 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 let descendants = stack.descendants("b");
376 assert_eq!(descendants.len(), 1);
377 assert_eq!(descendants[0].name, "c");
378
379 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 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 assert!(stack.would_create_cycle("a", "a"));
411 assert!(stack.would_create_cycle("b", "b"));
412
413 assert!(stack.would_create_cycle("a", "b")); assert!(stack.would_create_cycle("a", "c")); assert!(stack.would_create_cycle("a", "d")); assert!(stack.would_create_cycle("b", "c")); assert!(!stack.would_create_cycle("c", "a")); assert!(!stack.would_create_cycle("c", "d")); assert!(!stack.would_create_cycle("d", "b")); assert!(!stack.would_create_cycle("b", "d")); }
425
426 #[test]
427 fn test_reparent() {
428 let mut stack = Stack::new();
429 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 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 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 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 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 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 let result = stack.reparent("nonexistent", Some("a"));
497 assert!(result.is_err());
498 }
499}