//! 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), } #[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> { remote: RemoteId, repo: &'a R, } impl<'a, R: storage::WriteRepository> DraftStore<'a, R> { /// Create this draft store with an existing COB from storage, so that changes applied /// to this COB can evaluate properly. This creates a symbolic reference to the COB /// pointing to the public COB reference. pub fn with( self, remote: &RemoteId, typename: &cob::TypeName, id: &ObjectId, ) -> Result { let target = git::refs::storage::cob(remote, typename, id); let name = git::refs::storage::draft::cob(remote, typename, id); let repo = self.repo.raw(); repo.reference_symbolic( name.as_str(), target.as_str(), true, // The reference may already exist, overwrite it if so. format!("Link to COB {id} of type {typename}").as_str(), )?; Ok(self) } } impl<'a, R> DraftStore<'a, R> { pub fn new(remote: RemoteId, repo: &'a R) -> Self { Self { remote, repo } } } 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 { // Nb. There can only be one draft per COB, per remote. let Ok(r) = self.repo.raw().find_reference( git::refs::storage::draft::cob(&self.remote, typename, object_id).as_str(), ) else { return Ok(Objects::default()); }; let r = cob::object::Reference::try_from(r).map_err(Self::ObjectsError::from)?; Ok(Objects::new(r)) } fn types( &self, typename: &cob::TypeName, ) -> Result, Self::TypesError> { let glob = git::refs::storage::draft::cobs(&self.remote, typename); let references = self.repo.references_glob(&glob)?; let mut objs = BTreeMap::new(); for (name, id) in references { let r = cob::object::Reference { name: name.into_refstring(), target: cob::object::Commit { id }, }; objs.insert(id.into(), Objects::new(r)); } Ok(objs) } 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() } }