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:
parent
bda5d6f080
commit
489370dcbd
|
|
@ -86,12 +86,13 @@ impl change::Storage for git2::Repository {
|
||||||
type LoadError = error::Load;
|
type LoadError = error::Load;
|
||||||
|
|
||||||
type ObjectId = Oid;
|
type ObjectId = Oid;
|
||||||
type Resource = Oid;
|
type Parent = Oid;
|
||||||
type Signatures = ExtendedSignature;
|
type Signatures = ExtendedSignature;
|
||||||
|
|
||||||
fn store<Signer>(
|
fn store<Signer>(
|
||||||
&self,
|
&self,
|
||||||
resource: Self::Resource,
|
resource: Self::Parent,
|
||||||
|
parents: Vec<Self::Parent>,
|
||||||
signer: &Signer,
|
signer: &Signer,
|
||||||
spec: store::Template<Self::ObjectId>,
|
spec: store::Template<Self::ObjectId>,
|
||||||
) -> Result<Change, Self::StoreError>
|
) -> Result<Change, Self::StoreError>
|
||||||
|
|
@ -124,6 +125,7 @@ impl change::Storage for git2::Repository {
|
||||||
revision: revision.into(),
|
revision: revision.into(),
|
||||||
signature,
|
signature,
|
||||||
resource,
|
resource,
|
||||||
|
parents,
|
||||||
manifest,
|
manifest,
|
||||||
contents,
|
contents,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
|
@ -134,6 +136,11 @@ impl change::Storage for git2::Repository {
|
||||||
let commit = Commit::read(self, id.into())?;
|
let commit = Commit::read(self, id.into())?;
|
||||||
let timestamp = git2::Time::from(commit.committer().time).seconds() as u64;
|
let timestamp = git2::Time::from(commit.committer().time).seconds() as u64;
|
||||||
let resource = parse_resource_trailer(commit.trailers())?;
|
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)?
|
let mut signatures = Signatures::try_from(&commit)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
@ -153,6 +160,7 @@ impl change::Storage for git2::Repository {
|
||||||
revision: tree.id().into(),
|
revision: tree.id().into(),
|
||||||
signature: ExtendedSignature::new(key, sig),
|
signature: ExtendedSignature::new(key, sig),
|
||||||
resource,
|
resource,
|
||||||
|
parents,
|
||||||
manifest,
|
manifest,
|
||||||
contents,
|
contents,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,18 @@ pub trait Storage {
|
||||||
type LoadError: Error + Send + Sync + 'static;
|
type LoadError: Error + Send + Sync + 'static;
|
||||||
|
|
||||||
type ObjectId;
|
type ObjectId;
|
||||||
type Resource;
|
type Parent;
|
||||||
type Signatures;
|
type Signatures;
|
||||||
|
|
||||||
/// Store a new change.
|
/// Store a new change.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn store<G>(
|
fn store<G>(
|
||||||
&self,
|
&self,
|
||||||
authority: Self::Resource,
|
resource: Self::Parent,
|
||||||
|
parents: Vec<Self::Parent>,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
template: Template<Self::ObjectId>,
|
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
|
where
|
||||||
G: crypto::Signer;
|
G: crypto::Signer;
|
||||||
|
|
||||||
|
|
@ -38,7 +39,7 @@ pub trait Storage {
|
||||||
fn load(
|
fn load(
|
||||||
&self,
|
&self,
|
||||||
id: Self::ObjectId,
|
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.
|
/// 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,
|
/// The parent resource that this change lives under. For example,
|
||||||
/// this change could be for a patch of a project.
|
/// this change could be for a patch of a project.
|
||||||
pub resource: Resource,
|
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
|
/// The manifest describing the type of object as well as the type
|
||||||
/// of history for this `Change`.
|
/// of history for this `Change`.
|
||||||
pub manifest: Manifest,
|
pub manifest: Manifest,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ impl ChangeGraph {
|
||||||
oid: &ObjectId,
|
oid: &ObjectId,
|
||||||
) -> Option<ChangeGraph>
|
) -> Option<ChangeGraph>
|
||||||
where
|
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);
|
log::info!("loading object '{}' '{}'", typename, oid);
|
||||||
let mut builder = GraphBuilder::default();
|
let mut builder = GraphBuilder::default();
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ where
|
||||||
StoreError = git::change::error::Create,
|
StoreError = git::change::error::Create,
|
||||||
LoadError = git::change::error::Load,
|
LoadError = git::change::error::Load,
|
||||||
ObjectId = git_ext::Oid,
|
ObjectId = git_ext::Oid,
|
||||||
Resource = git_ext::Oid,
|
Parent = git_ext::Oid,
|
||||||
Signatures = ExtendedSignature,
|
Signatures = ExtendedSignature,
|
||||||
>,
|
>,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ where
|
||||||
{
|
{
|
||||||
let Create { ref typename, .. } = &args;
|
let Create { ref typename, .. } = &args;
|
||||||
let init_change = storage
|
let init_change = storage
|
||||||
.store(resource, signer, args.template())
|
.store(resource, vec![], signer, args.template())
|
||||||
.map_err(error::Create::from)?;
|
.map_err(error::Create::from)?;
|
||||||
let object_id = init_change.id().into();
|
let object_id = init_change.id().into();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,10 @@ pub struct Update {
|
||||||
/// The `signer` is expected to be a cryptographic signing key. This
|
/// The `signer` is expected to be a cryptographic signing key. This
|
||||||
/// ensures that the objects origin is cryptographically verifiable.
|
/// ensures that the objects origin is cryptographically verifiable.
|
||||||
///
|
///
|
||||||
/// The `resource` is the parent of this object, for example a
|
/// The `resource` is the resource this change lives under, eg. a project.
|
||||||
/// software project. Its content-address is stored in the
|
///
|
||||||
/// object's history.
|
/// 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
|
/// The `identifier` is a unqiue id that is passed through to the
|
||||||
/// [`crate::object::Storage`].
|
/// [`crate::object::Storage`].
|
||||||
|
|
@ -55,6 +56,7 @@ pub fn update<S, G>(
|
||||||
storage: &S,
|
storage: &S,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
resource: Oid,
|
resource: Oid,
|
||||||
|
parents: Vec<Oid>,
|
||||||
identifier: &S::Identifier,
|
identifier: &S::Identifier,
|
||||||
args: Update,
|
args: Update,
|
||||||
) -> Result<Updated, error::Update>
|
) -> Result<Updated, error::Update>
|
||||||
|
|
@ -80,6 +82,7 @@ where
|
||||||
|
|
||||||
let change = storage.store(
|
let change = storage.store(
|
||||||
resource,
|
resource,
|
||||||
|
parents,
|
||||||
signer,
|
signer,
|
||||||
change::Template {
|
change::Template {
|
||||||
tips: object.tips().iter().cloned().collect(),
|
tips: object.tips().iter().cloned().collect(),
|
||||||
|
|
|
||||||
|
|
@ -63,29 +63,30 @@ impl change::Storage for Storage {
|
||||||
type LoadError = <git2::Repository as change::Storage>::LoadError;
|
type LoadError = <git2::Repository as change::Storage>::LoadError;
|
||||||
|
|
||||||
type ObjectId = <git2::Repository as change::Storage>::ObjectId;
|
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;
|
type Signatures = <git2::Repository as change::Storage>::Signatures;
|
||||||
|
|
||||||
fn store<Signer>(
|
fn store<Signer>(
|
||||||
&self,
|
&self,
|
||||||
authority: Self::Resource,
|
authority: Self::Parent,
|
||||||
|
parents: Vec<Self::Parent>,
|
||||||
signer: &Signer,
|
signer: &Signer,
|
||||||
spec: change::Template<Self::ObjectId>,
|
spec: change::Template<Self::ObjectId>,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
change::store::Change<Self::Resource, Self::ObjectId, Self::Signatures>,
|
change::store::Change<Self::Parent, Self::ObjectId, Self::Signatures>,
|
||||||
Self::StoreError,
|
Self::StoreError,
|
||||||
>
|
>
|
||||||
where
|
where
|
||||||
Signer: crypto::Signer,
|
Signer: crypto::Signer,
|
||||||
{
|
{
|
||||||
self.as_raw().store(authority, signer, spec)
|
self.as_raw().store(authority, parents, signer, spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(
|
fn load(
|
||||||
&self,
|
&self,
|
||||||
id: Self::ObjectId,
|
id: Self::ObjectId,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
change::store::Change<Self::Resource, Self::ObjectId, Self::Signatures>,
|
change::store::Change<Self::Parent, Self::ObjectId, Self::Signatures>,
|
||||||
Self::LoadError,
|
Self::LoadError,
|
||||||
> {
|
> {
|
||||||
self.as_raw().load(id)
|
self.as_raw().load(id)
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,7 @@ fn update_cob() {
|
||||||
&storage,
|
&storage,
|
||||||
&signer,
|
&signer,
|
||||||
proj.project.content_id,
|
proj.project.content_id,
|
||||||
|
vec![],
|
||||||
&proj.identifier(),
|
&proj.identifier(),
|
||||||
Update {
|
Update {
|
||||||
changes: nonempty!(b"issue 1".to_vec()),
|
changes: nonempty!(b"issue 1".to_vec()),
|
||||||
|
|
@ -188,6 +189,7 @@ fn traverse_cobs() {
|
||||||
&storage,
|
&storage,
|
||||||
&neil_signer,
|
&neil_signer,
|
||||||
neil_proj.project.content_id,
|
neil_proj.project.content_id,
|
||||||
|
vec![],
|
||||||
&neil_proj.identifier(),
|
&neil_proj.identifier(),
|
||||||
Update {
|
Update {
|
||||||
changes: nonempty!(b"issue 2".to_vec()),
|
changes: nonempty!(b"issue 2".to_vec()),
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use crate::{
|
||||||
cob::{
|
cob::{
|
||||||
self,
|
self,
|
||||||
common::Timestamp,
|
common::Timestamp,
|
||||||
store::{self, FromHistory as _, Transaction},
|
store::{self, FromHistory as _, HistoryAction, Transaction},
|
||||||
},
|
},
|
||||||
identity::{doc::DocError, Did, Identity, IdentityError},
|
identity::{doc::DocError, Did, Identity, IdentityError},
|
||||||
prelude::{Doc, ReadRepository},
|
prelude::{Doc, ReadRepository},
|
||||||
|
|
@ -68,6 +68,8 @@ pub enum Action {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HistoryAction for Action {}
|
||||||
|
|
||||||
/// Error applying an operation onto a state.
|
/// Error applying an operation onto a state.
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ApplyError {
|
pub enum ApplyError {
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ use radicle_crdt::{LWWReg, LWWSet, Max, Semilattice};
|
||||||
|
|
||||||
use crate::cob;
|
use crate::cob;
|
||||||
use crate::cob::common::{Author, Reaction, Tag, Timestamp};
|
use crate::cob::common::{Author, Reaction, Tag, Timestamp};
|
||||||
use crate::cob::store::FromHistory as _;
|
|
||||||
use crate::cob::store::Transaction;
|
use crate::cob::store::Transaction;
|
||||||
|
use crate::cob::store::{FromHistory as _, HistoryAction};
|
||||||
use crate::cob::thread;
|
use crate::cob::thread;
|
||||||
use crate::cob::thread::{CommentId, Thread};
|
use crate::cob::thread::{CommentId, Thread};
|
||||||
use crate::cob::{store, ActorId, EntryId, ObjectId, TypeName};
|
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 {
|
impl From<thread::Action> for Action {
|
||||||
fn from(action: thread::Action) -> Self {
|
fn from(action: thread::Action) -> Self {
|
||||||
Self::Thread { action }
|
Self::Thread { action }
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ use radicle_crdt::{GMap, GSet, LWWReg, LWWSet, Lamport, Max, Redactable, Semilat
|
||||||
|
|
||||||
use crate::cob;
|
use crate::cob;
|
||||||
use crate::cob::common::{Author, Tag, Timestamp};
|
use crate::cob::common::{Author, Tag, Timestamp};
|
||||||
use crate::cob::store::FromHistory as _;
|
|
||||||
use crate::cob::store::Transaction;
|
use crate::cob::store::Transaction;
|
||||||
|
use crate::cob::store::{FromHistory as _, HistoryAction};
|
||||||
use crate::cob::thread;
|
use crate::cob::thread;
|
||||||
use crate::cob::thread::CommentId;
|
use crate::cob::thread::CommentId;
|
||||||
use crate::cob::thread::Thread;
|
use crate::cob::thread::Thread;
|
||||||
|
|
@ -119,6 +119,8 @@ pub enum Action {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HistoryAction for Action {}
|
||||||
|
|
||||||
/// Where a patch is intended to be merged.
|
/// Where a patch is intended to be merged.
|
||||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,19 @@ use crate::{cob, identity};
|
||||||
/// History type for standard radicle COBs.
|
/// History type for standard radicle COBs.
|
||||||
pub const HISTORY_TYPE: &str = "radicle";
|
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.
|
/// A type that can be materialized from an event history.
|
||||||
/// All collaborative objects implement this trait.
|
/// All collaborative objects implement this trait.
|
||||||
pub trait FromHistory: Sized + Default {
|
pub trait FromHistory: Sized + Default {
|
||||||
/// The underlying action composing each operation.
|
/// 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.
|
/// Error returned by `apply` function.
|
||||||
type Error: std::error::Error;
|
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.
|
/// Storage for collaborative objects of a specific type `T` in a single repository.
|
||||||
pub struct Store<'a, T> {
|
pub struct Store<'a, T> {
|
||||||
parent: git::Oid,
|
identity: git::Oid,
|
||||||
repo: &'a storage::Repository,
|
repo: &'a storage::Repository,
|
||||||
witness: PhantomData<T>,
|
witness: PhantomData<T>,
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +127,7 @@ impl<'a, T> Store<'a, T> {
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
repo,
|
repo,
|
||||||
parent: identity.head,
|
identity: identity.head,
|
||||||
witness: PhantomData,
|
witness: PhantomData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -137,11 +145,14 @@ where
|
||||||
actions: impl Into<NonEmpty<T::Action>>,
|
actions: impl Into<NonEmpty<T::Action>>,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> Result<Updated, Error> {
|
) -> 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(
|
let updated = cob::update(
|
||||||
self.repo,
|
self.repo,
|
||||||
signer,
|
signer,
|
||||||
self.parent,
|
self.identity,
|
||||||
|
parents,
|
||||||
signer.public_key(),
|
signer.public_key(),
|
||||||
Update {
|
Update {
|
||||||
object_id,
|
object_id,
|
||||||
|
|
@ -168,7 +179,7 @@ where
|
||||||
let cob = cob::create(
|
let cob = cob::create(
|
||||||
self.repo,
|
self.repo,
|
||||||
signer,
|
signer,
|
||||||
self.parent,
|
self.identity,
|
||||||
signer.public_key(),
|
signer.public_key(),
|
||||||
Create {
|
Create {
|
||||||
history_type: HISTORY_TYPE.to_owned(),
|
history_type: HISTORY_TYPE.to_owned(),
|
||||||
|
|
@ -304,7 +315,7 @@ impl<T: FromHistory> Transaction<T> {
|
||||||
let author = self.actor;
|
let author = self.actor;
|
||||||
let timestamp = object.history().timestamp().into();
|
let timestamp = object.history().timestamp().into();
|
||||||
let clock = self.clock.tick();
|
let clock = self.clock.tick();
|
||||||
let identity = store.parent;
|
let identity = store.identity;
|
||||||
|
|
||||||
// The history clock should be in sync with the tx clock.
|
// The history clock should be in sync with the tx clock.
|
||||||
assert_eq!(object.history().clock(), self.clock.get());
|
assert_eq!(object.history().clock(), self.clock.get());
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,8 @@ pub enum Action {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl cob::store::HistoryAction for Action {}
|
||||||
|
|
||||||
impl From<Action> for nonempty::NonEmpty<Action> {
|
impl From<Action> for nonempty::NonEmpty<Action> {
|
||||||
fn from(action: Action) -> Self {
|
fn from(action: Action) -> Self {
|
||||||
Self::new(action)
|
Self::new(action)
|
||||||
|
|
|
||||||
|
|
@ -41,19 +41,20 @@ impl change::Storage for Repository {
|
||||||
type LoadError = <git2::Repository as change::Storage>::LoadError;
|
type LoadError = <git2::Repository as change::Storage>::LoadError;
|
||||||
|
|
||||||
type ObjectId = <git2::Repository as change::Storage>::ObjectId;
|
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;
|
type Signatures = <git2::Repository as change::Storage>::Signatures;
|
||||||
|
|
||||||
fn store<Signer>(
|
fn store<Signer>(
|
||||||
&self,
|
&self,
|
||||||
authority: Self::Resource,
|
authority: Self::Parent,
|
||||||
|
parents: Vec<Self::Parent>,
|
||||||
signer: &Signer,
|
signer: &Signer,
|
||||||
spec: change::Template<Self::ObjectId>,
|
spec: change::Template<Self::ObjectId>,
|
||||||
) -> Result<cob::Change, Self::StoreError>
|
) -> Result<cob::Change, Self::StoreError>
|
||||||
where
|
where
|
||||||
Signer: crypto::Signer,
|
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> {
|
fn load(&self, id: Self::ObjectId) -> Result<cob::Change, Self::LoadError> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue