Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cob: Change APIs to take URIs for embeds
cloudhead committed 1 year ago
commit 855327d303566aac1d3e065d4ba7e16d9ccd18e6
parent 034eb418600f01ffc27b84ad399372410d49cd13
12 files changed +152 -144
modified radicle-cli/src/commands/patch/edit.rs
@@ -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 {
modified radicle-cob/src/backend/git/change.rs
@@ -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()?;

modified radicle-cob/src/change/store.rs
@@ -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 {
modified radicle-cob/src/object/collaboration/create.rs
@@ -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,
}
modified radicle-cob/src/object/collaboration/update.rs
@@ -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`].
modified radicle/src/cob/common.rs
@@ -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 {
modified radicle/src/cob/identity.rs
@@ -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)
        })
    }
modified radicle/src/cob/issue.rs
@@ -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();
-

-
        assert_eq!(b1.content(), &embed1.content);
-
        assert_eq!(b2.content(), &embed2.content);
-
        assert_eq!(b3.content(), &embed3.content);
+
        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.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]
modified radicle/src/cob/issue/cache.rs
@@ -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
modified radicle/src/cob/patch.rs
@@ -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)?;

modified radicle/src/cob/store.rs
@@ -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(())
modified radicle/src/storage/git.rs
@@ -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))