diff --git a/radicle-cli/src/commands/review.rs b/radicle-cli/src/commands/review.rs index 18e6e4dd..7d9524c9 100644 --- a/radicle-cli/src/commands/review.rs +++ b/radicle-cli/src/commands/review.rs @@ -276,7 +276,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { } } Operation::Delete => { - let name = git::refs::storage::review(profile.id(), &patch_id); + let name = git::refs::storage::draft::review(profile.id(), &patch_id); match repository.backend.find_reference(&name) { Ok(mut r) => r.delete()?, diff --git a/radicle-cli/src/commands/review/builder.rs b/radicle-cli/src/commands/review/builder.rs index 3c81d427..7078f57d 100644 --- a/radicle-cli/src/commands/review/builder.rs +++ b/radicle-cli/src/commands/review/builder.rs @@ -182,7 +182,7 @@ impl<'a> ReviewBuilder<'a> { pub fn new(patch_id: PatchId, nid: NodeId, repo: &'a Repository) -> Self { Self { patch_id, - refname: git::refs::storage::review(&nid, &patch_id), + refname: git::refs::storage::draft::review(&nid, &patch_id), repo, hunk: None, verdict: None, diff --git a/radicle/src/git.rs b/radicle/src/git.rs index 05d05495..010862ae 100644 --- a/radicle/src/git.rs +++ b/radicle/src/git.rs @@ -241,12 +241,57 @@ pub mod refs { .with_namespace(remote.into()) } - /// Review draft reference. + /// Draft references. /// - /// When building a patch review, we store the intermediate state in this ref. - pub fn review<'a>(remote: &RemoteId, patch: &cob::ObjectId) -> Namespaced<'a> { - Qualified::from_components(component!("reviews"), Component::from(patch), None) + /// These references are not replicated or signed. + pub mod draft { + use super::*; + + /// Review draft reference. Points to the non-COB part of a patch review. + /// + /// `refs/namespaces//refs/drafts/reviews/` + /// + /// When building a patch review, we store the intermediate state in this ref. + pub fn review<'a>(remote: &RemoteId, patch: &cob::ObjectId) -> Namespaced<'a> { + Qualified::from_components( + component!("drafts"), + component!("reviews"), + Some(Component::from(patch)), + ) .with_namespace(remote.into()) + } + + /// A draft collaborative object. This can also be a draft operation on an existing + /// object. + /// + /// `refs/namespaces//refs/drafts/cobs//` + /// + pub fn cob<'a>( + remote: &RemoteId, + typename: &cob::TypeName, + object_id: &cob::ObjectId, + ) -> Namespaced<'a> { + Qualified::from_components( + component!("drafts"), + component!("cobs"), + [Component::from(typename), object_id.into()], + ) + .with_namespace(remote.into()) + } + + /// Draft collaborative objects of a type. + /// + /// `refs/namespaces//refs/drafts/cobs//*` + /// + pub fn cobs(remote: &RemoteId, typename: &cob::TypeName) -> PatternString { + Qualified::from_components( + component!("drafts"), + component!("cobs"), + Some(Component::from(typename)), + ) + .with_namespace(remote.into()) + .to_pattern(refspec::pattern!("*")) + } } /// Staging/temporary references. diff --git a/radicle/src/storage/git/cob.rs b/radicle/src/storage/git/cob.rs index 0f7ec1cd..7e2ba5e0 100644 --- a/radicle/src/storage/git/cob.rs +++ b/radicle/src/storage/git/cob.rs @@ -1,11 +1,13 @@ //! COB storage Git backend. use std::collections::BTreeMap; +use cob::object::Objects; use radicle_cob as cob; use radicle_cob::change; use crate::git; use crate::storage::Error; +use crate::storage::ReadRepository; pub use crate::git::*; pub use cob::*; @@ -161,3 +163,119 @@ impl cob::object::Storage for Repository { reference.delete().map_err(Self::RemoveError::from) } } + +/// Stores draft collaborative objects. +pub struct DraftStore { + remote: RemoteId, + repo: Repository, +} + +impl cob::Store for DraftStore {} + +impl change::Storage for DraftStore { + type StoreError = ::StoreError; + type LoadError = ::LoadError; + + type ObjectId = ::ObjectId; + type Parent = ::Parent; + type Signatures = ::Signatures; + + fn store( + &self, + authority: Self::Parent, + parents: Vec, + signer: &Signer, + spec: change::Template, + ) -> Result + where + Signer: crypto::Signer, + { + self.repo.backend.store(authority, parents, signer, spec) + } + + fn load(&self, id: Self::ObjectId) -> Result { + self.repo.backend.load(id) + } + + fn parents_of(&self, id: &Oid) -> Result, Self::LoadError> { + self.repo.backend.parents_of(id) + } +} + +impl cob::object::Storage for DraftStore { + type ObjectsError = ObjectsError; + type TypesError = git::ext::Error; + type UpdateError = git2::Error; + type RemoveError = git2::Error; + + type Identifier = RemoteId; + + 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.backend.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: &Self::Identifier, + typename: &cob::TypeName, + object_id: &cob::ObjectId, + change: &cob::Change, + ) -> Result<(), Self::UpdateError> { + self.repo.backend.reference( + git::refs::storage::draft::cob(identifier, typename, object_id).as_str(), + (*change.id()).into(), + true, + &format!( + "Updating draft collaborative object '{}/{}' with new change {}", + typename, + object_id, + change.id() + ), + )?; + + Ok(()) + } + + fn remove( + &self, + identifier: &Self::Identifier, + typename: &cob::TypeName, + object_id: &cob::ObjectId, + ) -> Result<(), Self::RemoveError> { + let mut reference = self.repo.backend.find_reference( + git::refs::storage::draft::cob(identifier, typename, object_id).as_str(), + )?; + + reference.delete().map_err(Self::RemoveError::from) + } +}