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",
|
||||
"quickcheck",
|
||||
"quickcheck_macros",
|
||||
"radicle-cob",
|
||||
"radicle-crypto",
|
||||
"radicle-git-ext",
|
||||
"radicle-ssh",
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ version = "0.15.0"
|
|||
default-features = false
|
||||
features = ["vendored-libgit2"]
|
||||
|
||||
[dependencies.radicle-cob]
|
||||
path = "../radicle-cob"
|
||||
version = "0"
|
||||
|
||||
[dependencies.radicle-crypto]
|
||||
path = "../radicle-crypto"
|
||||
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 {
|
||||
use format::{
|
||||
name::component,
|
||||
refspec::{self, PatternString},
|
||||
};
|
||||
|
||||
use radicle_cob as cob;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Where the project's identity document is stored.
|
||||
|
|
@ -106,6 +113,48 @@ pub mod refs {
|
|||
pub fn id(remote: &RemoteId) -> Namespaced {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -402,6 +402,18 @@ pub struct Identity<I> {
|
|||
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> {
|
||||
pub fn verified(self, id: Id) -> Result<Identity<Id>, IdentityError> {
|
||||
// The root hash must be equal to the id.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::ops::Deref;
|
||||
use std::{ffi::OsString, fmt, str::FromStr};
|
||||
|
||||
use git_ref_format::{Component, RefString};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::crypto;
|
||||
|
|
@ -99,3 +100,11 @@ impl<'de> serde::Deserialize<'de> for Id {
|
|||
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 mod cob;
|
||||
pub mod collections;
|
||||
pub mod git;
|
||||
pub mod hash;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use std::{fs, io};
|
|||
use crypto::{Signer, Unverified, Verified};
|
||||
use git_ref_format::refspec;
|
||||
use once_cell::sync::Lazy;
|
||||
use radicle_cob::{self as cob, change};
|
||||
|
||||
use crate::git;
|
||||
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)]
|
||||
pub struct Storage {
|
||||
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 {
|
||||
use std::str::FromStr;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue