From 489370dcbd5aa7d8d8b60fe53bb6274f66222378 Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Wed, 12 Apr 2023 13:31:54 +0200 Subject: [PATCH] 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. --- radicle-cob/src/backend/git/change.rs | 12 +++++++-- radicle-cob/src/change/store.rs | 11 +++++--- radicle-cob/src/change_graph.rs | 2 +- radicle-cob/src/lib.rs | 2 +- .../src/object/collaboration/create.rs | 2 +- .../src/object/collaboration/update.rs | 9 ++++--- radicle-cob/src/test/storage.rs | 11 ++++---- radicle-cob/src/tests.rs | 2 ++ radicle/src/cob/identity.rs | 4 ++- radicle/src/cob/issue.rs | 4 ++- radicle/src/cob/patch.rs | 4 ++- radicle/src/cob/store.rs | 25 +++++++++++++------ radicle/src/cob/thread.rs | 2 ++ radicle/src/storage/git/cob.rs | 7 +++--- 14 files changed, 67 insertions(+), 30 deletions(-) diff --git a/radicle-cob/src/backend/git/change.rs b/radicle-cob/src/backend/git/change.rs index fca190d8..6cdbe84a 100644 --- a/radicle-cob/src/backend/git/change.rs +++ b/radicle-cob/src/backend/git/change.rs @@ -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( &self, - resource: Self::Resource, + resource: Self::Parent, + parents: Vec, signer: &Signer, spec: store::Template, ) -> Result @@ -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::>(); @@ -153,6 +160,7 @@ impl change::Storage for git2::Repository { revision: tree.id().into(), signature: ExtendedSignature::new(key, sig), resource, + parents, manifest, contents, timestamp, diff --git a/radicle-cob/src/change/store.rs b/radicle-cob/src/change/store.rs index 85578cf9..934c4fb4 100644 --- a/radicle-cob/src/change/store.rs +++ b/radicle-cob/src/change/store.rs @@ -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( &self, - authority: Self::Resource, + resource: Self::Parent, + parents: Vec, signer: &G, template: Template, - ) -> Result, Self::StoreError> + ) -> Result, Self::StoreError> where G: crypto::Signer; @@ -38,7 +39,7 @@ pub trait Storage { fn load( &self, id: Self::ObjectId, - ) -> Result, Self::LoadError>; + ) -> Result, Self::LoadError>; } /// Change template, used to create a new change. @@ -62,6 +63,8 @@ pub struct Change { /// 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, /// The manifest describing the type of object as well as the type /// of history for this `Change`. pub manifest: Manifest, diff --git a/radicle-cob/src/change_graph.rs b/radicle-cob/src/change_graph.rs index a523cb25..db1b6e37 100644 --- a/radicle-cob/src/change_graph.rs +++ b/radicle-cob/src/change_graph.rs @@ -31,7 +31,7 @@ impl ChangeGraph { oid: &ObjectId, ) -> Option where - S: change::Storage, + S: change::Storage, { log::info!("loading object '{}' '{}'", typename, oid); let mut builder = GraphBuilder::default(); diff --git a/radicle-cob/src/lib.rs b/radicle-cob/src/lib.rs index 46ec66d3..b4508f30 100644 --- a/radicle-cob/src/lib.rs +++ b/radicle-cob/src/lib.rs @@ -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, >, { diff --git a/radicle-cob/src/object/collaboration/create.rs b/radicle-cob/src/object/collaboration/create.rs index 3393957c..c86f5e7a 100644 --- a/radicle-cob/src/object/collaboration/create.rs +++ b/radicle-cob/src/object/collaboration/create.rs @@ -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(); diff --git a/radicle-cob/src/object/collaboration/update.rs b/radicle-cob/src/object/collaboration/update.rs index 1fa3fb08..0739d46e 100644 --- a/radicle-cob/src/object/collaboration/update.rs +++ b/radicle-cob/src/object/collaboration/update.rs @@ -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( storage: &S, signer: &G, resource: Oid, + parents: Vec, identifier: &S::Identifier, args: Update, ) -> Result @@ -80,6 +82,7 @@ where let change = storage.store( resource, + parents, signer, change::Template { tips: object.tips().iter().cloned().collect(), diff --git a/radicle-cob/src/test/storage.rs b/radicle-cob/src/test/storage.rs index 896e7851..2fe83cd4 100644 --- a/radicle-cob/src/test/storage.rs +++ b/radicle-cob/src/test/storage.rs @@ -63,29 +63,30 @@ impl change::Storage for Storage { type LoadError = ::LoadError; type ObjectId = ::ObjectId; - type Resource = ::Resource; + type Parent = ::Parent; type Signatures = ::Signatures; fn store( &self, - authority: Self::Resource, + authority: Self::Parent, + parents: Vec, signer: &Signer, spec: change::Template, ) -> Result< - change::store::Change, + change::store::Change, 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, + change::store::Change, Self::LoadError, > { self.as_raw().load(id) diff --git a/radicle-cob/src/tests.rs b/radicle-cob/src/tests.rs index e8226e4b..63900473 100644 --- a/radicle-cob/src/tests.rs +++ b/radicle-cob/src/tests.rs @@ -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()), diff --git a/radicle/src/cob/identity.rs b/radicle/src/cob/identity.rs index 9fd88bde..9439677b 100644 --- a/radicle/src/cob/identity.rs +++ b/radicle/src/cob/identity.rs @@ -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 { diff --git a/radicle/src/cob/issue.rs b/radicle/src/cob/issue.rs index c9d23653..40eca7e3 100644 --- a/radicle/src/cob/issue.rs +++ b/radicle/src/cob/issue.rs @@ -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 for Action { fn from(action: thread::Action) -> Self { Self::Thread { action } diff --git a/radicle/src/cob/patch.rs b/radicle/src/cob/patch.rs index 05d8b3fe..4901a9a3 100644 --- a/radicle/src/cob/patch.rs +++ b/radicle/src/cob/patch.rs @@ -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")] diff --git a/radicle/src/cob/store.rs b/radicle/src/cob/store.rs index 99461dc8..98658a30 100644 --- a/radicle/src/cob/store.rs +++ b/radicle/src/cob/store.rs @@ -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 { + 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, } @@ -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>, signer: &G, ) -> Result { - 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 Transaction { 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()); diff --git a/radicle/src/cob/thread.rs b/radicle/src/cob/thread.rs index b560176e..cb2461c0 100644 --- a/radicle/src/cob/thread.rs +++ b/radicle/src/cob/thread.rs @@ -153,6 +153,8 @@ pub enum Action { }, } +impl cob::store::HistoryAction for Action {} + impl From for nonempty::NonEmpty { fn from(action: Action) -> Self { Self::new(action) diff --git a/radicle/src/storage/git/cob.rs b/radicle/src/storage/git/cob.rs index 8cebc3df..e2b24063 100644 --- a/radicle/src/storage/git/cob.rs +++ b/radicle/src/storage/git/cob.rs @@ -41,19 +41,20 @@ impl change::Storage for Repository { type LoadError = ::LoadError; type ObjectId = ::ObjectId; - type Resource = ::Resource; + type Parent = ::Parent; type Signatures = ::Signatures; fn store( &self, - authority: Self::Resource, + authority: Self::Parent, + parents: Vec, signer: &Signer, spec: change::Template, ) -> Result where Signer: crypto::Signer, { - self.backend.store(authority, signer, spec) + self.backend.store(authority, parents, signer, spec) } fn load(&self, id: Self::ObjectId) -> Result {