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:
Fintan Halpenny 2022-11-07 16:32:09 +00:00
parent 0bd213d15a
commit cb2d83daae
No known key found for this signature in database
GPG Key ID: 2552FB6F64066CB7
8 changed files with 326 additions and 0 deletions

1
Cargo.lock generated
View File

@ -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",

View File

@ -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"

117
radicle/src/cob.rs Normal file
View File

@ -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)
}

View File

@ -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 {

View File

@ -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.

View File

@ -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")
}
}

View File

@ -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;

View File

@ -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;