radicle: Introduce `cob::DraftStore`

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.
This commit is contained in:
Alexis Sellier 2023-06-23 12:53:20 +02:00
parent c9162121b0
commit ff7e9ded17
No known key found for this signature in database
4 changed files with 169 additions and 6 deletions

View File

@ -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()?,

View File

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

View File

@ -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/<remote>/refs/drafts/reviews/<patch-id>`
///
/// 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/<remote>/refs/drafts/cobs/<typename>/<object_id>`
///
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/<remote>/refs/drafts/cobs/<typename>/*`
///
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.

View File

@ -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 = <git2::Repository as change::Storage>::StoreError;
type LoadError = <git2::Repository as change::Storage>::LoadError;
type ObjectId = <git2::Repository as change::Storage>::ObjectId;
type Parent = <git2::Repository as change::Storage>::Parent;
type Signatures = <git2::Repository as change::Storage>::Signatures;
fn store<Signer>(
&self,
authority: Self::Parent,
parents: Vec<Self::Parent>,
signer: &Signer,
spec: change::Template<Self::ObjectId>,
) -> Result<cob::Change, Self::StoreError>
where
Signer: crypto::Signer,
{
self.repo.backend.store(authority, parents, signer, spec)
}
fn load(&self, id: Self::ObjectId) -> Result<cob::Change, Self::LoadError> {
self.repo.backend.load(id)
}
fn parents_of(&self, id: &Oid) -> Result<Vec<Oid>, 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<cob::object::Objects, Self::ObjectsError> {
// 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<BTreeMap<cob::ObjectId, cob::object::Objects>, 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)
}
}