use crate::checkpoint::Checkpoint;
use crate::error::{Result, TitorError};
use serde::{Deserialize, Serialize};
use crate::collections::{HashMap, HashMapExt, HashSet, HashSetExt};
use std::collections::VecDeque;
use tracing::{debug, trace};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Timeline {
pub checkpoints: HashMap<String, Checkpoint>,
pub current_checkpoint_id: Option<String>,
pub roots: Vec<String>,
pub children: HashMap<String, Vec<String>>,
}
impl Timeline {
pub fn new() -> Self {
Self {
checkpoints: HashMap::new(),
current_checkpoint_id: None,
roots: Vec::new(),
children: HashMap::new(),
}
}
pub fn add_checkpoint(&mut self, checkpoint: Checkpoint) -> Result<()> {
let checkpoint_id = checkpoint.id.clone();
let parent_id = checkpoint.parent_id.clone();
if let Some(parent_id) = &parent_id {
if self.would_create_cycle(&checkpoint_id, parent_id) {
return Err(TitorError::CircularDependency);
}
}
self.checkpoints.insert(checkpoint_id.clone(), checkpoint);
if let Some(parent_id) = parent_id {
self.children
.entry(parent_id)
.or_insert_with(Vec::new)
.push(checkpoint_id.clone());
} else {
self.roots.push(checkpoint_id.clone());
}
if self.current_checkpoint_id.is_none() {
self.current_checkpoint_id = Some(checkpoint_id.clone());
}
debug!("Added checkpoint {} to timeline", &checkpoint_id[..8]);
Ok(())
}
pub fn remove_checkpoint(&mut self, checkpoint_id: &str) -> Result<()> {
if self.children.contains_key(checkpoint_id) {
return Err(TitorError::CheckpointHasChildren(checkpoint_id.to_string()));
}
let checkpoint = self.checkpoints
.get(checkpoint_id)
.ok_or_else(|| TitorError::CheckpointNotFound(checkpoint_id.to_string()))?
.clone();
self.checkpoints.remove(checkpoint_id);
if let Some(parent_id) = &checkpoint.parent_id {
if let Some(children) = self.children.get_mut(parent_id) {
children.retain(|id| id != checkpoint_id);
if children.is_empty() {
self.children.remove(parent_id);
}
}
} else {
self.roots.retain(|id| id != checkpoint_id);
}
if self.current_checkpoint_id.as_deref() == Some(checkpoint_id) {
self.current_checkpoint_id = checkpoint.parent_id;
}
debug!("Removed checkpoint {} from timeline", &checkpoint_id[..8]);
Ok(())
}
pub fn set_current(&mut self, checkpoint_id: &str) -> Result<()> {
if !self.checkpoints.contains_key(checkpoint_id) {
return Err(TitorError::CheckpointNotFound(checkpoint_id.to_string()));
}
self.current_checkpoint_id = Some(checkpoint_id.to_string());
trace!("Set current checkpoint to {}", &checkpoint_id[..8]);
Ok(())
}
pub fn current_checkpoint(&self) -> Option<&Checkpoint> {
self.current_checkpoint_id
.as_ref()
.and_then(|id| self.checkpoints.get(id))
}
pub fn get_checkpoint(&self, checkpoint_id: &str) -> Option<&Checkpoint> {
self.checkpoints.get(checkpoint_id)
}
pub fn get_children(&self, checkpoint_id: &str) -> Vec<&Checkpoint> {
self.children
.get(checkpoint_id)
.map(|child_ids| {
child_ids
.iter()
.filter_map(|id| self.checkpoints.get(id))
.collect()
})
.unwrap_or_default()
}
pub fn get_descendants(&self, checkpoint_id: &str) -> Vec<&Checkpoint> {
let mut descendants = Vec::new();
let mut queue = VecDeque::new();
if let Some(children) = self.children.get(checkpoint_id) {
queue.extend(children.iter());
}
while let Some(id) = queue.pop_front() {
if let Some(checkpoint) = self.checkpoints.get(id) {
descendants.push(checkpoint);
if let Some(children) = self.children.get(id) {
queue.extend(children.iter());
}
}
}
descendants
}
pub fn get_ancestors(&self, checkpoint_id: &str) -> Vec<&Checkpoint> {
let mut ancestors = Vec::new();
let mut current_id = checkpoint_id;
while let Some(checkpoint) = self.checkpoints.get(current_id) {
if let Some(parent_id) = &checkpoint.parent_id {
if let Some(parent) = self.checkpoints.get(parent_id) {
ancestors.push(parent);
current_id = parent_id;
} else {
break;
}
} else {
break;
}
}
ancestors.reverse();
ancestors
}
pub fn find_common_ancestor(&self, id1: &str, id2: &str) -> Option<&Checkpoint> {
let ancestors1: HashSet<_> = self.get_ancestors(id1)
.into_iter()
.map(|c| &c.id)
.collect();
let mut current_id = id2;
loop {
if ancestors1.contains(¤t_id.to_string()) {
return self.checkpoints.get(current_id);
}
let checkpoint = self.checkpoints.get(current_id)?;
match &checkpoint.parent_id {
Some(parent_id) => current_id = parent_id,
None => return None,
}
}
}
pub fn get_path(&self, from_id: &str, to_id: &str) -> Result<Vec<&Checkpoint>> {
if from_id == to_id {
return Ok(vec![]);
}
let from_ancestors = self.get_ancestors(from_id);
let to_ancestors = self.get_ancestors(to_id);
if from_ancestors.iter().any(|c| c.id == to_id) {
return Ok(from_ancestors
.into_iter()
.take_while(|c| c.id != to_id)
.chain(std::iter::once(self.checkpoints.get(to_id).unwrap()))
.collect());
}
if to_ancestors.iter().any(|c| c.id == from_id) {
return Ok(to_ancestors
.into_iter()
.skip_while(|c| c.id != from_id)
.skip(1)
.chain(std::iter::once(self.checkpoints.get(to_id).unwrap()))
.collect());
}
if let Some(common) = self.find_common_ancestor(from_id, to_id) {
let mut path = Vec::new();
let mut current_id = from_id;
while current_id != &common.id {
let checkpoint = self.checkpoints.get(current_id).unwrap();
path.push(checkpoint);
current_id = checkpoint.parent_id.as_ref().unwrap();
}
path.push(common);
let to_ancestors_from_common: Vec<_> = self.get_ancestors(to_id)
.into_iter()
.skip_while(|c| c.id != common.id)
.skip(1)
.collect();
path.extend(to_ancestors_from_common);
path.push(self.checkpoints.get(to_id).unwrap());
Ok(path)
} else {
Err(TitorError::internal(format!(
"No path found between {} and {}",
from_id, to_id
)))
}
}
fn would_create_cycle(&self, child_id: &str, parent_id: &str) -> bool {
if child_id == parent_id {
return true;
}
let mut current_id = parent_id;
let mut visited = HashSet::new();
while let Some(checkpoint) = self.checkpoints.get(current_id) {
if !visited.insert(current_id) {
return true;
}
if current_id == child_id {
return true;
}
match &checkpoint.parent_id {
Some(id) => current_id = id,
None => break,
}
}
false
}
pub fn stats(&self) -> TimelineStats {
let total_checkpoints = self.checkpoints.len();
let root_checkpoints = self.roots.len();
let leaf_checkpoints = self.checkpoints
.keys()
.filter(|id| !self.children.contains_key(*id))
.count();
let max_depth = self.roots
.iter()
.map(|root| self.calculate_depth(root))
.max()
.unwrap_or(0);
let total_branches = self.children
.values()
.filter(|children| children.len() > 1)
.count();
TimelineStats {
total_checkpoints,
root_checkpoints,
leaf_checkpoints,
max_depth,
total_branches,
}
}
fn calculate_depth(&self, checkpoint_id: &str) -> usize {
if let Some(children) = self.children.get(checkpoint_id) {
1 + children
.iter()
.map(|child| self.calculate_depth(child))
.max()
.unwrap_or(0)
} else {
1
}
}
pub fn to_tree_nodes(&self) -> Vec<TimelineNode> {
self.roots
.iter()
.map(|root_id| self.build_tree_node(root_id))
.collect()
}
fn build_tree_node(&self, checkpoint_id: &str) -> TimelineNode {
let checkpoint = self.checkpoints.get(checkpoint_id).unwrap();
let children = self.children
.get(checkpoint_id)
.map(|child_ids| {
child_ids
.iter()
.map(|id| self.build_tree_node(id))
.collect()
})
.unwrap_or_default();
TimelineNode {
checkpoint: checkpoint.clone(),
children,
is_current: self.current_checkpoint_id.as_deref() == Some(checkpoint_id),
}
}
}
#[derive(Debug, Clone)]
pub struct TimelineNode {
pub checkpoint: Checkpoint,
pub children: Vec<TimelineNode>,
pub is_current: bool,
}
impl TimelineNode {
pub fn format_tree(&self, prefix: &str, is_last: bool) -> String {
let mut result = String::new();
let connector = if is_last { "└── " } else { "├── " };
let marker = if self.is_current { "* " } else { "" };
result.push_str(prefix);
result.push_str(connector);
result.push_str(marker);
result.push_str(&self.checkpoint.display_format());
result.push('\n');
let extension = if is_last { " " } else { "│ " };
let child_prefix = format!("{}{}", prefix, extension);
for (i, child) in self.children.iter().enumerate() {
let is_last_child = i == self.children.len() - 1;
result.push_str(&child.format_tree(&child_prefix, is_last_child));
}
result
}
}
#[derive(Debug)]
pub struct TimelineStats {
pub total_checkpoints: usize,
pub root_checkpoints: usize,
pub leaf_checkpoints: usize,
pub max_depth: usize,
pub total_branches: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::checkpoint::CheckpointMetadataBuilder;
fn create_test_checkpoint(id: &str, parent_id: Option<String>) -> Checkpoint {
let mut checkpoint = Checkpoint::new(
parent_id,
Some(format!("Checkpoint {}", id)),
CheckpointMetadataBuilder::new().build(),
"merkle_root".to_string(),
);
checkpoint.id = id.to_string(); checkpoint
}
#[test]
fn test_timeline_basic_operations() {
let mut timeline = Timeline::new();
let root = create_test_checkpoint("root", None);
timeline.add_checkpoint(root).unwrap();
assert_eq!(timeline.checkpoints.len(), 1);
assert_eq!(timeline.roots.len(), 1);
assert_eq!(timeline.current_checkpoint_id, Some("root".to_string()));
let child = create_test_checkpoint("child", Some("root".to_string()));
timeline.add_checkpoint(child).unwrap();
assert_eq!(timeline.checkpoints.len(), 2);
assert_eq!(timeline.get_children("root").len(), 1);
}
#[test]
fn test_timeline_circular_dependency() {
let mut timeline = Timeline::new();
let c1 = create_test_checkpoint("c1", None);
let c2 = create_test_checkpoint("c2", Some("c1".to_string()));
timeline.add_checkpoint(c1).unwrap();
timeline.add_checkpoint(c2).unwrap();
let circular = create_test_checkpoint("c1", Some("c2".to_string()));
assert!(matches!(
timeline.add_checkpoint(circular),
Err(TitorError::CircularDependency)
));
}
#[test]
fn test_timeline_remove_checkpoint() {
let mut timeline = Timeline::new();
timeline.add_checkpoint(create_test_checkpoint("root", None)).unwrap();
timeline.add_checkpoint(create_test_checkpoint("child1", Some("root".to_string()))).unwrap();
timeline.add_checkpoint(create_test_checkpoint("child2", Some("root".to_string()))).unwrap();
assert!(matches!(
timeline.remove_checkpoint("root"),
Err(TitorError::CheckpointHasChildren(_))
));
timeline.remove_checkpoint("child1").unwrap();
assert_eq!(timeline.checkpoints.len(), 2);
assert_eq!(timeline.get_children("root").len(), 1);
}
#[test]
fn test_timeline_navigation() {
let mut timeline = Timeline::new();
timeline.add_checkpoint(create_test_checkpoint("root", None)).unwrap();
timeline.add_checkpoint(create_test_checkpoint("branch1", Some("root".to_string()))).unwrap();
timeline.add_checkpoint(create_test_checkpoint("leaf1", Some("branch1".to_string()))).unwrap();
timeline.add_checkpoint(create_test_checkpoint("branch2", Some("root".to_string()))).unwrap();
let ancestors = timeline.get_ancestors("leaf1");
assert_eq!(ancestors.len(), 2);
assert_eq!(ancestors[0].id, "root");
assert_eq!(ancestors[1].id, "branch1");
let descendants = timeline.get_descendants("root");
assert_eq!(descendants.len(), 3);
let common = timeline.find_common_ancestor("leaf1", "branch2").unwrap();
assert_eq!(common.id, "root");
let path = timeline.get_path("leaf1", "branch2").unwrap();
assert_eq!(path.len(), 4); }
#[test]
fn test_timeline_stats() {
let mut timeline = Timeline::new();
timeline.add_checkpoint(create_test_checkpoint("root", None)).unwrap();
timeline.add_checkpoint(create_test_checkpoint("b1", Some("root".to_string()))).unwrap();
timeline.add_checkpoint(create_test_checkpoint("b2", Some("root".to_string()))).unwrap();
timeline.add_checkpoint(create_test_checkpoint("l1", Some("b1".to_string()))).unwrap();
timeline.add_checkpoint(create_test_checkpoint("l2", Some("b1".to_string()))).unwrap();
let stats = timeline.stats();
assert_eq!(stats.total_checkpoints, 5);
assert_eq!(stats.root_checkpoints, 1);
assert_eq!(stats.leaf_checkpoints, 3); assert_eq!(stats.max_depth, 3); assert_eq!(stats.total_branches, 2); }
}