use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackEntry {
pub id: Uuid,
pub branch: String,
pub commit_hash: String,
pub message: String,
pub parent_id: Option<Uuid>,
pub children: Vec<Uuid>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub is_submitted: bool,
pub pull_request_id: Option<String>,
pub is_synced: bool,
#[serde(default)]
pub is_merged: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum StackStatus {
Clean,
Dirty,
OutOfSync,
Conflicted,
Rebasing,
NeedsSync,
Corrupted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Stack {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub base_branch: String,
pub working_branch: Option<String>,
pub entries: Vec<StackEntry>,
pub entry_map: HashMap<Uuid, StackEntry>,
pub status: StackStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub is_active: bool,
}
impl Stack {
pub fn new(name: String, base_branch: String, description: Option<String>) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
name,
description,
base_branch,
working_branch: None,
entries: Vec::new(),
entry_map: HashMap::new(),
status: StackStatus::Clean,
created_at: now,
updated_at: now,
is_active: false,
}
}
pub fn push_entry(&mut self, branch: String, commit_hash: String, message: String) -> Uuid {
let now = Utc::now();
let entry_id = Uuid::new_v4();
let parent_id = self.entries.last().map(|entry| entry.id);
let entry = StackEntry {
id: entry_id,
branch,
commit_hash,
message,
parent_id,
children: Vec::new(),
created_at: now,
updated_at: now,
is_submitted: false,
pull_request_id: None,
is_synced: false,
is_merged: false,
};
if let Some(parent_id) = parent_id {
if let Some(parent) = self.entry_map.get_mut(&parent_id) {
parent.children.push(entry_id);
}
}
self.entries.push(entry.clone());
self.entry_map.insert(entry_id, entry);
self.updated_at = now;
entry_id
}
pub fn pop_entry(&mut self) -> Option<StackEntry> {
if let Some(entry) = self.entries.pop() {
let entry_id = entry.id;
self.entry_map.remove(&entry_id);
if let Some(parent_id) = entry.parent_id {
if let Some(parent) = self.entry_map.get_mut(&parent_id) {
parent.children.retain(|&id| id != entry_id);
}
}
self.updated_at = Utc::now();
Some(entry)
} else {
None
}
}
pub fn remove_entry_at(&mut self, index: usize) -> Option<StackEntry> {
if index >= self.entries.len() {
return None;
}
let entry = self.entries.remove(index);
let entry_id = entry.id;
self.entry_map.remove(&entry_id);
for &child_id in &entry.children {
if let Some(child) = self.entry_map.get_mut(&child_id) {
child.parent_id = entry.parent_id;
}
}
if let Some(parent_id) = entry.parent_id {
if let Some(parent) = self.entry_map.get_mut(&parent_id) {
parent.children.retain(|&id| id != entry_id);
for &child_id in &entry.children {
if !parent.children.contains(&child_id) {
parent.children.push(child_id);
}
}
}
}
self.sync_entries_from_map();
self.updated_at = Utc::now();
Some(entry)
}
pub fn get_entry(&self, id: &Uuid) -> Option<&StackEntry> {
self.entry_map.get(id)
}
pub fn get_entry_mut(&mut self, id: &Uuid) -> Option<&mut StackEntry> {
self.entry_map.get_mut(id)
}
pub fn update_entry_commit_hash(
&mut self,
entry_id: &Uuid,
new_commit_hash: String,
) -> Result<(), String> {
let updated_in_vec = self
.entries
.iter_mut()
.find(|e| e.id == *entry_id)
.map(|entry| {
entry.commit_hash = new_commit_hash.clone();
})
.is_some();
let updated_in_map = self
.entry_map
.get_mut(entry_id)
.map(|entry| {
entry.commit_hash = new_commit_hash;
})
.is_some();
if updated_in_vec && updated_in_map {
Ok(())
} else {
Err(format!("Entry {} not found", entry_id))
}
}
pub fn get_base_entry(&self) -> Option<&StackEntry> {
self.entries.first()
}
pub fn get_top_entry(&self) -> Option<&StackEntry> {
self.entries.last()
}
pub fn get_children(&self, entry_id: &Uuid) -> Vec<&StackEntry> {
if let Some(entry) = self.get_entry(entry_id) {
entry
.children
.iter()
.filter_map(|id| self.get_entry(id))
.collect()
} else {
Vec::new()
}
}
pub fn get_parent(&self, entry_id: &Uuid) -> Option<&StackEntry> {
if let Some(entry) = self.get_entry(entry_id) {
entry
.parent_id
.and_then(|parent_id| self.get_entry(&parent_id))
} else {
None
}
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn mark_entry_submitted(&mut self, entry_id: &Uuid, pull_request_id: String) -> bool {
if let Some(entry) = self.get_entry_mut(entry_id) {
entry.is_submitted = true;
entry.pull_request_id = Some(pull_request_id);
entry.updated_at = Utc::now();
entry.is_merged = false;
self.updated_at = Utc::now();
self.sync_entries_from_map();
true
} else {
false
}
}
fn sync_entries_from_map(&mut self) {
for entry in &mut self.entries {
if let Some(updated_entry) = self.entry_map.get(&entry.id) {
*entry = updated_entry.clone();
}
}
}
pub fn repair_data_consistency(&mut self) {
self.sync_entries_from_map();
}
pub fn mark_entry_synced(&mut self, entry_id: &Uuid) -> bool {
if let Some(entry) = self.get_entry_mut(entry_id) {
entry.is_synced = true;
entry.updated_at = Utc::now();
self.updated_at = Utc::now();
self.sync_entries_from_map();
true
} else {
false
}
}
pub fn mark_entry_merged(&mut self, entry_id: &Uuid, merged: bool) -> bool {
if let Some(entry) = self.get_entry_mut(entry_id) {
entry.is_merged = merged;
entry.updated_at = Utc::now();
self.updated_at = Utc::now();
self.sync_entries_from_map();
true
} else {
false
}
}
pub fn update_status(&mut self, status: StackStatus) {
self.status = status;
self.updated_at = Utc::now();
}
pub fn set_active(&mut self, active: bool) {
self.is_active = active;
self.updated_at = Utc::now();
}
pub fn get_branch_names(&self) -> Vec<String> {
self.entries
.iter()
.map(|entry| entry.branch.clone())
.collect()
}
pub fn validate(&self) -> Result<String, String> {
if self.entries.is_empty() {
return Ok("Empty stack is valid".to_string());
}
for (i, entry) in self.entries.iter().enumerate() {
if i == 0 {
if entry.parent_id.is_some() {
return Err(format!(
"First entry {} should not have a parent",
entry.short_hash()
));
}
} else {
let expected_parent = &self.entries[i - 1];
if entry.parent_id != Some(expected_parent.id) {
return Err(format!(
"Entry {} has incorrect parent relationship",
entry.short_hash()
));
}
}
if let Some(parent_id) = entry.parent_id {
if !self.entry_map.contains_key(&parent_id) {
return Err(format!(
"Entry {} references non-existent parent {}",
entry.short_hash(),
parent_id
));
}
}
}
for entry in &self.entries {
if !self.entry_map.contains_key(&entry.id) {
return Err(format!(
"Entry {} is not in the entry map",
entry.short_hash()
));
}
}
let mut seen_ids = std::collections::HashSet::new();
for entry in &self.entries {
if !seen_ids.insert(entry.id) {
return Err(format!("Duplicate entry ID: {}", entry.id));
}
}
let mut seen_branches = std::collections::HashSet::new();
for entry in &self.entries {
if !seen_branches.insert(&entry.branch) {
return Err(format!("Duplicate branch name: {}", entry.branch));
}
}
Ok("Stack validation passed".to_string())
}
pub fn validate_git_integrity(
&self,
git_repo: &crate::git::GitRepository,
) -> Result<String, String> {
use tracing::warn;
let mut issues = Vec::new();
let mut warnings = Vec::new();
for entry in &self.entries {
if !git_repo.branch_exists(&entry.branch) {
issues.push(format!(
"Branch '{}' for entry {} does not exist",
entry.branch,
entry.short_hash()
));
continue;
}
match git_repo.get_branch_head(&entry.branch) {
Ok(branch_head) => {
if branch_head != entry.commit_hash {
issues.push(format!(
"Branch '{}' has diverged from stack metadata\n \
Expected commit: {} (from stack entry)\n \
Actual commit: {} (current branch HEAD)\n \
This commonly happens after 'ca entry amend' without --restack\n \
Run 'ca validate' and choose 'Incorporate' to update metadata",
entry.branch,
&entry.commit_hash[..8],
&branch_head[..8]
));
}
}
Err(e) => {
warnings.push(format!(
"Could not check branch '{}' HEAD: {}",
entry.branch, e
));
}
}
match git_repo.commit_exists(&entry.commit_hash) {
Ok(exists) => {
if !exists {
issues.push(format!(
"Commit {} for entry {} no longer exists",
entry.short_hash(),
entry.id
));
}
}
Err(e) => {
warnings.push(format!(
"Could not verify commit {} existence: {}",
entry.short_hash(),
e
));
}
}
}
for warning in &warnings {
warn!("{}", warning);
}
if !issues.is_empty() {
Err(format!(
"Git integrity validation failed:\n{}{}",
issues.join("\n"),
if !warnings.is_empty() {
format!("\n\nWarnings:\n{}", warnings.join("\n"))
} else {
String::new()
}
))
} else if !warnings.is_empty() {
Ok(format!(
"Git integrity validation passed with warnings:\n{}",
warnings.join("\n")
))
} else {
Ok("Git integrity validation passed".to_string())
}
}
}
impl StackEntry {
pub fn can_modify(&self) -> bool {
!self.is_submitted && !self.is_synced && !self.is_merged
}
pub fn short_hash(&self) -> String {
if self.commit_hash.len() >= 8 {
self.commit_hash[..8].to_string()
} else {
self.commit_hash.clone()
}
}
pub fn short_message(&self, max_len: usize) -> String {
let trimmed = self.message.trim();
if trimmed.len() > max_len {
format!("{}...", &trimmed[..max_len])
} else {
trimmed.to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_empty_stack() {
let stack = Stack::new(
"test-stack".to_string(),
"main".to_string(),
Some("Test stack description".to_string()),
);
assert_eq!(stack.name, "test-stack");
assert_eq!(stack.base_branch, "main");
assert_eq!(
stack.description,
Some("Test stack description".to_string())
);
assert!(stack.is_empty());
assert_eq!(stack.len(), 0);
assert_eq!(stack.status, StackStatus::Clean);
assert!(!stack.is_active);
}
#[test]
fn test_push_pop_entries() {
let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
let entry1_id = stack.push_entry(
"feature-1".to_string(),
"abc123".to_string(),
"Add feature 1".to_string(),
);
assert_eq!(stack.len(), 1);
assert!(!stack.is_empty());
let entry1 = stack.get_entry(&entry1_id).unwrap();
assert_eq!(entry1.branch, "feature-1");
assert_eq!(entry1.commit_hash, "abc123");
assert_eq!(entry1.message, "Add feature 1");
assert_eq!(entry1.parent_id, None);
assert!(entry1.children.is_empty());
let entry2_id = stack.push_entry(
"feature-2".to_string(),
"def456".to_string(),
"Add feature 2".to_string(),
);
assert_eq!(stack.len(), 2);
let entry2 = stack.get_entry(&entry2_id).unwrap();
assert_eq!(entry2.parent_id, Some(entry1_id));
let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
assert_eq!(updated_entry1.children, vec![entry2_id]);
let popped = stack.pop_entry().unwrap();
assert_eq!(popped.id, entry2_id);
assert_eq!(stack.len(), 1);
let updated_entry1 = stack.get_entry(&entry1_id).unwrap();
assert!(updated_entry1.children.is_empty());
}
#[test]
fn test_stack_navigation() {
let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
let entry1_id = stack.push_entry(
"branch1".to_string(),
"hash1".to_string(),
"msg1".to_string(),
);
let entry2_id = stack.push_entry(
"branch2".to_string(),
"hash2".to_string(),
"msg2".to_string(),
);
let entry3_id = stack.push_entry(
"branch3".to_string(),
"hash3".to_string(),
"msg3".to_string(),
);
assert_eq!(stack.get_base_entry().unwrap().id, entry1_id);
assert_eq!(stack.get_top_entry().unwrap().id, entry3_id);
assert_eq!(stack.get_parent(&entry2_id).unwrap().id, entry1_id);
assert_eq!(stack.get_parent(&entry3_id).unwrap().id, entry2_id);
assert!(stack.get_parent(&entry1_id).is_none());
let children_of_1 = stack.get_children(&entry1_id);
assert_eq!(children_of_1.len(), 1);
assert_eq!(children_of_1[0].id, entry2_id);
}
#[test]
fn test_stack_validation() {
let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
assert!(stack.validate().is_ok());
stack.push_entry(
"branch1".to_string(),
"hash1".to_string(),
"msg1".to_string(),
);
stack.push_entry(
"branch2".to_string(),
"hash2".to_string(),
"msg2".to_string(),
);
let result = stack.validate();
assert!(result.is_ok());
assert!(result.unwrap().contains("validation passed"));
}
#[test]
fn test_mark_entry_submitted() {
let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
let entry_id = stack.push_entry(
"branch1".to_string(),
"hash1".to_string(),
"msg1".to_string(),
);
assert!(!stack.get_entry(&entry_id).unwrap().is_submitted);
assert!(stack
.get_entry(&entry_id)
.unwrap()
.pull_request_id
.is_none());
assert!(stack.mark_entry_submitted(&entry_id, "PR-123".to_string()));
let entry = stack.get_entry(&entry_id).unwrap();
assert!(entry.is_submitted);
assert_eq!(entry.pull_request_id, Some("PR-123".to_string()));
}
#[test]
fn test_mark_entry_merged() {
let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
let entry_id = stack.push_entry(
"branch1".to_string(),
"hash1".to_string(),
"msg1".to_string(),
);
assert!(!stack.get_entry(&entry_id).unwrap().is_merged);
assert!(stack.mark_entry_merged(&entry_id, true));
let merged_entry = stack.get_entry(&entry_id).unwrap();
assert!(merged_entry.is_merged);
assert!(!merged_entry.can_modify());
assert!(stack.mark_entry_merged(&entry_id, false));
assert!(!stack.get_entry(&entry_id).unwrap().is_merged);
}
#[test]
fn test_branch_names() {
let mut stack = Stack::new("test".to_string(), "main".to_string(), None);
assert!(stack.get_branch_names().is_empty());
stack.push_entry(
"feature-1".to_string(),
"hash1".to_string(),
"msg1".to_string(),
);
stack.push_entry(
"feature-2".to_string(),
"hash2".to_string(),
"msg2".to_string(),
);
let branches = stack.get_branch_names();
assert_eq!(branches, vec!["feature-1", "feature-2"]);
}
}