cob: Change APIs to take URIs for embeds

To facilitate edit actions, take URIs instead of the actual blobs. This
means API callers don't have to load all the blobs just for them to be
re-hashed when an edit action is submitted.

There are some peculiarities when dealing with the `Identity` COB since
the embed is the identity document itself. We handle that special case.
This commit is contained in:
cloudhead 2024-10-09 16:59:53 +02:00
parent 034eb41860
commit 855327d303
No known key found for this signature in database
12 changed files with 152 additions and 144 deletions

View File

@ -1,6 +1,6 @@
use super::*; use super::*;
use radicle::cob::{self, patch, resolve_embed}; use radicle::cob::{self, patch};
use radicle::crypto; use radicle::crypto;
use radicle::prelude::*; use radicle::prelude::*;
use radicle::storage::git::Repository; use radicle::storage::git::Repository;
@ -19,12 +19,11 @@ pub fn run(
let Ok(patch) = patches.get_mut(patch_id) else { let Ok(patch) = patches.get_mut(patch_id) else {
anyhow::bail!("Patch `{patch_id}` not found"); anyhow::bail!("Patch `{patch_id}` not found");
}; };
let (title, description) = term::patch::get_edit_message(message, &patch)?; let (title, description) = term::patch::get_edit_message(message, &patch)?;
match revision_id { match revision_id {
Some(id) => edit_revision(patch, id, title, description, repository, &signer), Some(id) => edit_revision(patch, id, title, description, &signer),
None => edit_root(patch, title, description, repository, &signer), None => edit_root(patch, title, description, &signer),
} }
} }
@ -32,7 +31,6 @@ fn edit_root<G>(
mut patch: patch::PatchMut<'_, '_, Repository, cob::cache::StoreWriter>, mut patch: patch::PatchMut<'_, '_, Repository, cob::cache::StoreWriter>,
title: String, title: String,
description: String, description: String,
repository: &Repository,
signer: &G, signer: &G,
) -> anyhow::Result<()> ) -> anyhow::Result<()>
where where
@ -56,11 +54,7 @@ where
let (root, _) = patch.root(); let (root, _) = patch.root();
let target = patch.target(); let target = patch.target();
let embeds = patch let embeds = patch.embeds().to_owned();
.embeds()
.iter()
.filter_map(|embed| resolve_embed(repository, embed.clone()))
.collect::<Vec<_>>();
patch.transaction("Edit root", signer, |tx| { patch.transaction("Edit root", signer, |tx| {
if let Some(t) = title { if let Some(t) = title {
@ -80,17 +74,12 @@ fn edit_revision<G>(
revision: patch::RevisionId, revision: patch::RevisionId,
mut title: String, mut title: String,
description: String, description: String,
repository: &Repository,
signer: &G, signer: &G,
) -> anyhow::Result<()> ) -> anyhow::Result<()>
where where
G: crypto::Signer, G: crypto::Signer,
{ {
let embeds = patch let embeds = patch.embeds().to_owned();
.embeds()
.iter()
.filter_map(|embed| resolve_embed(repository, embed.clone()))
.collect::<Vec<_>>();
let description = if description.is_empty() { let description = if description.is_empty() {
title title
} else { } else {

View File

@ -341,7 +341,7 @@ fn write_commit(
fn write_manifest( fn write_manifest(
repo: &git2::Repository, repo: &git2::Repository,
manifest: &store::Manifest, manifest: &store::Manifest,
embeds: Vec<Embed>, embeds: Vec<Embed<Oid>>,
contents: &NonEmpty<Vec<u8>>, contents: &NonEmpty<Vec<u8>>,
) -> Result<git2::Oid, git2::Error> { ) -> Result<git2::Oid, git2::Error> {
let mut root = repo.treebuilder(None)?; let mut root = repo.treebuilder(None)?;
@ -372,10 +372,10 @@ fn write_manifest(
let mut embeds_tree = repo.treebuilder(None)?; let mut embeds_tree = repo.treebuilder(None)?;
for embed in embeds { for embed in embeds {
let oid = repo.blob(&embed.content)?; let oid = embed.content;
let path = PathBuf::from(embed.name); let path = PathBuf::from(embed.name);
embeds_tree.insert(path, oid, git2::FileMode::Blob.into())?; embeds_tree.insert(path, *oid, git2::FileMode::Blob.into())?;
} }
let oid = embeds_tree.write()?; let oid = embeds_tree.write()?;

View File

@ -45,7 +45,7 @@ pub struct Template<Id> {
pub type_name: TypeName, pub type_name: TypeName,
pub tips: Vec<Id>, pub tips: Vec<Id>,
pub message: String, pub message: String,
pub embeds: Vec<Embed>, pub embeds: Vec<Embed<Oid>>,
pub contents: NonEmpty<Vec<u8>>, pub contents: NonEmpty<Vec<u8>>,
} }
@ -191,6 +191,22 @@ pub struct Embed<T = Vec<u8>> {
pub content: T, pub content: T,
} }
impl<T: From<Oid>> Embed<T> {
/// Create a new embed.
pub fn store(
name: impl ToString,
content: &[u8],
repo: &git2::Repository,
) -> Result<Self, git2::Error> {
let oid = repo.blob(content)?;
Ok(Self {
name: name.to_string(),
content: T::from(oid.into()),
})
}
}
impl Embed<Vec<u8>> { impl Embed<Vec<u8>> {
/// Get the object id of the embedded content. /// Get the object id of the embedded content.
pub fn oid(&self) -> Oid { pub fn oid(&self) -> Oid {

View File

@ -18,7 +18,7 @@ pub struct Create {
/// The message to add when creating this object. /// The message to add when creating this object.
pub message: String, pub message: String,
/// Embedded content. /// Embedded content.
pub embeds: Vec<Embed>, pub embeds: Vec<Embed<Oid>>,
/// COB version. /// COB version.
pub version: Version, pub version: Version,
} }

View File

@ -34,7 +34,7 @@ pub struct Update {
/// The message to add when updating this object. /// The message to add when updating this object.
pub message: String, pub message: String,
/// Embedded files. /// Embedded files.
pub embeds: Vec<Embed>, pub embeds: Vec<Embed<Oid>>,
} }
/// Update an existing [`CollaborativeObject`]. /// Update an existing [`CollaborativeObject`].

View File

@ -8,10 +8,8 @@ use base64::prelude::{Engine, BASE64_STANDARD};
use localtime::LocalTime; use localtime::LocalTime;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::cob::Embed;
use crate::git::Oid; use crate::git::Oid;
use crate::prelude::{Did, PublicKey}; use crate::prelude::{Did, PublicKey};
use crate::storage::ReadRepository;
/// Timestamp used for COB operations. /// Timestamp used for COB operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
@ -348,24 +346,6 @@ impl TryFrom<&Uri> for DataUri {
} }
} }
/// Resolve an embed with a URI to one with actual data.
pub fn resolve_embed(repo: &impl ReadRepository, embed: Embed<Uri>) -> Option<Embed<Vec<u8>>> {
DataUri::try_from(&embed.content)
.ok()
.map(|content| Embed {
name: embed.name.clone(),
content: content.into(),
})
.or_else(|| {
Oid::try_from(&embed.content).ok().and_then(|oid| {
repo.blob(oid).ok().map(|blob| Embed {
name: embed.name,
content: blob.content().to_vec(),
})
})
})
}
/// The result of an authorization check on an COB action. /// The result of an authorization check on an COB action.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Authorization { pub enum Authorization {

View File

@ -3,7 +3,7 @@ use std::{fmt, ops::Deref, str::FromStr};
use crypto::{PublicKey, Signature}; use crypto::{PublicKey, Signature};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use radicle_cob::{ObjectId, TypeName}; use radicle_cob::{Embed, ObjectId, TypeName};
use radicle_crypto::{Signer, Verified}; use radicle_crypto::{Signer, Verified};
use radicle_git_ext as git_ext; use radicle_git_ext as git_ext;
use radicle_git_ext::Oid; use radicle_git_ext::Oid;
@ -15,7 +15,7 @@ use crate::{
cob::{ cob::{
op, store, op, store,
store::{Cob, CobAction, Transaction}, store::{Cob, CobAction, Transaction},
ActorId, Timestamp, ActorId, Timestamp, Uri,
}, },
identity::{ identity::{
doc::{Doc, DocError, RepoId}, doc::{Doc, DocError, RepoId},
@ -187,10 +187,12 @@ impl Identity {
signer: &G, signer: &G,
) -> Result<IdentityMut<'a, R>, cob::store::Error> { ) -> Result<IdentityMut<'a, R>, cob::store::Error> {
let mut store = cob::store::Store::open(store)?; let mut store = cob::store::Store::open(store)?;
let (id, identity) = let (id, identity) = Transaction::<Identity, _>::initial(
Transaction::<Identity, _>::initial("Initialize identity", &mut store, signer, |tx| { "Initialize identity",
tx.revision("Initial revision", "", doc, None, signer) &mut store,
})?; signer,
|tx, repo| tx.revision("Initial revision", "", doc, None, repo, signer),
)?;
Ok(IdentityMut { Ok(IdentityMut {
id, id,
@ -825,22 +827,26 @@ impl<R: ReadRepository> store::Transaction<Identity, R> {
pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> { pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> {
self.push(Action::RevisionRedact { revision }) self.push(Action::RevisionRedact { revision })
} }
}
impl<R: WriteRepository> store::Transaction<Identity, R> {
pub fn revision<G: Signer>( pub fn revision<G: Signer>(
&mut self, &mut self,
title: impl ToString, title: impl ToString,
description: impl ToString, description: impl ToString,
doc: &Doc<Verified>, doc: &Doc<Verified>,
parent: Option<RevisionId>, parent: Option<RevisionId>,
repo: &R,
signer: &G, signer: &G,
) -> Result<(), store::Error> { ) -> Result<(), store::Error> {
let (blob, content, signature) = doc.sign(signer).map_err(store::Error::Identity)?; let (blob, bytes, signature) = doc.sign(signer).map_err(store::Error::Identity)?;
// Store document blob in repository.
let embed =
Embed::<Uri>::store("radicle.json", &bytes, repo.raw()).map_err(store::Error::Git)?;
debug_assert_eq!(embed.content, Uri::from(blob)); // Make sure we pre-computed the correct OID for the blob.
// Identity document. // Identity document.
self.embed([cob::Embed { self.embed([embed])?;
name: String::from("radicle.json"),
content,
}])?;
// Revision metadata. // Revision metadata.
self.push(Action::Revision { self.push(Action::Revision {
@ -891,10 +897,10 @@ where
) -> Result<EntryId, Error> ) -> Result<EntryId, Error>
where where
G: Signer, G: Signer,
F: FnOnce(&mut Transaction<Identity, R>) -> Result<(), store::Error>, F: FnOnce(&mut Transaction<Identity, R>, &R) -> Result<(), store::Error>,
{ {
let mut tx = Transaction::default(); let mut tx = Transaction::default();
operations(&mut tx)?; operations(&mut tx, self.store.as_ref())?;
let (doc, commit) = tx.commit(message, self.id, &mut self.store, signer)?; let (doc, commit) = tx.commit(message, self.id, &mut self.store, signer)?;
self.identity = doc; self.identity = doc;
@ -912,8 +918,8 @@ where
signer: &G, signer: &G,
) -> Result<RevisionId, Error> { ) -> Result<RevisionId, Error> {
let parent = self.current; let parent = self.current;
let id = self.transaction("Propose revision", signer, |tx| { let id = self.transaction("Propose revision", signer, |tx, repo| {
tx.revision(title, description, doc, Some(parent), signer) tx.revision(title, description, doc, Some(parent), repo, signer)
})?; })?;
Ok(id) Ok(id)
@ -929,7 +935,7 @@ where
let revision = self.revision(revision).ok_or(Error::NotFound(id))?; let revision = self.revision(revision).ok_or(Error::NotFound(id))?;
let signature = revision.sign(signer)?; let signature = revision.sign(signer)?;
self.transaction("Accept revision", signer, |tx| tx.accept(id, signature)) self.transaction("Accept revision", signer, |tx, _| tx.accept(id, signature))
} }
/// Reject an active revision. /// Reject an active revision.
@ -938,7 +944,7 @@ where
revision: RevisionId, revision: RevisionId,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
self.transaction("Reject revision", signer, |tx| tx.reject(revision)) self.transaction("Reject revision", signer, |tx, _| tx.reject(revision))
} }
/// Redact a revision. /// Redact a revision.
@ -947,7 +953,7 @@ where
revision: RevisionId, revision: RevisionId,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
self.transaction("Redact revision", signer, |tx| tx.redact(revision)) self.transaction("Redact revision", signer, |tx, _| tx.redact(revision))
} }
/// Edit an active revision's title or description. /// Edit an active revision's title or description.
@ -958,7 +964,7 @@ where
description: String, description: String,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
self.transaction("Edit revision", signer, |tx| { self.transaction("Edit revision", signer, |tx, _| {
tx.edit(revision, title, description) tx.edit(revision, title, description)
}) })
} }

View File

@ -472,15 +472,13 @@ impl<R: ReadRepository> store::Transaction<Issue, R> {
&mut self, &mut self,
id: CommentId, id: CommentId,
body: impl ToString, body: impl ToString,
embeds: Vec<Embed>, embeds: Vec<Embed<Uri>>,
) -> Result<(), store::Error> { ) -> Result<(), store::Error> {
let hashed = embeds.iter().map(|e| e.hashed()).collect(); self.embed(embeds.clone())?;
self.embed(embeds)?;
self.push(Action::CommentEdit { self.push(Action::CommentEdit {
id, id,
body: body.to_string(), body: body.to_string(),
embeds: hashed, embeds,
}) })
} }
@ -506,15 +504,13 @@ impl<R: ReadRepository> store::Transaction<Issue, R> {
&mut self, &mut self,
body: S, body: S,
reply_to: CommentId, reply_to: CommentId,
embeds: Vec<Embed>, embeds: Vec<Embed<Uri>>,
) -> Result<(), store::Error> { ) -> Result<(), store::Error> {
let hashed = embeds.iter().map(|e| e.hashed()).collect(); self.embed(embeds.clone())?;
self.embed(embeds)?;
self.push(Action::Comment { self.push(Action::Comment {
body: body.to_string(), body: body.to_string(),
reply_to: Some(reply_to), reply_to: Some(reply_to),
embeds: hashed, embeds,
}) })
} }
@ -545,16 +541,15 @@ impl<R: ReadRepository> store::Transaction<Issue, R> {
fn thread<S: ToString>( fn thread<S: ToString>(
&mut self, &mut self,
body: S, body: S,
embeds: impl IntoIterator<Item = Embed>, embeds: impl IntoIterator<Item = Embed<Uri>>,
) -> Result<(), store::Error> { ) -> Result<(), store::Error> {
let embeds = embeds.into_iter().collect::<Vec<_>>(); let embeds = embeds.into_iter().collect::<Vec<_>>();
let hashed = embeds.iter().map(|e| e.hashed()).collect();
self.embed(embeds)?; self.embed(embeds.clone())?;
self.push(Action::Comment { self.push(Action::Comment {
body: body.to_string(), body: body.to_string(),
reply_to: None, reply_to: None,
embeds: hashed, embeds,
}) })
} }
} }
@ -613,7 +608,7 @@ where
pub fn edit_description<G: Signer>( pub fn edit_description<G: Signer>(
&mut self, &mut self,
description: impl ToString, description: impl ToString,
embeds: impl IntoIterator<Item = Embed>, embeds: impl IntoIterator<Item = Embed<Uri>>,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
let (id, _) = self.root(); let (id, _) = self.root();
@ -633,7 +628,7 @@ where
&mut self, &mut self,
body: S, body: S,
reply_to: CommentId, reply_to: CommentId,
embeds: impl IntoIterator<Item = Embed>, embeds: impl IntoIterator<Item = Embed<Uri>>,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
self.transaction("Comment", signer, |tx| { self.transaction("Comment", signer, |tx| {
@ -646,7 +641,7 @@ where
&mut self, &mut self,
id: CommentId, id: CommentId,
body: S, body: S,
embeds: impl IntoIterator<Item = Embed>, embeds: impl IntoIterator<Item = Embed<Uri>>,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
self.transaction("Edit comment", signer, |tx| { self.transaction("Edit comment", signer, |tx| {
@ -777,7 +772,7 @@ where
description: impl ToString, description: impl ToString,
labels: &[Label], labels: &[Label],
assignees: &[Did], assignees: &[Did],
embeds: impl IntoIterator<Item = Embed>, embeds: impl IntoIterator<Item = Embed<Uri>>,
cache: &'g mut C, cache: &'g mut C,
signer: &G, signer: &G,
) -> Result<IssueMut<'a, 'g, R, C>, Error> ) -> Result<IssueMut<'a, 'g, R, C>, Error>
@ -785,7 +780,7 @@ where
G: Signer, G: Signer,
C: cob::cache::Update<Issue>, C: cob::cache::Update<Issue>,
{ {
let (id, issue) = Transaction::initial("Create issue", &mut self.raw, signer, |tx| { let (id, issue) = Transaction::initial("Create issue", &mut self.raw, signer, |tx, _| {
tx.thread(description, embeds)?; tx.thread(description, embeds)?;
tx.edit(title)?; tx.edit(title)?;
@ -1511,17 +1506,22 @@ mod test {
fn test_embeds() { fn test_embeds() {
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default(); let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
let mut issues = Cache::no_cache(&*repo).unwrap(); let mut issues = Cache::no_cache(&*repo).unwrap();
let content1 = repo.backend.blob(b"<html>Hello World!</html>").unwrap();
let content2 = repo.backend.blob(b"<html>Hello Radicle!</html>").unwrap();
let content3 = repo.backend.blob(b"body { color: red }").unwrap();
let embed1 = Embed { let embed1 = Embed {
name: String::from("example.html"), name: String::from("example.html"),
content: b"<html>Hello World!</html>".to_vec(), content: Uri::from(Oid::from(content1)),
}; };
let embed2 = Embed { let embed2 = Embed {
name: String::from("style.css"), name: String::from("style.css"),
content: b"body { color: red }".to_vec(), content: Uri::from(Oid::from(content2)),
}; };
let embed3 = Embed { let embed3 = Embed {
name: String::from("bin"), name: String::from("bin"),
content: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9], content: Uri::from(Oid::from(content3)),
}; };
let mut issue = issues let mut issue = issues
.create( .create(
@ -1552,34 +1552,35 @@ mod test {
let e2 = &c0.embeds()[1]; let e2 = &c0.embeds()[1];
let e3 = &c1.embeds()[0]; let e3 = &c1.embeds()[0];
let b1 = repo.blob(Oid::try_from(&e1.content).unwrap()).unwrap(); let b1 = Oid::try_from(&e1.content).unwrap();
let b2 = repo.blob(Oid::try_from(&e2.content).unwrap()).unwrap(); let b2 = Oid::try_from(&e2.content).unwrap();
let b3 = repo.blob(Oid::try_from(&e3.content).unwrap()).unwrap(); let b3 = Oid::try_from(&e3.content).unwrap();
assert_eq!(b1.content(), &embed1.content); assert_eq!(b1, Oid::try_from(&embed1.content).unwrap());
assert_eq!(b2.content(), &embed2.content); assert_eq!(b2, Oid::try_from(&embed2.content).unwrap());
assert_eq!(b3.content(), &embed3.content); assert_eq!(b3, Oid::try_from(&embed3.content).unwrap());
assert_eq!(b1.is_binary(), false);
assert_eq!(b2.is_binary(), false);
assert_eq!(b3.is_binary(), true);
} }
#[test] #[test]
fn test_embeds_edit() { fn test_embeds_edit() {
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default(); let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
let mut issues = Cache::no_cache(&*repo).unwrap(); let mut issues = Cache::no_cache(&*repo).unwrap();
let content1 = repo.backend.blob(b"<html>Hello World!</html>").unwrap();
let content1_edited = repo.backend.blob(b"<html>Hello Radicle!</html>").unwrap();
let content2 = repo.backend.blob(b"body { color: red }").unwrap();
let embed1 = Embed { let embed1 = Embed {
name: String::from("example.html"), name: String::from("example.html"),
content: b"<html>Hello World!</html>".to_vec(), content: Uri::from(Oid::from(content1)),
}; };
let embed1_edited = Embed { let embed1_edited = Embed {
name: String::from("example.html"), name: String::from("style.css"),
content: b"<html>Hello Radicle!</html>".to_vec(), content: Uri::from(Oid::from(content1_edited)),
}; };
let embed2 = Embed { let embed2 = Embed {
name: String::from("style.css"), name: String::from("bin"),
content: b"body { color: red }".to_vec(), content: Uri::from(Oid::from(content2)),
}; };
let mut issue = issues let mut issue = issues
.create( .create(
@ -1603,10 +1604,10 @@ mod test {
assert_eq!(c0.embeds().len(), 1); assert_eq!(c0.embeds().len(), 1);
let e1 = &c0.embeds()[0]; let e1 = &c0.embeds()[0];
let b1 = repo.blob(Oid::try_from(&e1.content).unwrap()).unwrap(); let b1 = Oid::try_from(&e1.content).unwrap();
assert_eq!(e1.content, Uri::from(embed1_edited.oid())); assert_eq!(e1.content, embed1_edited.content);
assert_eq!(b1.content(), &embed1_edited.content); assert_eq!(b1, Oid::try_from(&embed1_edited.content).unwrap());
} }
#[test] #[test]

View File

@ -8,7 +8,7 @@ use crate::cob;
use crate::cob::cache; use crate::cob::cache;
use crate::cob::cache::{Remove, StoreReader, StoreWriter, Update}; use crate::cob::cache::{Remove, StoreReader, StoreWriter, Update};
use crate::cob::store; use crate::cob::store;
use crate::cob::{Embed, Label, ObjectId, TypeName}; use crate::cob::{Embed, Label, ObjectId, TypeName, Uri};
use crate::crypto::Signer; use crate::crypto::Signer;
use crate::prelude::{Did, RepoId}; use crate::prelude::{Did, RepoId};
use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository}; use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository};
@ -79,7 +79,7 @@ impl<'a, R, C> Cache<super::Issues<'a, R>, C> {
description: impl ToString, description: impl ToString,
labels: &[Label], labels: &[Label],
assignees: &[Did], assignees: &[Did],
embeds: impl IntoIterator<Item = Embed>, embeds: impl IntoIterator<Item = Embed<Uri>>,
signer: &G, signer: &G,
) -> Result<IssueMut<'a, 'g, R, C>, super::Error> ) -> Result<IssueMut<'a, 'g, R, C>, super::Error>
where where

View File

@ -475,7 +475,7 @@ impl Patch {
} }
/// Patch embeds. /// Patch embeds.
pub fn embeds(&self) -> &Vec<Embed<Uri>> { pub fn embeds(&self) -> &[Embed<Uri>] {
let (_, r) = self.root(); let (_, r) = self.root();
r.embeds() r.embeds()
} }
@ -1410,7 +1410,7 @@ impl Revision {
self.description.iter() self.description.iter()
} }
pub fn embeds(&self) -> &Vec<Embed<Uri>> { pub fn embeds(&self) -> &[Embed<Uri>] {
&self.description.last().embeds &self.description.last().embeds
} }
@ -1663,15 +1663,13 @@ impl<R: ReadRepository> store::Transaction<Patch, R> {
&mut self, &mut self,
revision: RevisionId, revision: RevisionId,
description: impl ToString, description: impl ToString,
embeds: Vec<Embed>, embeds: Vec<Embed<Uri>>,
) -> Result<(), store::Error> { ) -> Result<(), store::Error> {
let hashed = embeds.iter().map(|e| e.hashed()).collect(); self.embed(embeds.clone())?;
self.embed(embeds)?;
self.push(Action::RevisionEdit { self.push(Action::RevisionEdit {
revision, revision,
description: description.to_string(), description: description.to_string(),
embeds: hashed, embeds,
}) })
} }
@ -1718,17 +1716,15 @@ impl<R: ReadRepository> store::Transaction<Patch, R> {
body: S, body: S,
reply_to: Option<CommentId>, reply_to: Option<CommentId>,
location: Option<CodeLocation>, location: Option<CodeLocation>,
embeds: Vec<Embed>, embeds: Vec<Embed<Uri>>,
) -> Result<(), store::Error> { ) -> Result<(), store::Error> {
let hashed = embeds.iter().map(|e| e.hashed()).collect(); self.embed(embeds.clone())?;
self.embed(embeds)?;
self.push(Action::RevisionComment { self.push(Action::RevisionComment {
revision, revision,
body: body.to_string(), body: body.to_string(),
reply_to, reply_to,
location, location,
embeds: hashed, embeds,
}) })
} }
@ -1738,16 +1734,14 @@ impl<R: ReadRepository> store::Transaction<Patch, R> {
revision: RevisionId, revision: RevisionId,
comment: CommentId, comment: CommentId,
body: S, body: S,
embeds: Vec<Embed>, embeds: Vec<Embed<Uri>>,
) -> Result<(), store::Error> { ) -> Result<(), store::Error> {
let hashed = embeds.iter().map(|e| e.hashed()).collect(); self.embed(embeds.clone())?;
self.embed(embeds)?;
self.push(Action::RevisionCommentEdit { self.push(Action::RevisionCommentEdit {
revision, revision,
comment, comment,
body: body.to_string(), body: body.to_string(),
embeds: hashed, embeds,
}) })
} }
@ -1783,17 +1777,15 @@ impl<R: ReadRepository> store::Transaction<Patch, R> {
body: S, body: S,
location: Option<CodeLocation>, location: Option<CodeLocation>,
reply_to: Option<CommentId>, reply_to: Option<CommentId>,
embeds: Vec<Embed>, embeds: Vec<Embed<Uri>>,
) -> Result<(), store::Error> { ) -> Result<(), store::Error> {
let hashed = embeds.iter().map(|e| e.hashed()).collect(); self.embed(embeds.clone())?;
self.embed(embeds)?;
self.push(Action::ReviewComment { self.push(Action::ReviewComment {
review, review,
body: body.to_string(), body: body.to_string(),
location, location,
reply_to, reply_to,
embeds: hashed, embeds,
}) })
} }
@ -1821,16 +1813,14 @@ impl<R: ReadRepository> store::Transaction<Patch, R> {
review: ReviewId, review: ReviewId,
comment: EntryId, comment: EntryId,
body: S, body: S,
embeds: Vec<Embed>, embeds: Vec<Embed<Uri>>,
) -> Result<(), store::Error> { ) -> Result<(), store::Error> {
let hashed = embeds.iter().map(|e| e.hashed()).collect(); self.embed(embeds.clone())?;
self.embed(embeds)?;
self.push(Action::ReviewCommentEdit { self.push(Action::ReviewCommentEdit {
review, review,
comment, comment,
body: body.to_string(), body: body.to_string(),
embeds: hashed, embeds,
}) })
} }
@ -2011,7 +2001,7 @@ where
&mut self, &mut self,
revision: RevisionId, revision: RevisionId,
description: String, description: String,
embeds: impl IntoIterator<Item = Embed>, embeds: impl IntoIterator<Item = Embed<Uri>>,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
self.transaction("Edit revision", signer, |tx| { self.transaction("Edit revision", signer, |tx| {
@ -2045,7 +2035,7 @@ where
body: S, body: S,
reply_to: Option<CommentId>, reply_to: Option<CommentId>,
location: Option<CodeLocation>, location: Option<CodeLocation>,
embeds: impl IntoIterator<Item = Embed>, embeds: impl IntoIterator<Item = Embed<Uri>>,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
self.transaction("Comment", signer, |tx| { self.transaction("Comment", signer, |tx| {
@ -2079,7 +2069,7 @@ where
revision: RevisionId, revision: RevisionId,
comment: CommentId, comment: CommentId,
body: S, body: S,
embeds: impl IntoIterator<Item = Embed>, embeds: impl IntoIterator<Item = Embed<Uri>>,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
self.transaction("Edit comment", signer, |tx| { self.transaction("Edit comment", signer, |tx| {
@ -2120,7 +2110,7 @@ where
body: S, body: S,
location: Option<CodeLocation>, location: Option<CodeLocation>,
reply_to: Option<CommentId>, reply_to: Option<CommentId>,
embeds: impl IntoIterator<Item = Embed>, embeds: impl IntoIterator<Item = Embed<Uri>>,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
self.transaction("Review comment", signer, |tx| { self.transaction("Review comment", signer, |tx| {
@ -2140,7 +2130,7 @@ where
review: ReviewId, review: ReviewId,
comment: EntryId, comment: EntryId,
body: S, body: S,
embeds: impl IntoIterator<Item = Embed>, embeds: impl IntoIterator<Item = Embed<Uri>>,
signer: &G, signer: &G,
) -> Result<EntryId, Error> { ) -> Result<EntryId, Error> {
self.transaction("Edit review comment", signer, |tx| { self.transaction("Edit review comment", signer, |tx| {
@ -2567,7 +2557,7 @@ where
where where
C: cob::cache::Update<Patch>, C: cob::cache::Update<Patch>,
{ {
let (id, patch) = Transaction::initial("Create patch", &mut self.raw, signer, |tx| { let (id, patch) = Transaction::initial("Create patch", &mut self.raw, signer, |tx, _| {
tx.revision(description, base, oid)?; tx.revision(description, base, oid)?;
tx.edit(title, target)?; tx.edit(title, target)?;

View File

@ -9,7 +9,7 @@ use radicle_cob::CollaborativeObject;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::cob::op::Op; use crate::cob::op::Op;
use crate::cob::{Create, Embed, EntryId, ObjectId, TypeName, Update, Updated, Version}; use crate::cob::{Create, Embed, EntryId, ObjectId, TypeName, Update, Updated, Uri, Version};
use crate::git; use crate::git;
use crate::prelude::*; use crate::prelude::*;
use crate::storage::git as storage; use crate::storage::git as storage;
@ -92,6 +92,10 @@ pub enum Error {
NotFound(TypeName, ObjectId), NotFound(TypeName, ObjectId),
#[error("signed refs: {0}")] #[error("signed refs: {0}")]
SignRefs(#[from] storage::Error), SignRefs(#[from] storage::Error),
#[error("invalid or unknown embed URI: {0}")]
EmbedUri(Uri),
#[error(transparent)]
Git(git::raw::Error),
#[error("failed to find reference '{name}': {err}")] #[error("failed to find reference '{name}': {err}")]
RefLookup { RefLookup {
name: git::RefString, name: git::RefString,
@ -148,12 +152,21 @@ where
object_id: ObjectId, object_id: ObjectId,
message: &str, message: &str,
actions: impl Into<NonEmpty<T::Action>>, actions: impl Into<NonEmpty<T::Action>>,
embeds: Vec<Embed>, embeds: Vec<Embed<Uri>>,
signer: &G, signer: &G,
) -> Result<Updated<T>, Error> { ) -> Result<Updated<T>, Error> {
let actions = actions.into(); let actions = actions.into();
let related = actions.iter().flat_map(T::Action::parents).collect(); let related = actions.iter().flat_map(T::Action::parents).collect();
let changes = actions.try_map(encoding::encode)?; let changes = actions.try_map(encoding::encode)?;
let embeds = embeds
.into_iter()
.map(|e| {
Ok::<_, Error>(Embed {
content: git::Oid::try_from(&e.content).map_err(Error::EmbedUri)?,
name: e.name.clone(),
})
})
.collect::<Result<_, _>>()?;
let updated = cob::update( let updated = cob::update(
self.repo, self.repo,
signer, signer,
@ -178,12 +191,21 @@ where
&self, &self,
message: &str, message: &str,
actions: impl Into<NonEmpty<T::Action>>, actions: impl Into<NonEmpty<T::Action>>,
embeds: Vec<Embed>, embeds: Vec<Embed<Uri>>,
signer: &G, signer: &G,
) -> Result<(ObjectId, T), Error> { ) -> Result<(ObjectId, T), Error> {
let actions = actions.into(); let actions = actions.into();
let parents = actions.iter().flat_map(T::Action::parents).collect(); let parents = actions.iter().flat_map(T::Action::parents).collect();
let contents = actions.try_map(encoding::encode)?; let contents = actions.try_map(encoding::encode)?;
let embeds = embeds
.into_iter()
.map(|e| {
Ok::<_, Error>(Embed {
content: git::Oid::try_from(&e.content).map_err(Error::EmbedUri)?,
name: e.name.clone(),
})
})
.collect::<Result<_, _>>()?;
let cob = cob::create::<T, _, G>( let cob = cob::create::<T, _, G>(
self.repo, self.repo,
signer, signer,
@ -263,7 +285,7 @@ where
#[derive(Debug)] #[derive(Debug)]
pub struct Transaction<T: Cob + cob::Evaluate<R>, R> { pub struct Transaction<T: Cob + cob::Evaluate<R>, R> {
actions: Vec<T::Action>, actions: Vec<T::Action>,
embeds: Vec<Embed>, embeds: Vec<Embed<Uri>>,
repo: PhantomData<R>, repo: PhantomData<R>,
} }
@ -290,12 +312,12 @@ where
) -> Result<(ObjectId, T), Error> ) -> Result<(ObjectId, T), Error>
where where
G: Signer, G: Signer,
F: FnOnce(&mut Self) -> Result<(), Error>, F: FnOnce(&mut Self, &R) -> Result<(), Error>,
R: ReadRepository + SignRepository + cob::Store, R: ReadRepository + SignRepository + cob::Store,
T::Action: Serialize + Clone, T::Action: Serialize + Clone,
{ {
let mut tx = Transaction::default(); let mut tx = Transaction::default();
operations(&mut tx)?; operations(&mut tx, store.as_ref())?;
let actions = NonEmpty::from_vec(tx.actions) let actions = NonEmpty::from_vec(tx.actions)
.expect("Transaction::initial: transaction must contain at least one action"); .expect("Transaction::initial: transaction must contain at least one action");
@ -311,7 +333,7 @@ where
} }
/// Embed media into the transaction. /// Embed media into the transaction.
pub fn embed(&mut self, embeds: impl IntoIterator<Item = Embed>) -> Result<(), Error> { pub fn embed(&mut self, embeds: impl IntoIterator<Item = Embed<Uri>>) -> Result<(), Error> {
self.embeds.extend(embeds); self.embeds.extend(embeds);
Ok(()) Ok(())

View File

@ -439,9 +439,13 @@ impl Repository {
storage: &S, storage: &S,
signer: &G, signer: &G,
) -> Result<(Self, git::Oid), RepositoryError> { ) -> Result<(Self, git::Oid), RepositoryError> {
let (doc_oid, _) = doc.encode()?; let (doc_oid, doc_bytes) = doc.encode()?;
let id = RepoId::from(doc_oid); let id = RepoId::from(doc_oid);
let repo = Self::create(paths::repository(storage, &id), id, storage.info())?; let repo = Self::create(paths::repository(storage, &id), id, storage.info())?;
let oid = repo.backend.blob(&doc_bytes)?; // Store document blob in repository.
debug_assert_eq!(oid, *doc_oid);
let commit = doc.init(&repo, signer)?; let commit = doc.init(&repo, signer)?;
Ok((repo, commit)) Ok((repo, commit))