diff --git a/radicle-cob/src/change_graph.rs b/radicle-cob/src/change_graph.rs index d7d1b055..f21f8482 100644 --- a/radicle-cob/src/change_graph.rs +++ b/radicle-cob/src/change_graph.rs @@ -113,11 +113,12 @@ impl ChangeGraph { let child_commits = outgoing_edges .map(|e| *self.graph[e.target()].id()) .collect::>(); - (node, child_commits) + + (node, idx, child_commits) }); let history = { let root_change = &self.graph[*root]; - evaluate(*root_change.id(), items) + evaluate(*root_change.id(), &self.graph, items) }; CollaborativeObject { typename, diff --git a/radicle-cob/src/change_graph/evaluation.rs b/radicle-cob/src/change_graph/evaluation.rs index b7716956..caf6a819 100644 --- a/radicle-cob/src/change_graph/evaluation.rs +++ b/radicle-cob/src/change_graph/evaluation.rs @@ -6,7 +6,9 @@ use std::{collections::HashMap, ops::ControlFlow}; use git_ext::Oid; +use petgraph::{visit::EdgeRef, EdgeDirection}; +use crate::history::entry::{EntryId, EntryWithClock}; use crate::{change::Change, history, pruning_fold}; /// # Panics @@ -14,11 +16,13 @@ use crate::{change::Change, history, pruning_fold}; /// If the change corresponding to the root OID is not in `items` pub fn evaluate<'b>( root: Oid, - items: impl Iterator)>, + graph: &petgraph::Graph, + items: impl Iterator, Vec)>, ) -> history::History { let entries = pruning_fold::pruning_fold( - HashMap::new(), - items.map(|(change, children)| ChangeWithChildren { + HashMap::::new(), + items.map(|(change, idx, children)| ChangeWithChildren { + idx, change, child_commits: children, }), @@ -31,8 +35,18 @@ pub fn evaluate<'b>( ControlFlow::Break(entries) } Ok(entry) => { + // Get parent commits and calculate this node's clock based on theirs. + let incoming = graph.edges_directed(c.idx, EdgeDirection::Incoming); + let clock = incoming + .into_iter() + .map(|e| entries[&graph[e.source()].id.into()].clock()) + .max() + .map(|n| n + 1) + .unwrap_or_default(); log::trace!("change '{}' accepted", c.change.id()); - entries.insert((*c.change.id()).into(), entry); + + entries.insert(*entry.id(), EntryWithClock { entry, clock }); + ControlFlow::Continue(entries) } }, @@ -60,6 +74,7 @@ fn evaluate_change( } struct ChangeWithChildren<'a> { + idx: petgraph::graph::NodeIndex, change: &'a Change, child_commits: Vec, } diff --git a/radicle-cob/src/history.rs b/radicle-cob/src/history.rs index 2a2978e0..9c83c515 100644 --- a/radicle-cob/src/history.rs +++ b/radicle-cob/src/history.rs @@ -14,7 +14,7 @@ use petgraph::visit::Walker as _; use crate::pruning_fold; pub mod entry; -pub use entry::{Contents, Entry, EntryId}; +pub use entry::{Clock, Contents, Entry, EntryId, EntryWithClock}; #[derive( Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, @@ -29,7 +29,7 @@ pub enum HistoryType { /// The DAG of changes making up the history of a collaborative object. #[derive(Clone, Debug)] pub struct History { - graph: petgraph::Graph, + graph: petgraph::Graph, indices: HashMap>, } @@ -66,12 +66,12 @@ impl History { contents, }; let mut entries = HashMap::new(); - entries.insert(id, root_entry.clone()); - let NewGraph { graph, indices } = create_petgraph(&root_entry.id, &entries); + entries.insert(id, EntryWithClock::from(root_entry)); + let NewGraph { graph, indices } = create_petgraph(&id, &entries); Self { graph, indices } } - pub fn new(root: Id, entries: HashMap) -> Result + pub fn new(root: Id, entries: HashMap) -> Result where Id: Into, { @@ -84,6 +84,16 @@ impl History { } } + /// Get the current value of the logical clock. + /// This is the maximum value of all tips. + pub fn clock(&self) -> Clock { + self.graph + .externals(petgraph::Direction::Outgoing) + .map(|n| self.graph[n].clock) + .max() + .unwrap_or_default() + } + /// A topological (parents before children) traversal of the dependency /// graph of this history. This is analagous to /// [`std::iter::Iterator::fold`] in that it folds every change into an @@ -92,13 +102,13 @@ impl History { /// `ControlFlow::Break`. pub fn traverse(&self, init: A, f: F) -> A where - F: for<'r> FnMut(A, &'r Entry) -> ControlFlow, + F: for<'r> FnMut(A, &'r EntryWithClock) -> ControlFlow, { let topo = petgraph::visit::Topo::new(&self.graph); #[allow(clippy::let_and_return)] let items = topo.iter(&self.graph).map(|idx| { - let node = &self.graph[idx]; - node + let entry = &self.graph[idx]; + entry }); pruning_fold::pruning_fold(init, items, f) } @@ -131,7 +141,10 @@ impl History { std::iter::empty::(), new_contents, ); - let new_ix = self.graph.add_node(new_entry); + let new_ix = self.graph.add_node(EntryWithClock { + entry: new_entry, + clock: self.clock() + 1, + }); for tip in tips { let tip_ix = self.indices.get(&tip.into()).unwrap(); self.graph.update_edge(*tip_ix, new_ix, ()); @@ -140,11 +153,14 @@ impl History { } struct NewGraph { - graph: petgraph::Graph, + graph: petgraph::Graph, indices: HashMap>, } -fn create_petgraph<'a>(root: &'a EntryId, entries: &'a HashMap) -> NewGraph { +fn create_petgraph<'a>( + root: &'a EntryId, + entries: &'a HashMap, +) -> NewGraph { let mut graph = petgraph::Graph::new(); let mut indices = HashMap::>::new(); let root = entries.get(root).unwrap().clone(); @@ -153,8 +169,8 @@ fn create_petgraph<'a>(root: &'a EntryId, entries: &'a HashMap) let mut to_process = vec![root]; while let Some(entry) = to_process.pop() { let entry_ix = indices[&entry.id]; - for child_id in entry.children { - let child = entries[&child_id].clone(); + for child_id in entry.children() { + let child = entries[child_id].clone(); let child_ix = graph.add_node(child.clone()); indices.insert(child.id, child_ix); graph.update_edge(entry_ix, child_ix, ()); diff --git a/radicle-cob/src/history/entry.rs b/radicle-cob/src/history/entry.rs index 2fbc03e0..7fbc17be 100644 --- a/radicle-cob/src/history/entry.rs +++ b/radicle-cob/src/history/entry.rs @@ -11,6 +11,9 @@ use crate::pruning_fold; /// This is the change payload. pub type Contents = Vec; +/// Logical clock used to track causality in change graph. +pub type Clock = u64; + /// A unique identifier for a history entry. #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, PartialOrd, Ord)] pub struct EntryId(Oid); @@ -107,3 +110,46 @@ impl pruning_fold::GraphNode for Entry { &self.children } } + +/// Wraps an [`Entry`], adding a logical clock to it. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct EntryWithClock { + pub entry: Entry, + pub clock: Clock, +} + +impl From for EntryWithClock { + fn from(entry: Entry) -> Self { + Self { + entry, + clock: Clock::default(), + } + } +} + +impl EntryWithClock { + /// Get the clock value. + pub fn clock(&self) -> Clock { + self.clock + } +} + +impl pruning_fold::GraphNode for EntryWithClock { + type Id = EntryId; + + fn id(&self) -> &Self::Id { + &self.entry.id + } + + fn child_ids(&self) -> &[Self::Id] { + &self.entry.children + } +} + +impl std::ops::Deref for EntryWithClock { + type Target = Entry; + + fn deref(&self) -> &Self::Target { + &self.entry + } +}