Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle: wire up radicle-cob
Fintan Halpenny committed 3 years ago
commit cb2d83daae7149c0077a92f8ea9a94923ba65f21
parent 0bd213d15ae1d12603d2d21951723b66d14890ca
8 files changed +326 -0
modified Cargo.lock
@@ -1478,6 +1478,7 @@ dependencies = [
 "once_cell",
 "quickcheck",
 "quickcheck_macros",
+
 "radicle-cob",
 "radicle-crypto",
 "radicle-git-ext",
 "radicle-ssh",
modified radicle/Cargo.toml
@@ -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"
added radicle/src/cob.rs
@@ -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)
+
}
modified radicle/src/git.rs
@@ -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 {
modified radicle/src/identity/project.rs
@@ -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.
modified radicle/src/identity/project/id.rs
@@ -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")
+
    }
+
}
modified radicle/src/lib.rs
@@ -4,6 +4,7 @@

pub extern crate radicle_crypto as crypto;

+
pub mod cob;
pub mod collections;
pub mod git;
pub mod hash;
modified radicle/src/storage/git.rs
@@ -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;