use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::BranchName;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Stack {
pub branches: Vec<StackBranch>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub merged: Vec<MergedBranch>,
}
impl Stack {
#[must_use]
pub const fn new() -> Self {
Self {
branches: Vec::new(),
merged: Vec::new(),
}
}
#[must_use]
pub fn find_branch(&self, name: &str) -> Option<&StackBranch> {
self.branches.iter().find(|b| b.name == name)
}
pub fn find_branch_mut(&mut self, name: &str) -> Option<&mut StackBranch> {
self.branches.iter_mut().find(|b| b.name == name)
}
pub fn add_branch(&mut self, branch: StackBranch) {
self.branches.push(branch);
}
pub fn remove_branch(&mut self, name: &str) -> Option<StackBranch> {
if let Some(pos) = self.branches.iter().position(|b| b.name == name) {
Some(self.branches.remove(pos))
} else {
None
}
}
pub fn mark_merged(&mut self, name: &str) -> Option<StackBranch> {
let branch = self.remove_branch(name)?;
if let Some(pr) = branch.pr {
self.merged.push(MergedBranch {
name: branch.name.clone(),
parent: branch.parent.clone(),
pr,
merged_at: Utc::now(),
});
}
Some(branch)
}
#[must_use]
pub fn find_merged(&self, name: &str) -> Option<&MergedBranch> {
self.merged.iter().find(|b| b.name == name)
}
#[must_use]
pub fn find_merged_by_pr(&self, pr: u64) -> Option<&MergedBranch> {
self.merged.iter().find(|b| b.pr == pr)
}
pub fn clear_merged_if_empty(&mut self) {
if self.branches.is_empty() {
self.merged.clear();
}
}
#[must_use]
pub fn children_of(&self, name: &str) -> Vec<&StackBranch> {
self.branches
.iter()
.filter(|b| b.parent.as_deref() == Some(name))
.collect()
}
#[must_use]
pub fn descendants(&self, name: &str) -> Vec<&StackBranch> {
let mut result = Vec::new();
let mut stack = vec![name];
while let Some(current_parent) = stack.pop() {
for branch in &self.branches {
if branch.parent.as_deref() == Some(current_parent) {
result.push(branch);
stack.push(&branch.name);
}
}
}
result
}
#[must_use]
pub fn ancestry(&self, name: &str) -> Vec<&StackBranch> {
let mut chain = vec![];
let mut current = name;
while let Some(branch) = self.find_branch(current) {
chain.push(branch);
match &branch.parent {
Some(parent) if self.find_branch(parent).is_some() => {
current = parent;
}
_ => break,
}
}
chain.reverse();
chain
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.branches.is_empty()
}
#[must_use]
pub const fn len(&self) -> usize {
self.branches.len()
}
#[must_use]
pub fn would_create_cycle(&self, branch: &str, new_parent: &str) -> bool {
if branch == new_parent {
return true;
}
self.descendants(branch)
.iter()
.any(|b| b.name == new_parent)
}
pub fn reparent(&mut self, branch: &str, new_parent: Option<&str>) -> crate::Result<()> {
if let Some(parent) = new_parent
&& self.would_create_cycle(branch, parent)
{
return Err(crate::error::Error::CyclicDependency(format!(
"reparenting '{branch}' to '{parent}' would create a cycle"
)));
}
let branch_entry = self
.find_branch_mut(branch)
.ok_or_else(|| crate::error::Error::BranchNotFound(branch.to_string()))?;
branch_entry.parent = new_parent.map(BranchName::new).transpose().map_err(|_| {
crate::error::Error::BranchNotFound(new_parent.unwrap_or_default().to_string())
})?;
Ok(())
}
}
impl Default for Stack {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackBranch {
pub name: BranchName,
pub parent: Option<BranchName>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pr: Option<u64>,
pub created: DateTime<Utc>,
}
impl StackBranch {
#[must_use]
pub fn new(name: BranchName, parent: Option<BranchName>) -> Self {
Self {
name,
parent,
pr: None,
created: Utc::now(),
}
}
pub fn try_new(
name: impl Into<String>,
parent: Option<impl Into<String>>,
) -> crate::Result<Self> {
let name = BranchName::new(name)?;
let parent = parent.map(BranchName::new).transpose()?;
Ok(Self::new(name, parent))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MergedBranch {
pub name: BranchName,
#[serde(default)]
pub parent: Option<BranchName>,
pub pr: u64,
pub merged_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum BranchState {
Synced,
Diverged {
commits_behind: usize,
},
Conflict {
files: Vec<String>,
},
Detached,
}
impl BranchState {
#[must_use]
pub const fn needs_sync(&self) -> bool {
matches!(self, Self::Diverged { .. })
}
#[must_use]
pub const fn has_conflicts(&self) -> bool {
matches!(self, Self::Conflict { .. })
}
#[must_use]
pub const fn is_healthy(&self) -> bool {
matches!(self, Self::Synced)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_stack_operations() {
let mut stack = Stack::new();
assert!(stack.is_empty());
stack.add_branch(StackBranch::try_new("feature/auth", Some("main")).unwrap());
stack.add_branch(StackBranch::try_new("feature/auth-ui", Some("feature/auth")).unwrap());
assert_eq!(stack.len(), 2);
assert!(stack.find_branch("feature/auth").is_some());
let children = stack.children_of("feature/auth");
assert_eq!(children.len(), 1);
assert_eq!(children[0].name, "feature/auth-ui");
}
#[test]
fn test_ancestry() {
let mut stack = Stack::new();
stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
stack.add_branch(StackBranch::try_new("b", Some("a")).unwrap());
stack.add_branch(StackBranch::try_new("c", Some("b")).unwrap());
let ancestry = stack.ancestry("c");
assert_eq!(ancestry.len(), 3);
assert_eq!(ancestry[0].name, "a");
assert_eq!(ancestry[1].name, "b");
assert_eq!(ancestry[2].name, "c");
}
#[test]
fn test_descendants() {
let mut stack = Stack::new();
stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
stack.add_branch(StackBranch::try_new("b", Some("a")).unwrap());
stack.add_branch(StackBranch::try_new("c", Some("b")).unwrap());
stack.add_branch(StackBranch::try_new("d", Some("a")).unwrap());
let descendants = stack.descendants("a");
assert_eq!(descendants.len(), 3);
let names: Vec<&str> = descendants.iter().map(|b| b.name.as_str()).collect();
assert!(names.contains(&"b"));
assert!(names.contains(&"c"));
assert!(names.contains(&"d"));
let descendants = stack.descendants("b");
assert_eq!(descendants.len(), 1);
assert_eq!(descendants[0].name, "c");
let descendants = stack.descendants("c");
assert!(descendants.is_empty());
}
#[test]
fn test_branch_state() {
assert!(BranchState::Synced.is_healthy());
assert!(!BranchState::Synced.needs_sync());
let diverged = BranchState::Diverged { commits_behind: 3 };
assert!(diverged.needs_sync());
assert!(!diverged.is_healthy());
let conflict = BranchState::Conflict {
files: vec!["src/main.rs".into()],
};
assert!(conflict.has_conflicts());
}
#[test]
fn test_would_create_cycle() {
let mut stack = Stack::new();
stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
stack.add_branch(StackBranch::try_new("b", Some("a")).unwrap());
stack.add_branch(StackBranch::try_new("c", Some("b")).unwrap());
stack.add_branch(StackBranch::try_new("d", Some("a")).unwrap());
assert!(stack.would_create_cycle("a", "a"));
assert!(stack.would_create_cycle("b", "b"));
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")); }
#[test]
fn test_reparent() {
let mut stack = Stack::new();
stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
stack.add_branch(StackBranch::try_new("b", Some("a")).unwrap());
stack.add_branch(StackBranch::try_new("c", Some("a")).unwrap());
assert_eq!(
stack
.find_branch("b")
.unwrap()
.parent
.as_ref()
.unwrap()
.as_str(),
"a"
);
stack.reparent("b", Some("c")).unwrap();
assert_eq!(
stack
.find_branch("b")
.unwrap()
.parent
.as_ref()
.unwrap()
.as_str(),
"c"
);
let ancestry = stack.ancestry("b");
assert_eq!(ancestry.len(), 3);
assert_eq!(ancestry[0].name, "a");
assert_eq!(ancestry[1].name, "c");
assert_eq!(ancestry[2].name, "b");
}
#[test]
fn test_reparent_to_none() {
let mut stack = Stack::new();
stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
stack.add_branch(StackBranch::try_new("b", Some("a")).unwrap());
stack.reparent("b", None).unwrap();
assert!(stack.find_branch("b").unwrap().parent.is_none());
}
#[test]
fn test_reparent_cycle_error() {
let mut stack = Stack::new();
stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
stack.add_branch(StackBranch::try_new("b", Some("a")).unwrap());
let result = stack.reparent("a", Some("b"));
assert!(result.is_err());
}
#[test]
fn test_reparent_not_found() {
let mut stack = Stack::new();
stack.add_branch(StackBranch::try_new("a", Some("main")).unwrap());
let result = stack.reparent("nonexistent", Some("a"));
assert!(result.is_err());
}
}