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

View File

@ -341,7 +341,7 @@ fn write_commit(
fn write_manifest(
repo: &git2::Repository,
manifest: &store::Manifest,
embeds: Vec<Embed>,
embeds: Vec<Embed<Oid>>,
contents: &NonEmpty<Vec<u8>>,
) -> Result<git2::Oid, git2::Error> {
let mut root = repo.treebuilder(None)?;
@ -372,10 +372,10 @@ fn write_manifest(
let mut embeds_tree = repo.treebuilder(None)?;
for embed in embeds {
let oid = repo.blob(&embed.content)?;
let oid = embed.content;
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()?;

View File

@ -45,7 +45,7 @@ pub struct Template<Id> {
pub type_name: TypeName,
pub tips: Vec<Id>,
pub message: String,
pub embeds: Vec<Embed>,
pub embeds: Vec<Embed<Oid>>,
pub contents: NonEmpty<Vec<u8>>,
}
@ -191,6 +191,22 @@ pub struct Embed<T = Vec<u8>> {
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>> {
/// Get the object id of the embedded content.
pub fn oid(&self) -> Oid {

View File

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

View File

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

View File

@ -8,10 +8,8 @@ use base64::prelude::{Engine, BASE64_STANDARD};
use localtime::LocalTime;
use serde::{Deserialize, Serialize};
use crate::cob::Embed;
use crate::git::Oid;
use crate::prelude::{Did, PublicKey};
use crate::storage::ReadRepository;
/// Timestamp used for COB operations.
#[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.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Authorization {

View File

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

View File

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

View File

@ -8,7 +8,7 @@ use crate::cob;
use crate::cob::cache;
use crate::cob::cache::{Remove, StoreReader, StoreWriter, Update};
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::prelude::{Did, RepoId};
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,
labels: &[Label],
assignees: &[Did],
embeds: impl IntoIterator<Item = Embed>,
embeds: impl IntoIterator<Item = Embed<Uri>>,
signer: &G,
) -> Result<IssueMut<'a, 'g, R, C>, super::Error>
where

View File

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

View File

@ -9,7 +9,7 @@ use radicle_cob::CollaborativeObject;
use serde::{Deserialize, Serialize};
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::prelude::*;
use crate::storage::git as storage;
@ -92,6 +92,10 @@ pub enum Error {
NotFound(TypeName, ObjectId),
#[error("signed refs: {0}")]
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}")]
RefLookup {
name: git::RefString,
@ -148,12 +152,21 @@ where
object_id: ObjectId,
message: &str,
actions: impl Into<NonEmpty<T::Action>>,
embeds: Vec<Embed>,
embeds: Vec<Embed<Uri>>,
signer: &G,
) -> Result<Updated<T>, Error> {
let actions = actions.into();
let related = actions.iter().flat_map(T::Action::parents).collect();
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(
self.repo,
signer,
@ -178,12 +191,21 @@ where
&self,
message: &str,
actions: impl Into<NonEmpty<T::Action>>,
embeds: Vec<Embed>,
embeds: Vec<Embed<Uri>>,
signer: &G,
) -> Result<(ObjectId, T), Error> {
let actions = actions.into();
let parents = actions.iter().flat_map(T::Action::parents).collect();
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>(
self.repo,
signer,
@ -263,7 +285,7 @@ where
#[derive(Debug)]
pub struct Transaction<T: Cob + cob::Evaluate<R>, R> {
actions: Vec<T::Action>,
embeds: Vec<Embed>,
embeds: Vec<Embed<Uri>>,
repo: PhantomData<R>,
}
@ -290,12 +312,12 @@ where
) -> Result<(ObjectId, T), Error>
where
G: Signer,
F: FnOnce(&mut Self) -> Result<(), Error>,
F: FnOnce(&mut Self, &R) -> Result<(), Error>,
R: ReadRepository + SignRepository + cob::Store,
T::Action: Serialize + Clone,
{
let mut tx = Transaction::default();
operations(&mut tx)?;
operations(&mut tx, store.as_ref())?;
let actions = NonEmpty::from_vec(tx.actions)
.expect("Transaction::initial: transaction must contain at least one action");
@ -311,7 +333,7 @@ where
}
/// 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);
Ok(())

View File

@ -439,9 +439,13 @@ impl Repository {
storage: &S,
signer: &G,
) -> Result<(Self, git::Oid), RepositoryError> {
let (doc_oid, _) = doc.encode()?;
let (doc_oid, doc_bytes) = doc.encode()?;
let id = RepoId::from(doc_oid);
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)?;
Ok((repo, commit))