// Copyright © 2021 The Radicle Link Contributors use std::collections::BTreeSet; use std::ops::ControlFlow; use git_ext::Oid; use radicle_dag::Dag; use crate::{ change, object, object::collaboration::Evaluate, signatures::ExtendedSignature, CollaborativeObject, Entry, EntryId, History, ObjectId, TypeName, }; #[derive(Debug, thiserror::Error)] pub enum EvaluateError { #[error("unable to initialize object: {0}")] Init(Box), #[error("invalid signature for entry '{0}'")] Signature(EntryId), #[error("root entry '{0}' missing from graph")] MissingRoot(EntryId), } /// The graph of changes for a particular collaborative object pub(super) struct ChangeGraph { object_id: ObjectId, graph: Dag, } impl ChangeGraph { /// Load the change graph from the underlying git store by walking /// backwards from references to the object pub(crate) fn load<'a, S>( storage: &S, tip_refs: impl Iterator + 'a, typename: &TypeName, oid: &ObjectId, ) -> Option where S: change::Storage, { log::debug!(target: "cob", "Loading object of type {typename} at {oid}"); let mut builder = GraphBuilder::default(); let mut edges_to_process: Vec<(Oid, Oid)> = Vec::new(); // Populate the initial set of edges_to_process from the refs we have for reference in tip_refs { log::trace!(target: "cob", "Loading object from reference '{}'", reference.name); match storage.load(reference.target.id) { Ok(change) => { let new_edges = builder.add_change(reference.target.id, change); edges_to_process.extend(new_edges); } Err(e) => { log::warn!( target: "cob", "Unable to load change from reference {}->{}: {e}", reference.name, reference.target.id, ); } } } // Process edges until we have no more to process while let Some((parent_commit_id, child_commit_id)) = edges_to_process.pop() { log::trace!( target: "cob", "Loading change parent='{}', child='{}'", parent_commit_id, child_commit_id ); match storage.load(parent_commit_id) { Ok(change) => { let new_edges = builder.add_change(parent_commit_id, change); edges_to_process.extend(new_edges); builder.add_edge(child_commit_id, parent_commit_id); } Err(e) => { log::warn!( target: "cob", "Unable to load change tree from commit {}: {e}", parent_commit_id, ); } } } builder.build(*oid) } /// Given a graph evaluate it to produce a collaborative object. This will /// filter out branches of the graph which do not have valid signatures. pub(crate) fn evaluate>( mut self, store: &S, ) -> Result, EvaluateError> { let root = *self.object_id; let root = self .graph .get(&root) .ok_or(EvaluateError::MissingRoot(root))?; if !root.valid_signatures() { return Err(EvaluateError::Signature(root.id)); } // Evaluate the root separately, since we can't have a COB without a valid root. // Then, traverse the graph starting from the root's dependents. let mut object = T::init(&root.value, store).map_err(|e| EvaluateError::Init(Box::new(e)))?; let children = Vec::from_iter(root.dependents.iter().cloned()); let manifest = root.manifest.clone(); let root = root.id; self.graph.prune(&children, |_, entry, siblings| { // Check the entry signatures are valid. if !entry.valid_signatures() { return ControlFlow::Break(()); } // Apply the entry to the state, and if there's an error, prune that branch. if object .apply(entry, siblings.map(|(k, n)| (k, &n.value)), store) .is_err() { return ControlFlow::Break(()); } ControlFlow::Continue(()) }); Ok(CollaborativeObject { manifest, object, history: History::new(root, self.graph), id: self.object_id, }) } /// Get the tips of the collaborative object pub(crate) fn tips(&self) -> BTreeSet { self.graph.tips().map(|(_, change)| *change.id()).collect() } pub(crate) fn number_of_nodes(&self) -> usize { self.graph.len() } } struct GraphBuilder { graph: Dag, } impl Default for GraphBuilder { fn default() -> Self { GraphBuilder { graph: Dag::new() } } } impl GraphBuilder { /// Add a change to the graph which we are building up, returning any edges /// corresponding to the parents of this node in the change graph fn add_change(&mut self, commit_id: Oid, change: Entry) -> Vec<(Oid, Oid)> { let resource = change.resource().copied(); let parents = change.parents.clone(); if !self.graph.contains(&commit_id) { self.graph.node(commit_id, change); } parents .into_iter() .filter_map(move |parent| { debug_assert_ne!(Some(parent), resource); if !self.graph.has_dependency(&commit_id, &parent) { Some((parent, commit_id)) } else { None } }) .collect() } fn add_edge(&mut self, child: Oid, parent: Oid) { self.graph.dependency(child, parent); } fn build(self, object_id: ObjectId) -> Option { if self.graph.roots().next().is_some() { Some(ChangeGraph { object_id, graph: self.graph, }) } else { None } } }