radicle: wire up radicle-cob
To wire up radicle-cob, it is necessary to implement the traits for the following types: * `Repository`: `change::Storage`, `object::Storage`, and `Store` * `project::Identity`: `identity::Identity` For `Repository`, the `change::Storage` implementation is trivial, as it uses the underlying `git2::Repository` implementation. Then `object::Storage` simply defines the required references names and does lookups or updates. The aforementioned reference naming scheme is: refs/namespaces/<ns>/refs/cobs/<typename>/<object_id> For `project::Identity`, the `identity::Identity` implementation is straight-forward. To finalise the work, a simplified API is exposed as `radicle::cob` where the `create`, `update`, `get`, and `list` functions are redefined in terms of `radicle` specific types. Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com> X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
parent
0bd213d15a
commit
cb2d83daae
|
|
@ -1478,6 +1478,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"quickcheck",
|
"quickcheck",
|
||||||
"quickcheck_macros",
|
"quickcheck_macros",
|
||||||
|
"radicle-cob",
|
||||||
"radicle-crypto",
|
"radicle-crypto",
|
||||||
"radicle-git-ext",
|
"radicle-git-ext",
|
||||||
"radicle-ssh",
|
"radicle-ssh",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ version = "0.15.0"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["vendored-libgit2"]
|
features = ["vendored-libgit2"]
|
||||||
|
|
||||||
|
[dependencies.radicle-cob]
|
||||||
|
path = "../radicle-cob"
|
||||||
|
version = "0"
|
||||||
|
|
||||||
[dependencies.radicle-crypto]
|
[dependencies.radicle-crypto]
|
||||||
path = "../radicle-crypto"
|
path = "../radicle-crypto"
|
||||||
version = "0"
|
version = "0"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
pub use cob::{
|
||||||
|
identity, object::collaboration::error, CollaborativeObject, Create, Entry, History, ObjectId,
|
||||||
|
TypeName, Update,
|
||||||
|
};
|
||||||
|
use radicle_cob as cob;
|
||||||
|
use radicle_git_ext::Oid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
identity::{project::Identity, Did},
|
||||||
|
storage::git::Repository,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The `Author` of a [`create`] or [`update`].
|
||||||
|
///
|
||||||
|
/// **Note**: `Author` implements [`identity::Identity`], but since it
|
||||||
|
/// is not content-addressed, the [`identity::Identity::content_id`]
|
||||||
|
/// returns [`git2::Oid::zero`]. This means that if the `author` is
|
||||||
|
/// set in the updates the history entries for those changes will
|
||||||
|
/// contain the zero `Oid` for the `author` field.
|
||||||
|
pub struct Author {
|
||||||
|
did: Did,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl identity::Identity for Author {
|
||||||
|
type Identifier = String;
|
||||||
|
|
||||||
|
fn is_delegate(&self, delegation: &crypto::PublicKey) -> bool {
|
||||||
|
*self.did == *delegation
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content_id(&self) -> Oid {
|
||||||
|
git2::Oid::zero().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`CollaborativeObject`].
|
||||||
|
///
|
||||||
|
/// The `repository` is the project this collaborative object is being
|
||||||
|
/// stored under.
|
||||||
|
///
|
||||||
|
/// The `signer` is used to cryptographically sign the changes made
|
||||||
|
/// for this update. **Note** that the public key for the signer must
|
||||||
|
/// match the key of the `Author` -- if it is set.
|
||||||
|
///
|
||||||
|
/// The `project` is used to store its content-address in the history
|
||||||
|
/// of changes for the collaborative object.
|
||||||
|
///
|
||||||
|
/// The `args` are the metadata for this [`CollaborativeObject`]
|
||||||
|
/// udpate. See [`Update`] for further information.
|
||||||
|
pub fn create<S>(
|
||||||
|
repository: &Repository,
|
||||||
|
signer: S,
|
||||||
|
project: &Identity<Oid>,
|
||||||
|
args: Create<Author>,
|
||||||
|
) -> Result<CollaborativeObject, error::Create>
|
||||||
|
where
|
||||||
|
S: crypto::Signer,
|
||||||
|
{
|
||||||
|
let namespace = *signer.public_key();
|
||||||
|
cob::create(repository, signer, project, &namespace, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a [`CollaborativeObject`], if it exists.
|
||||||
|
///
|
||||||
|
/// The `repository` is the project this collaborative object is being
|
||||||
|
/// stored under.
|
||||||
|
///
|
||||||
|
/// The `typename` is the type of object to be found, while the
|
||||||
|
/// `object_id` is the identifier for the particular object under that
|
||||||
|
/// type.
|
||||||
|
pub fn get(
|
||||||
|
repository: &Repository,
|
||||||
|
typename: &TypeName,
|
||||||
|
object_id: &ObjectId,
|
||||||
|
) -> Result<Option<CollaborativeObject>, error::Retrieve> {
|
||||||
|
cob::get(repository, typename, object_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List a set of [`CollaborativeObject`].
|
||||||
|
///
|
||||||
|
/// The `repository` is the project this collaborative object is being
|
||||||
|
/// stored under.
|
||||||
|
///
|
||||||
|
/// The `typename` is the type of objects to listed.
|
||||||
|
pub fn list(
|
||||||
|
repository: &Repository,
|
||||||
|
typename: &TypeName,
|
||||||
|
) -> Result<Vec<CollaborativeObject>, error::Retrieve> {
|
||||||
|
cob::list(repository, typename)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an existing [`CollaborativeObject`].
|
||||||
|
///
|
||||||
|
/// The `repository` is the project this collaborative object is being
|
||||||
|
/// stored under.
|
||||||
|
///
|
||||||
|
/// The `signer` is used to cryptographically sign the changes made
|
||||||
|
/// for this update. **Note** that the public key for the signer must
|
||||||
|
/// match the key of the `Author` -- if it is set.
|
||||||
|
///
|
||||||
|
/// The `project` is used to store its content-address in the history
|
||||||
|
/// of changes for the collaborative object.
|
||||||
|
///
|
||||||
|
/// The `args` are the metadata for this [`CollaborativeObject`]
|
||||||
|
/// udpate. See [`Update`] for further information.
|
||||||
|
pub fn update<S>(
|
||||||
|
repository: &Repository,
|
||||||
|
signer: S,
|
||||||
|
project: &Identity<Oid>,
|
||||||
|
args: Update<Author>,
|
||||||
|
) -> Result<CollaborativeObject, error::Update>
|
||||||
|
where
|
||||||
|
S: crypto::Signer,
|
||||||
|
{
|
||||||
|
let namespace = *signer.public_key();
|
||||||
|
cob::update(repository, signer, project, &namespace, args)
|
||||||
|
}
|
||||||
|
|
@ -73,6 +73,13 @@ pub mod refs {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod storage {
|
pub mod storage {
|
||||||
|
use format::{
|
||||||
|
name::component,
|
||||||
|
refspec::{self, PatternString},
|
||||||
|
};
|
||||||
|
|
||||||
|
use radicle_cob as cob;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Where the project's identity document is stored.
|
/// Where the project's identity document is stored.
|
||||||
|
|
@ -106,6 +113,48 @@ pub mod refs {
|
||||||
pub fn id(remote: &RemoteId) -> Namespaced {
|
pub fn id(remote: &RemoteId) -> Namespaced {
|
||||||
IDENTITY_BRANCH.with_namespace(remote.into())
|
IDENTITY_BRANCH.with_namespace(remote.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The collaborative object reference, identified by `typename` and `object_id`, under the given `remote`.
|
||||||
|
///
|
||||||
|
/// `refs/namespaces/<remote>/refs/cobs/<typename>/<object_id>`
|
||||||
|
///
|
||||||
|
pub fn cob<'a>(
|
||||||
|
remote: &RemoteId,
|
||||||
|
typename: &cob::TypeName,
|
||||||
|
object_id: &cob::ObjectId,
|
||||||
|
) -> Namespaced<'a> {
|
||||||
|
Qualified::from_components(
|
||||||
|
component!("cobs"),
|
||||||
|
Component::from(typename),
|
||||||
|
Some(object_id.into()),
|
||||||
|
)
|
||||||
|
.with_namespace(remote.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All collaborative objects, identified by `typename` and `object_id`, for all remotes.
|
||||||
|
///
|
||||||
|
/// `refs/namespaces/*/refs/cobs/<typename>/<object_id>`
|
||||||
|
///
|
||||||
|
pub fn cobs(typename: &cob::TypeName, object_id: &cob::ObjectId) -> PatternString {
|
||||||
|
refspec::pattern!("refs/namespaces/*")
|
||||||
|
.join(refname!("refs/cobs"))
|
||||||
|
.join(Component::from(typename))
|
||||||
|
.join(Component::from(object_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cob_suffix<R>(cob: &R) -> Option<(cob::TypeName, cob::ObjectId)>
|
||||||
|
where
|
||||||
|
R: AsRef<RefStr>,
|
||||||
|
{
|
||||||
|
let cob = cob.as_ref().to_namespaced()?;
|
||||||
|
let cob = cob.strip_namespace();
|
||||||
|
let (_refs, _cobs, typename, mut object_id) = cob.non_empty_components();
|
||||||
|
let object_id = object_id
|
||||||
|
.next()
|
||||||
|
.and_then(|oid| oid.parse::<cob::ObjectId>().ok())?;
|
||||||
|
let typename = typename.parse::<cob::TypeName>().ok()?;
|
||||||
|
Some((typename, object_id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod workdir {
|
pub mod workdir {
|
||||||
|
|
|
||||||
|
|
@ -402,6 +402,18 @@ pub struct Identity<I> {
|
||||||
pub signatures: HashMap<PublicKey, Signature>,
|
pub signatures: HashMap<PublicKey, Signature>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl radicle_cob::identity::Identity for Identity<Oid> {
|
||||||
|
type Identifier = Oid;
|
||||||
|
|
||||||
|
fn is_delegate(&self, delegation: &crypto::PublicKey) -> bool {
|
||||||
|
self.doc.delegates.iter().any(|d| d.matches(delegation))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content_id(&self) -> Oid {
|
||||||
|
self.current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Identity<Oid> {
|
impl Identity<Oid> {
|
||||||
pub fn verified(self, id: Id) -> Result<Identity<Id>, IdentityError> {
|
pub fn verified(self, id: Id) -> Result<Identity<Id>, IdentityError> {
|
||||||
// The root hash must be equal to the id.
|
// The root hash must be equal to the id.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::{ffi::OsString, fmt, str::FromStr};
|
use std::{ffi::OsString, fmt, str::FromStr};
|
||||||
|
|
||||||
|
use git_ref_format::{Component, RefString};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
|
|
@ -99,3 +100,11 @@ impl<'de> serde::Deserialize<'de> for Id {
|
||||||
serde_ext::string::deserialize(deserializer)
|
serde_ext::string::deserialize(deserializer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&Id> for Component<'_> {
|
||||||
|
fn from(id: &Id) -> Self {
|
||||||
|
let refstr =
|
||||||
|
RefString::try_from(id.0.to_string()).expect("project id's are valid ref strings");
|
||||||
|
Component::from_refstring(refstr).expect("project id's are valid refname components")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
pub extern crate radicle_crypto as crypto;
|
pub extern crate radicle_crypto as crypto;
|
||||||
|
|
||||||
|
pub mod cob;
|
||||||
pub mod collections;
|
pub mod collections;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod hash;
|
pub mod hash;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use std::{fs, io};
|
||||||
use crypto::{Signer, Unverified, Verified};
|
use crypto::{Signer, Unverified, Verified};
|
||||||
use git_ref_format::refspec;
|
use git_ref_format::refspec;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use radicle_cob::{self as cob, change};
|
||||||
|
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::identity;
|
use crate::identity;
|
||||||
|
|
@ -60,6 +61,28 @@ impl ProjectError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum CobObjectsError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Convert(#[from] cob::object::storage::convert::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Git(#[from] git2::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum CobTypesError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Convert(#[from] cob::object::storage::convert::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Git(#[from] git2::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
ParseKey(#[from] crypto::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
ParseObjectId(#[from] cob::object::ParseObjectId),
|
||||||
|
#[error(transparent)]
|
||||||
|
RefFormat(#[from] git_ref_format::Error),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Storage {
|
pub struct Storage {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|
@ -651,6 +674,116 @@ impl WriteRepository for Repository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl cob::Store for Repository {}
|
||||||
|
|
||||||
|
impl change::Storage for Repository {
|
||||||
|
type CreateError = <git2::Repository as change::Storage>::CreateError;
|
||||||
|
type LoadError = <git2::Repository as change::Storage>::LoadError;
|
||||||
|
|
||||||
|
type ObjectId = <git2::Repository as change::Storage>::ObjectId;
|
||||||
|
type Author = <git2::Repository as change::Storage>::Author;
|
||||||
|
type Resource = <git2::Repository as change::Storage>::Resource;
|
||||||
|
type Signatures = <git2::Repository as change::Storage>::Signatures;
|
||||||
|
|
||||||
|
fn create<Signer>(
|
||||||
|
&self,
|
||||||
|
author: Option<Self::Author>,
|
||||||
|
authority: Self::Resource,
|
||||||
|
signer: &Signer,
|
||||||
|
spec: change::Create<Self::ObjectId>,
|
||||||
|
) -> Result<cob::Change, Self::CreateError>
|
||||||
|
where
|
||||||
|
Signer: crypto::Signer,
|
||||||
|
{
|
||||||
|
self.backend.create(author, authority, signer, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(&self, id: Self::ObjectId) -> Result<cob::Change, Self::LoadError> {
|
||||||
|
self.backend.load(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cob::object::Storage for Repository {
|
||||||
|
type ObjectsError = CobObjectsError;
|
||||||
|
type TypesError = CobTypesError;
|
||||||
|
type UpdateError = git2::Error;
|
||||||
|
|
||||||
|
type Identifier = RemoteId;
|
||||||
|
|
||||||
|
fn objects(
|
||||||
|
&self,
|
||||||
|
typename: &cob::TypeName,
|
||||||
|
object_id: &cob::ObjectId,
|
||||||
|
) -> Result<cob::object::Objects, Self::ObjectsError> {
|
||||||
|
let refs = self
|
||||||
|
.backend
|
||||||
|
.references_glob(git::refs::storage::cobs(typename, object_id).as_str())?;
|
||||||
|
let refs = refs
|
||||||
|
.map(|r| {
|
||||||
|
r.map_err(Self::ObjectsError::from).and_then(|r| {
|
||||||
|
cob::object::Reference::try_from(r).map_err(Self::ObjectsError::from)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
Ok(refs.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn types(
|
||||||
|
&self,
|
||||||
|
typename: &cob::TypeName,
|
||||||
|
) -> Result<HashMap<cob::ObjectId, cob::object::Objects>, Self::TypesError> {
|
||||||
|
let mut references = self.backend.references()?.filter_map(|reference| {
|
||||||
|
let reference = reference.ok()?;
|
||||||
|
match RefStr::try_from_str(reference.name()?) {
|
||||||
|
Ok(name) => {
|
||||||
|
let (ty, object_id) = git::refs::storage::cob_suffix(&name)?;
|
||||||
|
if ty == *typename {
|
||||||
|
Some(
|
||||||
|
cob::object::Reference::try_from(reference)
|
||||||
|
.map_err(Self::TypesError::from)
|
||||||
|
.map(|reference| (object_id, reference)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => Some(Err(err.into())),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
references.try_fold(HashMap::new(), |mut objects, result| {
|
||||||
|
let (oid, reference) = result?;
|
||||||
|
objects
|
||||||
|
.entry(oid)
|
||||||
|
.and_modify(|objs: &mut cob::object::Objects| objs.push(reference.clone()))
|
||||||
|
.or_insert_with(|| cob::object::Objects::new(reference));
|
||||||
|
Ok(objects)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&self,
|
||||||
|
identifier: &Self::Identifier,
|
||||||
|
typename: &cob::TypeName,
|
||||||
|
object_id: &cob::ObjectId,
|
||||||
|
change: &cob::Change,
|
||||||
|
) -> Result<(), Self::UpdateError> {
|
||||||
|
self.backend.reference(
|
||||||
|
git::refs::storage::cob(identifier, typename, object_id).as_str(),
|
||||||
|
(*change.id()).into(),
|
||||||
|
false,
|
||||||
|
&format!(
|
||||||
|
"Updating collaborative object '{}/{}' with new change {}",
|
||||||
|
typename,
|
||||||
|
object_id,
|
||||||
|
change.id()
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub mod trailers {
|
pub mod trailers {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue