//! COB storage Git backend. use std::collections::BTreeMap; use std::path::Path; use cob::object::Objects; use radicle_cob as cob; use radicle_cob::change; use storage::RemoteRepository; use storage::RepositoryError; use storage::SignRepository; use storage::ValidateRepository; use crate::git::*; use crate::storage; use crate::storage::Error; use crate::storage::{ git::{Remote, Remotes, Validations}, ReadRepository, Verified, }; use crate::{ git, identity, identity::{doc::DocError, PublicKey}, }; use super::{RemoteId, Repository}; pub use crate::cob::{store, ObjectId}; #[derive(Error, Debug)] pub enum ObjectsError { #[error(transparent)] Convert(#[from] cob::object::storage::convert::Error), #[error(transparent)] Git(#[from] git2::Error), #[error(transparent)] GitExt(#[from] git_ext::Error), } #[derive(Error, Debug)] pub enum TypesError { #[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::fmt::Error), } impl cob::Store for Repository {} impl change::Storage for Repository { type StoreError = ::StoreError; type LoadError = ::LoadError; type ObjectId = ::ObjectId; type Parent = ::Parent; type Signatures = ::Signatures; fn store( &self, authority: Option, parents: Vec, signer: &Signer, spec: change::Template, ) -> Result where Signer: crypto::Signer, { self.backend.store(authority, parents, signer, spec) } fn load(&self, id: Self::ObjectId) -> Result { self.backend.load(id) } fn parents_of(&self, id: &Oid) -> Result, Self::LoadError> { self.backend.parents_of(id) } } impl cob::object::Storage for Repository { type ObjectsError = ObjectsError; type TypesError = TypesError; type UpdateError = git2::Error; type RemoveError = git2::Error; fn objects( &self, typename: &cob::TypeName, object_id: &cob::ObjectId, ) -> Result { 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::, _>>()?; Ok(refs.into()) } fn types( &self, typename: &cob::TypeName, ) -> Result, Self::TypesError> { // TODO: Use glob here. 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) = cob::object::parse_refstr(&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(BTreeMap::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: &PublicKey, typename: &cob::TypeName, object_id: &cob::ObjectId, entry: &cob::EntryId, ) -> Result<(), Self::UpdateError> { self.backend.reference( git::refs::storage::cob(identifier, typename, object_id).as_str(), (*entry).into(), true, &format!( "Updating collaborative object '{}/{}' with new entry {}", typename, object_id, entry, ), )?; Ok(()) } fn remove( &self, identifier: &PublicKey, typename: &cob::TypeName, object_id: &cob::ObjectId, ) -> Result<(), Self::RemoveError> { let mut reference = self .backend .find_reference(git::refs::storage::cob(identifier, typename, object_id).as_str())?; reference.delete().map_err(Self::RemoveError::from) } } /// Stores draft collaborative objects. /// // This storage backend for COBs stores changes in a `draft/cobs/*` namespace, // which allows for some of the features needed for code review. For // example, users can draft comments and later decide to publish them. pub struct DraftStore<'a, R> { repo: &'a R, remote: RemoteId, } impl<'a, R> DraftStore<'a, R> { pub fn new(repo: &'a R, remote: RemoteId) -> Self { Self { repo, remote } } } impl<'a, R: storage::WriteRepository> cob::Store for DraftStore<'a, R> {} impl<'a, R: storage::WriteRepository> change::Storage for DraftStore<'a, R> { type StoreError = ::StoreError; type LoadError = ::LoadError; type ObjectId = ::ObjectId; type Parent = ::Parent; type Signatures = ::Signatures; fn store( &self, authority: Option, parents: Vec, signer: &Signer, spec: change::Template, ) -> Result where Signer: crypto::Signer, { self.repo.raw().store(authority, parents, signer, spec) } fn load(&self, id: Self::ObjectId) -> Result { self.repo.raw().load(id) } fn parents_of(&self, id: &Oid) -> Result, Self::LoadError> { self.repo.raw().parents_of(id) } } impl<'a, R: storage::ReadRepository> SignRepository for DraftStore<'a, R> { fn sign_refs( &self, signer: &G, ) -> Result, Error> { // Since this is a draft store, we do not actually want to sign the refs. // Instead, we just return the existing signed refs. let remote = self.repo.remote(signer.public_key())?; Ok(remote.refs) } } impl<'a, R: storage::RemoteRepository> RemoteRepository for DraftStore<'a, R> { fn remote(&self, id: &RemoteId) -> Result, storage::refs::Error> { self.repo.remote(id) } fn remotes(&self) -> Result, storage::refs::Error> { RemoteRepository::remotes(self.repo) } fn remote_refs_at(&self) -> Result, storage::refs::Error> { RemoteRepository::remote_refs_at(self.repo) } } impl<'a, R: storage::ValidateRepository> ValidateRepository for DraftStore<'a, R> { fn validate_remote(&self, remote: &Remote) -> Result { self.repo.validate_remote(remote) } } impl<'a, R: storage::ReadRepository> ReadRepository for DraftStore<'a, R> { fn id(&self) -> identity::RepoId { self.repo.id() } fn is_empty(&self) -> Result { self.repo.is_empty() } fn head(&self) -> Result<(Qualified, Oid), RepositoryError> { self.repo.head() } fn canonical_head(&self) -> Result<(Qualified, Oid), RepositoryError> { self.repo.canonical_head() } fn path(&self) -> &std::path::Path { self.repo.path() } fn commit(&self, oid: Oid) -> Result { self.repo.commit(oid) } fn revwalk(&self, head: Oid) -> Result { self.repo.revwalk(head) } fn contains(&self, oid: Oid) -> Result { self.repo.contains(oid) } fn is_ancestor_of(&self, ancestor: Oid, head: Oid) -> Result { self.repo.is_ancestor_of(ancestor, head) } fn blob_at>( &self, oid: git_ext::Oid, path: P, ) -> Result { self.repo.blob_at(oid, path) } fn blob(&self, oid: git_ext::Oid) -> Result { self.repo.blob(oid) } fn reference( &self, remote: &RemoteId, reference: &git::Qualified, ) -> Result { self.repo.reference(remote, reference) } fn reference_oid( &self, remote: &RemoteId, reference: &git::Qualified, ) -> Result { self.repo.reference_oid(remote, reference) } fn references_of(&self, remote: &RemoteId) -> Result { self.repo.references_of(remote) } fn references_glob( &self, pattern: &git::PatternStr, ) -> Result, git::ext::Error> { self.repo.references_glob(pattern) } fn identity_doc(&self) -> Result { self.repo.identity_doc() } fn identity_doc_at(&self, head: Oid) -> Result { self.repo.identity_doc_at(head) } fn identity_head(&self) -> Result { self.repo.identity_head() } fn identity_head_of(&self, remote: &RemoteId) -> Result { self.repo.identity_head_of(remote) } fn identity_root(&self) -> Result { self.repo.identity_root() } fn identity_root_of(&self, remote: &RemoteId) -> Result { self.repo.identity_root_of(remote) } fn canonical_identity_head(&self) -> Result { self.repo.canonical_identity_head() } fn merge_base(&self, left: &Oid, right: &Oid) -> Result { self.repo.merge_base(left, right) } } impl<'a, R: storage::WriteRepository> cob::object::Storage for DraftStore<'a, R> { type ObjectsError = ObjectsError; type TypesError = git::ext::Error; type UpdateError = git2::Error; type RemoveError = git2::Error; fn objects( &self, typename: &cob::TypeName, object_id: &cob::ObjectId, ) -> Result { let mut objs = Objects::default(); // First get the signed COB tips of all remotes for this object. let signed = self .repo .references_glob(&git::refs::storage::cobs(typename, object_id))?; for (r, id) in signed { let r = cob::object::Reference { name: r.to_ref_string(), target: cob::object::Commit { id }, }; objs.push(r); } // Then get the draft COB tip that belongs to us only, if any. let draft_ref = &git::refs::storage::draft::cob(&self.remote, typename, object_id); if let Ok(draft_oid) = self .repo .reference_oid(&self.remote, &draft_ref.strip_namespace()) { objs.push(cob::object::Reference { name: draft_ref.to_ref_string(), target: cob::object::Commit { id: draft_oid }, }) } Ok(objs) } fn types( &self, _typename: &cob::TypeName, ) -> Result, Self::TypesError> { unimplemented!() } fn update( &self, identifier: &PublicKey, typename: &cob::TypeName, object_id: &cob::ObjectId, entry: &cob::history::EntryId, ) -> Result<(), Self::UpdateError> { self.repo.raw().reference( git::refs::storage::draft::cob(identifier, typename, object_id).as_str(), (*entry).into(), true, &format!( "Updating draft collaborative object '{}/{}' with new entry {}", typename, object_id, entry, ), )?; Ok(()) } fn remove( &self, identifier: &PublicKey, typename: &cob::TypeName, object_id: &cob::ObjectId, ) -> Result<(), Self::RemoveError> { let mut reference = self.repo.raw().find_reference( git::refs::storage::draft::cob(identifier, typename, object_id).as_str(), )?; reference.delete() } }