cob: Add ability to add more parents to change

This allows change commits to have more than one parent that isn't
part of the DAG.

We can use this to create dependencies towards git objects such as
commits, in the case of patch actions.
This commit is contained in:
Alexis Sellier 2023-04-12 13:31:54 +02:00
parent bda5d6f080
commit 489370dcbd
No known key found for this signature in database
14 changed files with 67 additions and 30 deletions

View File

@ -86,12 +86,13 @@ impl change::Storage for git2::Repository {
type LoadError = error::Load;
type ObjectId = Oid;
type Resource = Oid;
type Parent = Oid;
type Signatures = ExtendedSignature;
fn store<Signer>(
&self,
resource: Self::Resource,
resource: Self::Parent,
parents: Vec<Self::Parent>,
signer: &Signer,
spec: store::Template<Self::ObjectId>,
) -> Result<Change, Self::StoreError>
@ -124,6 +125,7 @@ impl change::Storage for git2::Repository {
revision: revision.into(),
signature,
resource,
parents,
manifest,
contents,
timestamp,
@ -134,6 +136,11 @@ impl change::Storage for git2::Repository {
let commit = Commit::read(self, id.into())?;
let timestamp = git2::Time::from(commit.committer().time).seconds() as u64;
let resource = parse_resource_trailer(commit.trailers())?;
let parents = commit
.parents()
.map(Oid::from)
.filter(|p| *p != resource)
.collect();
let mut signatures = Signatures::try_from(&commit)?
.into_iter()
.collect::<Vec<_>>();
@ -153,6 +160,7 @@ impl change::Storage for git2::Repository {
revision: tree.id().into(),
signature: ExtendedSignature::new(key, sig),
resource,
parents,
manifest,
contents,
timestamp,

View File

@ -19,17 +19,18 @@ pub trait Storage {
type LoadError: Error + Send + Sync + 'static;
type ObjectId;
type Resource;
type Parent;
type Signatures;
/// Store a new change.
#[allow(clippy::type_complexity)]
fn store<G>(
&self,
authority: Self::Resource,
resource: Self::Parent,
parents: Vec<Self::Parent>,
signer: &G,
template: Template<Self::ObjectId>,
) -> Result<Change<Self::Resource, Self::ObjectId, Self::Signatures>, Self::StoreError>
) -> Result<Change<Self::Parent, Self::ObjectId, Self::Signatures>, Self::StoreError>
where
G: crypto::Signer;
@ -38,7 +39,7 @@ pub trait Storage {
fn load(
&self,
id: Self::ObjectId,
) -> Result<Change<Self::Resource, Self::ObjectId, Self::Signatures>, Self::LoadError>;
) -> Result<Change<Self::Parent, Self::ObjectId, Self::Signatures>, Self::LoadError>;
}
/// Change template, used to create a new change.
@ -62,6 +63,8 @@ pub struct Change<Resource, Id, Signature> {
/// The parent resource that this change lives under. For example,
/// this change could be for a patch of a project.
pub resource: Resource,
/// Other parents this change depends on.
pub parents: Vec<Resource>,
/// The manifest describing the type of object as well as the type
/// of history for this `Change`.
pub manifest: Manifest,

View File

@ -31,7 +31,7 @@ impl ChangeGraph {
oid: &ObjectId,
) -> Option<ChangeGraph>
where
S: change::Storage<ObjectId = Oid, Resource = Oid, Signatures = ExtendedSignature>,
S: change::Storage<ObjectId = Oid, Parent = Oid, Signatures = ExtendedSignature>,
{
log::info!("loading object '{}' '{}'", typename, oid);
let mut builder = GraphBuilder::default();

View File

@ -128,7 +128,7 @@ where
StoreError = git::change::error::Create,
LoadError = git::change::error::Load,
ObjectId = git_ext::Oid,
Resource = git_ext::Oid,
Parent = git_ext::Oid,
Signatures = ExtendedSignature,
>,
{

View File

@ -64,7 +64,7 @@ where
{
let Create { ref typename, .. } = &args;
let init_change = storage
.store(resource, signer, args.template())
.store(resource, vec![], signer, args.template())
.map_err(error::Create::from)?;
let object_id = init_change.id().into();

View File

@ -42,9 +42,10 @@ pub struct Update {
/// The `signer` is expected to be a cryptographic signing key. This
/// ensures that the objects origin is cryptographically verifiable.
///
/// The `resource` is the parent of this object, for example a
/// software project. Its content-address is stored in the
/// object's history.
/// The `resource` is the resource this change lives under, eg. a project.
///
/// The `parents` are other the parents of this object, for example a
/// code commit.
///
/// The `identifier` is a unqiue id that is passed through to the
/// [`crate::object::Storage`].
@ -55,6 +56,7 @@ pub fn update<S, G>(
storage: &S,
signer: &G,
resource: Oid,
parents: Vec<Oid>,
identifier: &S::Identifier,
args: Update,
) -> Result<Updated, error::Update>
@ -80,6 +82,7 @@ where
let change = storage.store(
resource,
parents,
signer,
change::Template {
tips: object.tips().iter().cloned().collect(),

View File

@ -63,29 +63,30 @@ impl change::Storage for Storage {
type LoadError = <git2::Repository as change::Storage>::LoadError;
type ObjectId = <git2::Repository as change::Storage>::ObjectId;
type Resource = <git2::Repository as change::Storage>::Resource;
type Parent = <git2::Repository as change::Storage>::Parent;
type Signatures = <git2::Repository as change::Storage>::Signatures;
fn store<Signer>(
&self,
authority: Self::Resource,
authority: Self::Parent,
parents: Vec<Self::Parent>,
signer: &Signer,
spec: change::Template<Self::ObjectId>,
) -> Result<
change::store::Change<Self::Resource, Self::ObjectId, Self::Signatures>,
change::store::Change<Self::Parent, Self::ObjectId, Self::Signatures>,
Self::StoreError,
>
where
Signer: crypto::Signer,
{
self.as_raw().store(authority, signer, spec)
self.as_raw().store(authority, parents, signer, spec)
}
fn load(
&self,
id: Self::ObjectId,
) -> Result<
change::store::Change<Self::Resource, Self::ObjectId, Self::Signatures>,
change::store::Change<Self::Parent, Self::ObjectId, Self::Signatures>,
Self::LoadError,
> {
self.as_raw().load(id)

View File

@ -126,6 +126,7 @@ fn update_cob() {
&storage,
&signer,
proj.project.content_id,
vec![],
&proj.identifier(),
Update {
changes: nonempty!(b"issue 1".to_vec()),
@ -188,6 +189,7 @@ fn traverse_cobs() {
&storage,
&neil_signer,
neil_proj.project.content_id,
vec![],
&neil_proj.identifier(),
Update {
changes: nonempty!(b"issue 2".to_vec()),

View File

@ -13,7 +13,7 @@ use crate::{
cob::{
self,
common::Timestamp,
store::{self, FromHistory as _, Transaction},
store::{self, FromHistory as _, HistoryAction, Transaction},
},
identity::{doc::DocError, Did, Identity, IdentityError},
prelude::{Doc, ReadRepository},
@ -68,6 +68,8 @@ pub enum Action {
},
}
impl HistoryAction for Action {}
/// Error applying an operation onto a state.
#[derive(Error, Debug)]
pub enum ApplyError {

View File

@ -10,8 +10,8 @@ use radicle_crdt::{LWWReg, LWWSet, Max, Semilattice};
use crate::cob;
use crate::cob::common::{Author, Reaction, Tag, Timestamp};
use crate::cob::store::FromHistory as _;
use crate::cob::store::Transaction;
use crate::cob::store::{FromHistory as _, HistoryAction};
use crate::cob::thread;
use crate::cob::thread::{CommentId, Thread};
use crate::cob::{store, ActorId, EntryId, ObjectId, TypeName};
@ -534,6 +534,8 @@ pub enum Action {
},
}
impl HistoryAction for Action {}
impl From<thread::Action> for Action {
fn from(action: thread::Action) -> Self {
Self::Thread { action }

View File

@ -15,8 +15,8 @@ use radicle_crdt::{GMap, GSet, LWWReg, LWWSet, Lamport, Max, Redactable, Semilat
use crate::cob;
use crate::cob::common::{Author, Tag, Timestamp};
use crate::cob::store::FromHistory as _;
use crate::cob::store::Transaction;
use crate::cob::store::{FromHistory as _, HistoryAction};
use crate::cob::thread;
use crate::cob::thread::CommentId;
use crate::cob::thread::Thread;
@ -119,6 +119,8 @@ pub enum Action {
},
}
impl HistoryAction for Action {}
/// Where a patch is intended to be merged.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]

View File

@ -18,11 +18,19 @@ use crate::{cob, identity};
/// History type for standard radicle COBs.
pub const HISTORY_TYPE: &str = "radicle";
pub trait HistoryAction {
/// Parent objects this action depends on. For example, patch revisions
/// have the commit objects as their parent.
fn parents(&self) -> Vec<git::Oid> {
Vec::new()
}
}
/// A type that can be materialized from an event history.
/// All collaborative objects implement this trait.
pub trait FromHistory: Sized + Default {
/// The underlying action composing each operation.
type Action: for<'de> Deserialize<'de> + Serialize;
type Action: HistoryAction + for<'de> Deserialize<'de> + Serialize;
/// Error returned by `apply` function.
type Error: std::error::Error;
@ -101,7 +109,7 @@ pub enum Error {
/// Storage for collaborative objects of a specific type `T` in a single repository.
pub struct Store<'a, T> {
parent: git::Oid,
identity: git::Oid,
repo: &'a storage::Repository,
witness: PhantomData<T>,
}
@ -119,7 +127,7 @@ impl<'a, T> Store<'a, T> {
Ok(Self {
repo,
parent: identity.head,
identity: identity.head,
witness: PhantomData,
})
}
@ -137,11 +145,14 @@ where
actions: impl Into<NonEmpty<T::Action>>,
signer: &G,
) -> Result<Updated, Error> {
let changes = actions.into().try_map(encoding::encode)?;
let actions = actions.into();
let parents = actions.iter().flat_map(T::Action::parents).collect();
let changes = actions.try_map(encoding::encode)?;
let updated = cob::update(
self.repo,
signer,
self.parent,
self.identity,
parents,
signer.public_key(),
Update {
object_id,
@ -168,7 +179,7 @@ where
let cob = cob::create(
self.repo,
signer,
self.parent,
self.identity,
signer.public_key(),
Create {
history_type: HISTORY_TYPE.to_owned(),
@ -304,7 +315,7 @@ impl<T: FromHistory> Transaction<T> {
let author = self.actor;
let timestamp = object.history().timestamp().into();
let clock = self.clock.tick();
let identity = store.parent;
let identity = store.identity;
// The history clock should be in sync with the tx clock.
assert_eq!(object.history().clock(), self.clock.get());

View File

@ -153,6 +153,8 @@ pub enum Action {
},
}
impl cob::store::HistoryAction for Action {}
impl From<Action> for nonempty::NonEmpty<Action> {
fn from(action: Action) -> Self {
Self::new(action)

View File

@ -41,19 +41,20 @@ impl change::Storage for Repository {
type LoadError = <git2::Repository as change::Storage>::LoadError;
type ObjectId = <git2::Repository as change::Storage>::ObjectId;
type Resource = <git2::Repository as change::Storage>::Resource;
type Parent = <git2::Repository as change::Storage>::Parent;
type Signatures = <git2::Repository as change::Storage>::Signatures;
fn store<Signer>(
&self,
authority: Self::Resource,
authority: Self::Parent,
parents: Vec<Self::Parent>,
signer: &Signer,
spec: change::Template<Self::ObjectId>,
) -> Result<cob::Change, Self::StoreError>
where
Signer: crypto::Signer,
{
self.backend.store(authority, signer, spec)
self.backend.store(authority, parents, signer, spec)
}
fn load(&self, id: Self::ObjectId) -> Result<cob::Change, Self::LoadError> {