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<dyn std::error::Error + Sync + Send + 'static>),
#[error("invalid signature for entry '{0}'")]
Signature(EntryId),
#[error("root entry '{0}' missing from graph")]
MissingRoot(EntryId),
}
pub(super) struct ChangeGraph {
object_id: ObjectId,
graph: Dag<Oid, Entry>,
}
impl ChangeGraph {
pub(crate) fn load<'a, S>(
storage: &S,
tip_refs: impl Iterator<Item = &'a object::Reference> + 'a,
typename: &TypeName,
oid: &ObjectId,
) -> Option<ChangeGraph>
where
S: change::Storage<ObjectId = Oid, Parent = Oid, Signatures = ExtendedSignature>,
{
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();
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,
);
}
}
}
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)
}
pub(crate) fn evaluate<S, T: Evaluate<S>>(
mut self,
store: &S,
) -> Result<CollaborativeObject<T>, 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));
}
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| {
if !entry.valid_signatures() {
return ControlFlow::Break(());
}
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,
})
}
pub(crate) fn tips(&self) -> BTreeSet<Oid> {
self.graph.tips().map(|(_, change)| *change.id()).collect()
}
pub(crate) fn number_of_nodes(&self) -> usize {
self.graph.len()
}
}
struct GraphBuilder {
graph: Dag<Oid, Entry>,
}
impl Default for GraphBuilder {
fn default() -> Self {
GraphBuilder { graph: Dag::new() }
}
}
impl GraphBuilder {
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<ChangeGraph> {
if self.graph.roots().next().is_some() {
Some(ChangeGraph {
object_id,
graph: self.graph,
})
} else {
None
}
}
}