cob: Add new history type, simplify `Content`

Keep the history type in the manifest, but no need to specify it
for all individual changes.

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-11-24 11:48:53 +01:00
parent 8dc655758e
commit 5526eceea0
No known key found for this signature in database
12 changed files with 80 additions and 99 deletions

View File

@ -96,13 +96,14 @@ impl change::Storage for git2::Repository {
{ {
let change::Create { let change::Create {
typename, typename,
history_type,
tips, tips,
message, message,
contents, contents,
} = spec; } = spec;
let manifest = store::Manifest { let manifest = store::Manifest {
typename, typename,
history_type: (&contents).into(), history_type,
}; };
let revision = write_manifest(self, &manifest, &contents)?; let revision = write_manifest(self, &manifest, &contents)?;
@ -200,7 +201,7 @@ fn load_contents(
manifest: &store::Manifest, manifest: &store::Manifest,
) -> Result<entry::Contents, error::Load> { ) -> Result<entry::Contents, error::Load> {
Ok(match manifest.history_type { Ok(match manifest.history_type {
HistoryType::Automerge => { HistoryType::Default | HistoryType::Automerge => {
let contents_tree_entry = tree let contents_tree_entry = tree
.get_name(CHANGE_BLOB_NAME) .get_name(CHANGE_BLOB_NAME)
.ok_or_else(|| error::Load::NoChange(tree.id().into()))?; .ok_or_else(|| error::Load::NoChange(tree.id().into()))?;
@ -208,7 +209,7 @@ fn load_contents(
let contents_blob = contents_object let contents_blob = contents_object
.as_blob() .as_blob()
.ok_or_else(|| error::Load::ChangeNotBlob(tree.id().into()))?; .ok_or_else(|| error::Load::ChangeNotBlob(tree.id().into()))?;
entry::Contents::automerge(contents_blob.content()) contents_blob.content().to_owned()
} }
}) })
} }

View File

@ -47,6 +47,7 @@ pub trait Storage {
pub struct Create<Id> { pub struct Create<Id> {
pub typename: TypeName, pub typename: TypeName,
pub history_type: HistoryType,
pub tips: Vec<Id>, pub tips: Vec<Id>,
pub message: String, pub message: String,
pub contents: Contents, pub contents: Contents,

View File

@ -16,14 +16,17 @@ use crate::pruning_fold;
pub mod entry; pub mod entry;
pub use entry::{Contents, Entry, EntryId}; pub use entry::{Contents, Entry, EntryId};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum HistoryType { pub enum HistoryType {
#[default]
Default,
Automerge, Automerge,
} }
/// The DAG of changes making up the history of a collaborative object. /// The DAG of changes making up the history of a collaborative object.
///
/// The `Author` represents the content address for the author of the change entry.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct History { pub struct History {
graph: petgraph::Graph<Entry, (), petgraph::Directed, u32>, graph: petgraph::Graph<Entry, (), petgraph::Directed, u32>,

View File

@ -7,34 +7,9 @@ use git_ext::Oid;
use crate::pruning_fold; use crate::pruning_fold;
use super::HistoryType; /// Entry contents.
/// This is the change payload.
#[derive(Clone, Debug, PartialEq, Hash, Eq)] pub type Contents = Vec<u8>;
pub enum Contents {
Automerge(Vec<u8>),
}
impl Contents {
pub fn automerge(bytes: &[u8]) -> Self {
Self::Automerge(bytes.to_vec())
}
}
impl From<&Contents> for HistoryType {
fn from(c: &Contents) -> Self {
match c {
Contents::Automerge(..) => HistoryType::Automerge,
}
}
}
impl AsRef<[u8]> for Contents {
fn as_ref(&self) -> &[u8] {
match self {
Self::Automerge(bytes) => bytes,
}
}
}
/// A unique identifier for a history entry. /// A unique identifier for a history entry.
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, PartialOrd, Ord)]

View File

@ -3,7 +3,7 @@
// This file is part of radicle-link, distributed under the GPLv3 with Radicle // This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file. // Linking Exception. For full terms see the included LICENSE file.
use crate::Store; use crate::{HistoryType, Store};
use super::*; use super::*;
@ -11,6 +11,8 @@ use super::*;
pub struct Create<Author> { pub struct Create<Author> {
/// The identity of the author for this object's first change. /// The identity of the author for this object's first change.
pub author: Option<Author>, pub author: Option<Author>,
/// The type of history that will be used for this object.
pub history_type: HistoryType,
/// The CRDT history to initialize this object with. /// The CRDT history to initialize this object with.
pub contents: Contents, pub contents: Contents,
/// The typename for this object. /// The typename for this object.
@ -23,6 +25,7 @@ impl<Author> Create<Author> {
fn create_spec(&self) -> change::Create<git_ext::Oid> { fn create_spec(&self) -> change::Create<git_ext::Oid> {
change::Create { change::Create {
typename: self.typename.clone(), typename: self.typename.clone(),
history_type: self.history_type,
tips: Vec::new(), tips: Vec::new(),
message: self.message.clone(), message: self.message.clone(),
contents: self.contents.clone(), contents: self.contents.clone(),

View File

@ -4,8 +4,8 @@
// Linking Exception. For full terms see the included LICENSE file. // Linking Exception. For full terms see the included LICENSE file.
use crate::{ use crate::{
change, change_graph::ChangeGraph, identity::Identity, CollaborativeObject, Contents, ObjectId, change, change_graph::ChangeGraph, identity::Identity, CollaborativeObject, Contents,
Store, TypeName, HistoryType, ObjectId, Store, TypeName,
}; };
use super::error; use super::error;
@ -14,6 +14,8 @@ use super::error;
pub struct Update<Author> { pub struct Update<Author> {
/// The identity of the author for the update of this object. /// The identity of the author for the update of this object.
pub author: Option<Author>, pub author: Option<Author>,
/// The type of history that will be used for this object.
pub history_type: HistoryType,
/// The CRDT changes to add to the object. /// The CRDT changes to add to the object.
pub changes: Contents, pub changes: Contents,
/// The object ID of the object to be updated. /// The object ID of the object to be updated.
@ -60,6 +62,7 @@ where
author, author,
ref typename, ref typename,
object_id, object_id,
history_type,
changes, changes,
message, message,
} = args; } = args;
@ -89,6 +92,7 @@ where
signer, signer,
change::Create { change::Create {
tips: object.tips().iter().cloned().collect(), tips: object.tips().iter().cloned().collect(),
history_type,
contents: changes.clone(), contents: changes.clone(),
typename: typename.clone(), typename: typename.clone(),
message, message,

View File

@ -6,7 +6,7 @@ use quickcheck::Arbitrary;
use radicle_crypto::Signer; use radicle_crypto::Signer;
use crate::{ use crate::{
create, get, history, list, object, test::arbitrary::Invalid, update, Create, ObjectId, create, get, list, object, test::arbitrary::Invalid, update, Create, HistoryType, ObjectId,
TypeName, Update, TypeName, Update,
}; };
@ -22,7 +22,6 @@ fn roundtrip() {
project: proj, project: proj,
person: terry.clone(), person: terry.clone(),
}; };
let contents = history::Contents::Automerge(Vec::new());
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap(); let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
let cob = create( let cob = create(
&storage, &storage,
@ -31,7 +30,8 @@ fn roundtrip() {
&proj.identifier(), &proj.identifier(),
Create { Create {
author: Some(terry), author: Some(terry),
contents, history_type: HistoryType::Automerge,
contents: Vec::new(),
typename: typename.clone(), typename: typename.clone(),
message: "creating xyz.rad.issue".to_string(), message: "creating xyz.rad.issue".to_string(),
}, },
@ -63,7 +63,8 @@ fn list_cobs() {
&proj.identifier(), &proj.identifier(),
Create { Create {
author: Some(terry.clone()), author: Some(terry.clone()),
contents: history::Contents::Automerge(b"issue 1".to_vec()), history_type: HistoryType::Automerge,
contents: b"issue 1".to_vec(),
typename: typename.clone(), typename: typename.clone(),
message: "creating xyz.rad.issue".to_string(), message: "creating xyz.rad.issue".to_string(),
}, },
@ -77,7 +78,8 @@ fn list_cobs() {
&proj.identifier(), &proj.identifier(),
Create { Create {
author: Some(terry), author: Some(terry),
contents: history::Contents::Automerge(b"issue 2".to_vec()), history_type: HistoryType::Automerge,
contents: b"issue 2".to_vec(),
typename: typename.clone(), typename: typename.clone(),
message: "commenting xyz.rad.issue".to_string(), message: "commenting xyz.rad.issue".to_string(),
}, },
@ -103,7 +105,6 @@ fn update_cob() {
project: proj, project: proj,
person: terry.clone(), person: terry.clone(),
}; };
let contents = history::Contents::Automerge(Vec::new());
let typename = "xyz.rad.issue".parse::<TypeName>().unwrap(); let typename = "xyz.rad.issue".parse::<TypeName>().unwrap();
let cob = create( let cob = create(
&storage, &storage,
@ -112,7 +113,8 @@ fn update_cob() {
&proj.identifier(), &proj.identifier(),
Create { Create {
author: Some(terry.clone()), author: Some(terry.clone()),
contents, history_type: HistoryType::Automerge,
contents: Vec::new(),
typename: typename.clone(), typename: typename.clone(),
message: "creating xyz.rad.issue".to_string(), message: "creating xyz.rad.issue".to_string(),
}, },
@ -130,7 +132,8 @@ fn update_cob() {
&proj.identifier(), &proj.identifier(),
Update { Update {
author: Some(terry), author: Some(terry),
changes: history::Contents::Automerge(b"issue 1".to_vec()), changes: b"issue 1".to_vec(),
history_type: HistoryType::Automerge,
object_id: *cob.id(), object_id: *cob.id(),
typename: typename.clone(), typename: typename.clone(),
message: "commenting xyz.rad.issue".to_string(), message: "commenting xyz.rad.issue".to_string(),
@ -170,7 +173,8 @@ fn traverse_cobs() {
&terry_proj.identifier(), &terry_proj.identifier(),
Create { Create {
author: Some(terry), author: Some(terry),
contents: history::Contents::Automerge(b"issue 1".to_vec()), contents: b"issue 1".to_vec(),
history_type: HistoryType::Automerge,
typename: typename.clone(), typename: typename.clone(),
message: "creating xyz.rad.issue".to_string(), message: "creating xyz.rad.issue".to_string(),
}, },
@ -192,7 +196,8 @@ fn traverse_cobs() {
&neil_proj.identifier(), &neil_proj.identifier(),
Update { Update {
author: Some(neil), author: Some(neil),
changes: history::Contents::Automerge(b"issue 2".to_vec()), changes: b"issue 2".to_vec(),
history_type: HistoryType::Automerge,
object_id: *cob.id(), object_id: *cob.id(),
typename, typename,
message: "commenting on xyz.rad.issue".to_string(), message: "commenting on xyz.rad.issue".to_string(),
@ -210,7 +215,7 @@ fn traverse_cobs() {
if let (Some(author), Some(project)) = (author, project) { if let (Some(author), Some(project)) = (author, project) {
if project.delegate_check(&author) { if project.delegate_check(&author) {
acc.push(entry.contents().as_ref().to_vec()) acc.push(entry.contents().to_vec())
} }
} }
ControlFlow::Continue(acc) ControlFlow::Continue(acc)
@ -220,7 +225,7 @@ fn traverse_cobs() {
// traverse over the history and filter by changes that were only authorized by neil // traverse over the history and filter by changes that were only authorized by neil
let contents = updated.history().traverse(Vec::new(), |mut acc, entry| { let contents = updated.history().traverse(Vec::new(), |mut acc, entry| {
acc.push(entry.contents().as_ref().to_vec()); acc.push(entry.contents().to_vec());
ControlFlow::Continue(acc) ControlFlow::Continue(acc)
}); });

View File

@ -9,7 +9,7 @@ pub mod value;
pub use cob::{ pub use cob::{
identity, object::collaboration::error, CollaborativeObject, Contents, Create, Entry, History, identity, object::collaboration::error, CollaborativeObject, Contents, Create, Entry, History,
ObjectId, TypeName, Update, HistoryType, ObjectId, TypeName, Update,
}; };
pub use shared::Timestamp; pub use shared::Timestamp;
pub use store::Store; pub use store::Store;

View File

@ -135,16 +135,13 @@ impl FromHistory for Issue {
fn from_history(history: &History) -> Result<Self, Error> { fn from_history(history: &History) -> Result<Self, Error> {
let doc = history.traverse(Automerge::new(), |mut doc, entry| { let doc = history.traverse(Automerge::new(), |mut doc, entry| {
match entry.contents() { let bytes = entry.contents();
Contents::Automerge(bytes) => { match automerge::Change::from_bytes(bytes.clone()) {
match automerge::Change::from_bytes(bytes.clone()) { Ok(change) => {
Ok(change) => { doc.apply_changes([change]).ok();
doc.apply_changes([change]).ok(); }
} Err(_err) => {
Err(_err) => { // Ignore
// Ignore
}
}
} }
} }
ControlFlow::Continue(doc) ControlFlow::Continue(doc)
@ -379,7 +376,7 @@ mod events {
.map_err(|failure| failure.error)? .map_err(|failure| failure.error)?
.result; .result;
Ok(Contents::Automerge(doc.save_incremental())) Ok(doc.save_incremental())
} }
pub fn comment( pub fn comment(
@ -414,7 +411,7 @@ mod events {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
let change = issue.get_last_local_change().unwrap().raw_bytes().to_vec(); let change = issue.get_last_local_change().unwrap().raw_bytes().to_vec();
Ok(Contents::Automerge(change)) Ok(change)
} }
pub fn lifecycle( pub fn lifecycle(
@ -439,7 +436,7 @@ mod events {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
let change = issue.get_last_local_change().unwrap().raw_bytes().to_vec(); let change = issue.get_last_local_change().unwrap().raw_bytes().to_vec();
Ok(Contents::Automerge(change)) Ok(change)
} }
pub fn label( pub fn label(
@ -466,7 +463,7 @@ mod events {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
let change = issue.get_last_local_change().unwrap().raw_bytes().to_vec(); let change = issue.get_last_local_change().unwrap().raw_bytes().to_vec();
Ok(Contents::Automerge(change)) Ok(change)
} }
pub fn reply( pub fn reply(
@ -503,7 +500,7 @@ mod events {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
let change = issue.get_last_local_change().unwrap().raw_bytes().to_vec(); let change = issue.get_last_local_change().unwrap().raw_bytes().to_vec();
Ok(Contents::Automerge(change)) Ok(change)
} }
pub fn react( pub fn react(
@ -546,7 +543,7 @@ mod events {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
let change = issue.get_last_local_change().unwrap().raw_bytes().to_vec(); let change = issue.get_last_local_change().unwrap().raw_bytes().to_vec();
Ok(Contents::Automerge(change)) Ok(change)
} }
} }

View File

@ -43,16 +43,13 @@ impl TryFrom<&History> for Label {
fn try_from(history: &History) -> Result<Self, Self::Error> { fn try_from(history: &History) -> Result<Self, Self::Error> {
let doc = history.traverse(Automerge::new(), |mut doc, entry| { let doc = history.traverse(Automerge::new(), |mut doc, entry| {
match entry.contents() { let bytes = entry.contents();
Contents::Automerge(bytes) => { match automerge::Change::from_bytes(bytes.clone()) {
match automerge::Change::from_bytes(bytes.clone()) { Ok(change) => {
Ok(change) => { doc.apply_changes([change]).ok();
doc.apply_changes([change]).ok(); }
} Err(_err) => {
Err(_err) => { // Ignore
// Ignore
}
}
} }
} }
ControlFlow::Continue(doc) ControlFlow::Continue(doc)
@ -143,7 +140,7 @@ mod events {
) )
.map_err(|failure| failure.error)?; .map_err(|failure| failure.error)?;
Ok(Contents::Automerge(doc.save_incremental())) Ok(doc.save_incremental())
} }
} }

View File

@ -161,16 +161,13 @@ impl TryFrom<&History> for Patch {
fn try_from(history: &History) -> Result<Self, Self::Error> { fn try_from(history: &History) -> Result<Self, Self::Error> {
let doc = history.traverse(Automerge::new(), |mut doc, entry| { let doc = history.traverse(Automerge::new(), |mut doc, entry| {
match entry.contents() { let bytes = entry.contents();
Contents::Automerge(bytes) => { match automerge::Change::from_bytes(bytes.clone()) {
match automerge::Change::from_bytes(bytes.clone()) { Ok(change) => {
Ok(change) => { doc.apply_changes([change]).ok();
doc.apply_changes([change]).ok(); }
} Err(_err) => {
Err(_err) => { // Ignore
// Ignore
}
}
} }
} }
ControlFlow::Continue(doc) ControlFlow::Continue(doc)
@ -750,7 +747,7 @@ mod events {
.map_err(|failure| failure.error)? .map_err(|failure| failure.error)?
.result; .result;
Ok(Contents::Automerge(doc.save_incremental())) Ok(doc.save_incremental())
} }
pub fn comment( pub fn comment(
@ -788,7 +785,7 @@ mod events {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
let change = patch.get_last_local_change().unwrap().raw_bytes().to_vec(); let change = patch.get_last_local_change().unwrap().raw_bytes().to_vec();
Ok(Contents::Automerge(change)) Ok(change)
} }
pub fn update( pub fn update(
@ -817,7 +814,7 @@ mod events {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
let change = patch.get_last_local_change().unwrap().raw_bytes().to_vec(); let change = patch.get_last_local_change().unwrap().raw_bytes().to_vec();
Ok((revision_ix, Contents::Automerge(change))) Ok((revision_ix, change))
} }
pub fn reply( pub fn reply(
@ -857,7 +854,7 @@ mod events {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
let change = patch.get_last_local_change().unwrap().raw_bytes().to_vec(); let change = patch.get_last_local_change().unwrap().raw_bytes().to_vec();
Ok(Contents::Automerge(change)) Ok(change)
} }
pub fn review( pub fn review(
@ -888,7 +885,7 @@ mod events {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
let change = patch.get_last_local_change().unwrap().raw_bytes().to_vec(); let change = patch.get_last_local_change().unwrap().raw_bytes().to_vec();
Ok(((), Contents::Automerge(change))) Ok(((), change))
} }
pub fn merge( pub fn merge(
@ -921,7 +918,7 @@ mod events {
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
let change = patch.get_last_local_change().unwrap().raw_bytes().to_vec(); let change = patch.get_last_local_change().unwrap().raw_bytes().to_vec();
Ok(Contents::Automerge(change)) Ok(change)
} }
} }

View File

@ -11,7 +11,7 @@ use crate::cob::shared::Author;
use crate::cob::transaction::TransactionError; use crate::cob::transaction::TransactionError;
use crate::cob::CollaborativeObject; use crate::cob::CollaborativeObject;
use crate::cob::{issue, label, patch}; use crate::cob::{issue, label, patch};
use crate::cob::{Contents, Create, History, ObjectId, TypeName, Update}; use crate::cob::{Contents, Create, History, HistoryType, ObjectId, TypeName, Update};
use crate::crypto::PublicKey; use crate::crypto::PublicKey;
use crate::git; use crate::git;
use crate::identity::project; use crate::identity::project;
@ -134,6 +134,7 @@ impl<'a, T: FromHistory> Store<'a, T> {
Update { Update {
author: Some(cob::Author::from(*signer.public_key())), author: Some(cob::Author::from(*signer.public_key())),
object_id, object_id,
history_type: HistoryType::Automerge,
typename: T::type_name().clone(), typename: T::type_name().clone(),
message: message.to_owned(), message: message.to_owned(),
changes, changes,
@ -154,6 +155,7 @@ impl<'a, T: FromHistory> Store<'a, T> {
&self.project, &self.project,
Create { Create {
author: Some(cob::Author::from(*signer.public_key())), author: Some(cob::Author::from(*signer.public_key())),
history_type: HistoryType::Automerge,
typename: T::type_name().clone(), typename: T::type_name().clone(),
message: message.to_owned(), message: message.to_owned(),
contents, contents,
@ -182,11 +184,7 @@ impl<'a, T: FromHistory> Store<'a, T> {
}; };
let doc = cob.history().traverse(Vec::new(), |mut doc, entry| { let doc = cob.history().traverse(Vec::new(), |mut doc, entry| {
match entry.contents() { doc.extend(entry.contents());
Contents::Automerge(bytes) => {
doc.extend(bytes);
}
}
ControlFlow::Continue(doc) ControlFlow::Continue(doc)
}); });