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 {