diff --git a/crates/radicle-cli/src/commands/cob.rs b/crates/radicle-cli/src/commands/cob.rs index 12b92364..3a7a818a 100644 --- a/crates/radicle-cli/src/commands/cob.rs +++ b/crates/radicle-cli/src/commands/cob.rs @@ -11,6 +11,8 @@ use nonempty::NonEmpty; use radicle::cob; use radicle::cob::store::CobAction; +use radicle::cob::store::access::ReadOnly; +use radicle::cob::store::access::WriteAs; use radicle::cob::stream::CobStream as _; use radicle::git; use radicle::prelude::*; @@ -48,21 +50,22 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { type_name, operation, }) => { - let signer = &profile.signer()?; + let signer = profile.signer()?; + let access = WriteAs::new(&signer); let repo = storage.repository_mut(repo)?; let embeds = embeds(&repo, operation.embed_files, operation.embed_hashes)?; let oid = match type_name { Patch => { - let store: Store = Store::open(&repo)?; + let mut store: Store = Store::open(&repo, access)?; let actions = read_jsonl_actions(&operation.actions)?; - let (oid, _) = store.create(&operation.message, actions, embeds, signer)?; + let (oid, _) = store.create(&operation.message, actions, embeds)?; oid } Issue => { - let store: Store = Store::open(&repo)?; + let mut store: Store = Store::open(&repo, access)?; let actions = read_jsonl_actions(&operation.actions)?; - let (oid, _) = store.create(&operation.message, actions, embeds, signer)?; + let (oid, _) = store.create(&operation.message, actions, embeds)?; oid } Identity => anyhow::bail!( @@ -70,10 +73,10 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { &type_name ), Other(type_name) => { - let store: Store = - Store::open_for(&type_name, &repo)?; + let mut store: Store = + Store::open_for(&type_name, &repo, access)?; let actions = read_jsonl_actions(&operation.actions)?; - let (oid, _) = store.create(&operation.message, actions, embeds, signer)?; + let (oid, _) = store.create(&operation.message, actions, embeds)?; oid } }; @@ -178,9 +181,9 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { Patch => { let actions: Vec = read_jsonl_actions(&operation.actions)?.into(); - let mut patches = profile.patches_mut(&repo)?; + let mut patches = crate::terminal::cob::patches_mut(&profile, &repo, signer)?; let mut patch = patches.get_mut(&oid)?; - patch.transaction(&operation.message, &*profile.signer()?, |tx| { + patch.transaction(&operation.message, |tx| { tx.extend(actions)?; tx.embed(embeds)?; Ok(()) @@ -189,9 +192,9 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { Issue => { let actions: Vec = read_jsonl_actions(&operation.actions)?.into(); - let mut issues = profile.issues_mut(&repo)?; + let mut issues = crate::terminal::cob::issues_mut(&profile, &repo, signer)?; let mut issue = issues.get_mut(&oid)?; - issue.transaction(&operation.message, &*profile.signer()?, |tx| { + issue.transaction(&operation.message, |tx| { tx.extend(actions)?; tx.embed(embeds)?; Ok(()) @@ -204,9 +207,10 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { Other(type_name) => { use cob::external::{Action, External}; let actions: Vec = read_jsonl_actions(&operation.actions)?.into(); - let mut store: Store = Store::open_for(&type_name, &repo)?; + let mut store: Store = + Store::open_for(&type_name, &repo, WriteAs::new(signer))?; let tx = cob::store::Transaction::new(type_name.clone(), actions, embeds); - let (_, oid) = tx.commit(&operation.message, oid, &mut store, signer)?; + let (_, oid) = tx.commit(&operation.message, oid, &mut store)?; oid } }; @@ -244,6 +248,7 @@ fn show( } FilteredTypeName::Issue => { use radicle::issue::cache::Issues as _; + let issues = term::cob::issues(profile, repo)?; for oid in oids { let oid = &oid.resolve(&repo.backend)?; @@ -259,6 +264,7 @@ fn show( } FilteredTypeName::Patch => { use radicle::patch::cache::Patches as _; + let patches = term::cob::patches(profile, repo)?; for oid in oids { let oid = &oid.resolve(&repo.backend)?; @@ -273,8 +279,9 @@ fn show( } } FilteredTypeName::Other(type_name) => { - let store = - cob::store::Store::::open_for(&type_name, repo)?; + let store = cob::store::Store::::open_for( + &type_name, repo, ReadOnly, + )?; for oid in oids { let oid = &oid.resolve(&repo.backend)?; let cob = store diff --git a/crates/radicle-cli/src/commands/id.rs b/crates/radicle-cli/src/commands/id.rs index f4018f53..c2e8c994 100644 --- a/crates/radicle-cli/src/commands/id.rs +++ b/crates/radicle-cli/src/commands/id.rs @@ -9,7 +9,6 @@ use radicle::cob::identity::{self, IdentityMut, Revision, RevisionId}; use radicle::identity::doc::update; use radicle::identity::{Doc, Identity, RawDoc, doc}; use radicle::node::NodeId; -use radicle::node::device::Device; use radicle::storage::{ReadStorage as _, WriteRepository}; use radicle::{Profile, cob, crypto}; use radicle_surf::diff::Diff; @@ -37,7 +36,9 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { let repo = storage .repository(rid) .context(anyhow!("repository `{rid}` not found in local storage"))?; - let mut identity = Identity::load_mut(&repo)?; + + let device = profile.signer()?; + let mut identity = Identity::load_mut(&repo, &device)?; let current = identity.current().clone(); let interactive = args.interactive(); @@ -47,14 +48,13 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { Command::Accept { revision } => { let revision = get(revision, &identity, &repo)?.clone(); let id = revision.id; - let signer = term::signer(&profile)?; if !revision.is_active() { anyhow::bail!("cannot vote on revision that is {}", revision.state); } if interactive.confirm(format!("Accept revision {}?", term::format::tertiary(id))) { - identity.accept(&revision.id, &signer)?; + identity.accept(&revision.id)?; if let Some(revision) = identity.revision(&id) { // Update the canonical head to point to the latest accepted revision. @@ -72,7 +72,6 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { } Command::Reject { revision } => { let revision = get(revision, &identity, &repo)?.clone(); - let signer = term::signer(&profile)?; if !revision.is_active() { anyhow::bail!("cannot vote on revision that is {}", revision.state); @@ -82,7 +81,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { "Reject revision {}?", term::format::tertiary(revision.id) )) { - identity.reject(revision.id, &signer)?; + identity.reject(revision.id)?; if !args.quiet { term::success!("Revision {} rejected", revision.id); @@ -96,7 +95,6 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { description, } => { let revision = get(revision, &identity, &repo)?.clone(); - let signer = term::signer(&profile)?; if !revision.is_active() { anyhow::bail!("revision can no longer be edited"); @@ -104,7 +102,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { let Some((title, description)) = edit_title_description(title, description)? else { anyhow::bail!("revision title or description missing"); }; - identity.edit(revision.id, title, description, &signer)?; + identity.edit(revision.id, title, description)?; if !args.quiet { term::success!("Revision {} edited", revision.id); @@ -193,15 +191,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { } return Ok(()); } - let signer = term::signer(&profile)?; - let revision = update( - title, - description, - proposal, - &mut identity, - &signer, - &profile, - )?; + let revision = update(title, description, proposal, &mut identity, &profile)?; if revision.is_accepted() && revision.parent == Some(current.id) { // Update the canonical head to point to the latest accepted revision. @@ -253,7 +243,6 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { } Command::Redact { revision } => { let revision = get(revision, &identity, &repo)?.clone(); - let signer = term::signer(&profile)?; if revision.is_accepted() { anyhow::bail!("cannot redact accepted revision"); @@ -262,7 +251,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { "Redact revision {}?", term::format::tertiary(revision.id) )) { - identity.redact(revision.id, &signer)?; + identity.redact(revision.id)?; if !args.quiet { term::success!("Revision {} redacted", revision.id); @@ -422,21 +411,23 @@ and description. Ok(result) } -fn update( +fn update( title: Option, description: Option<String>, doc: Doc, - current: &mut IdentityMut<R>, - signer: &Device<G>, + current: &mut IdentityMut<Repo, Signer>, profile: &Profile, ) -> anyhow::Result<Revision> where - R: WriteRepository + cob::Store<Namespace = NodeId>, - G: crypto::signature::Signer<crypto::Signature>, + Repo: WriteRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { if let Some((title, description)) = edit_title_description(title, description)? { let id = current - .update(title, description, &doc, signer) + .update(title, description, &doc) .map_err(|e| on_identity_err(e, profile))?; let revision = current .revision(&id) diff --git a/crates/radicle-cli/src/commands/issue.rs b/crates/radicle-cli/src/commands/issue.rs index 2efc4d21..d18dd484 100644 --- a/crates/radicle-cli/src/commands/issue.rs +++ b/crates/radicle-cli/src/commands/issue.rs @@ -6,13 +6,13 @@ use anyhow::Context as _; use radicle::cob::common::Label; use radicle::cob::issue::{CloseReason, State}; +use radicle::cob::store::access::WriteAs; use radicle::cob::{Title, issue}; use radicle::Profile; use radicle::crypto; use radicle::issue::cache::Issues as _; use radicle::node::NodeId; -use radicle::node::device::Device; use radicle::prelude::Did; use radicle::profile; use radicle::storage; @@ -48,7 +48,8 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { .unwrap_or_else(|| Command::List(args.empty.into())); let announce = !args.no_announce && command.should_announce_for(); - let mut issues = term::cob::issues_mut(&profile, &repo)?; + let signer = profile.signer()?; + let mut issues = term::cob::issues_mut(&profile, &repo, &signer)?; match command { Command::Edit { @@ -56,8 +57,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { title, description, } => { - let signer = term::signer(&profile)?; - let issue = edit(&mut issues, &repo, id, title, description, &signer)?; + let issue = edit(&mut issues, &repo, id, title, description)?; if !args.quiet { term::issue::show(&issue, issue.id(), Format::Header, args.verbose, &profile)?; } @@ -68,7 +68,6 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { labels, assignees, } => { - let signer = term::signer(&profile)?; open( title, description, @@ -77,7 +76,6 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { args.verbose, args.quiet, &mut issues, - &signer, &profile, )?; } @@ -131,10 +129,9 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { Command::State { id, target_state } => { let to: StateArg = target_state.into(); let id = id.resolve(&repo.backend)?; - let signer = term::signer(&profile)?; let mut issue = issues.get_mut(&id)?; let state = to.into(); - issue.lifecycle(state, &signer)?; + issue.lifecycle(state)?; if !args.quiet { let success = @@ -155,7 +152,6 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { } => { let id = id.resolve(&repo.backend)?; if let Ok(mut issue) = issues.get_mut(&id) { - let signer = term::signer(&profile)?; let comment_id = match comment_id { Some(cid) => cid.resolve(&repo.backend)?, None => *term::io::comment_select(&issue).map(|(cid, _)| cid)?, @@ -164,11 +160,10 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { Some(reaction) => reaction, None => term::io::reaction_select()?, }; - issue.react(comment_id, reaction, true, &signer)?; + issue.react(comment_id, reaction, true)?; } } Command::Assign { id, add, delete } => { - let signer = term::signer(&profile)?; let id = id.resolve(&repo.backend)?; let Ok(mut issue) = issues.get_mut(&id) else { anyhow::bail!("Issue `{id}` not found"); @@ -179,7 +174,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { .chain(add.iter()) .cloned() .collect::<Vec<_>>(); - issue.assign(assignees, &signer)?; + issue.assign(assignees)?; } Command::Label { id, add, delete } => { let id = id.resolve(&repo.backend)?; @@ -192,8 +187,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { .chain(add.iter()) .cloned() .collect::<Vec<_>>(); - let signer = term::signer(&profile)?; - issue.label(labels, &signer)?; + issue.label(labels)?; } Command::List(list_args) => { list( @@ -206,8 +200,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { } Command::Delete { id } => { let id = id.resolve(&repo.backend)?; - let signer = term::signer(&profile)?; - issues.remove(&id, &signer)?; + issues.remove(&id)?; } Command::Cache { id, storage } => { let mode = if storage { @@ -365,20 +358,22 @@ fn mk_issue_row( ] } -fn open<R, G>( +fn open<Repo, Signer>( title: Option<Title>, description: Option<String>, labels: Vec<Label>, assignees: Vec<Did>, verbose: bool, quiet: bool, - cache: &mut issue::Cache<issue::Issues<'_, R>, cob::cache::StoreWriter>, - signer: &Device<G>, + cache: &mut issue::Cache<'_, Repo, WriteAs<'_, Signer>, cob::cache::StoreWriter>, profile: &Profile, ) -> anyhow::Result<()> where - R: WriteRepository + cob::Store<Namespace = NodeId>, - G: crypto::signature::Signer<crypto::Signature>, + Repo: WriteRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { let (title, description) = if let (Some(t), Some(d)) = (title.as_ref(), description.as_ref()) { (t.to_owned(), d.to_owned()) @@ -393,7 +388,6 @@ where labels.as_slice(), assignees.as_slice(), [], - signer, )?; if !quiet { @@ -402,17 +396,19 @@ where Ok(()) } -fn edit<'a, 'g, R, G>( - issues: &'g mut issue::Cache<issue::Issues<'a, R>, cob::cache::StoreWriter>, +fn edit<'a, 'b, 'g, Repo, Signer>( + issues: &'g mut issue::Cache<'a, Repo, WriteAs<'b, Signer>, cob::cache::StoreWriter>, repo: &storage::git::Repository, id: Rev, title: Option<Title>, description: Option<String>, - signer: &Device<G>, -) -> anyhow::Result<issue::IssueMut<'a, 'g, R, cob::cache::StoreWriter>> +) -> anyhow::Result<issue::IssueMut<'a, 'b, 'g, Repo, Signer, cob::cache::StoreWriter>> where - R: WriteRepository + cob::Store<Namespace = NodeId>, - G: crypto::signature::Signer<crypto::Signature>, + Repo: WriteRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: radicle::crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { let id = id.resolve(&repo.backend)?; let mut issue = issues.get_mut(&id)?; @@ -421,7 +417,7 @@ where if title.is_some() || description.is_some() { // Editing by command line arguments. - issue.transaction("Edit", signer, |tx| { + issue.transaction("Edit", |tx| { if let Some(t) = title { tx.edit(t)?; } @@ -442,7 +438,7 @@ where return Ok(issue); }; - issue.transaction("Edit", signer, |tx| { + issue.transaction("Edit", |tx| { tx.edit(title)?; tx.edit_comment(comment_id, description, vec![])?; diff --git a/crates/radicle-cli/src/commands/issue/cache.rs b/crates/radicle-cli/src/commands/issue/cache.rs index e2fb606c..4685c580 100644 --- a/crates/radicle-cli/src/commands/issue/cache.rs +++ b/crates/radicle-cli/src/commands/issue/cache.rs @@ -1,6 +1,7 @@ use std::ops::ControlFlow; use radicle::Profile; +use radicle::cob::store::access::ReadOnly; use radicle::issue::IssueId; use radicle::storage::ReadStorage as _; use radicle::storage::git::Repository; @@ -37,7 +38,18 @@ pub fn run(mode: CacheMode, profile: &Profile) -> anyhow::Result<()> { } fn cache(id: Option<IssueId>, repository: &Repository, profile: &Profile) -> anyhow::Result<()> { - let mut issues = term::cob::issues_mut(profile, repository)?; + let mut issues = { + // NOTE: Since we require a cache that is writeable, on top of a store that + // is read-only, we can neither use [`term::cob::issues_mut`] nor [`term::cob::issues`] + // since these convenience functions pair a writeable cache with a writeable + // store, and respectively a read-only cache with a read-only store. + + let db = profile.cobs_db_mut()?; + db.check_version()?; + let store = radicle::cob::issue::Issues::open(repository, ReadOnly)?; + + radicle::cob::issue::Cache::open(store, db) + }; match id { Some(id) => { diff --git a/crates/radicle-cli/src/commands/issue/comment.rs b/crates/radicle-cli/src/commands/issue/comment.rs index d072716f..e78803bf 100644 --- a/crates/radicle-cli/src/commands/issue/comment.rs +++ b/crates/radicle-cli/src/commands/issue/comment.rs @@ -1,4 +1,5 @@ use radicle::Profile; +use radicle::cob::store::access::WriteAs; use radicle::cob::thread; use radicle::storage::WriteRepository; use radicle::{cob, git, issue, storage}; @@ -8,27 +9,35 @@ use crate::terminal as term; use crate::terminal::Element as _; use crate::terminal::patch::Message; -pub(super) fn comment( +pub(super) fn comment<Signer>( profile: &Profile, repo: &storage::git::Repository, issues: &mut issue::Cache< - issue::Issues<'_, storage::git::Repository>, + '_, + storage::git::Repository, + WriteAs<'_, Signer>, cob::cache::Store<cob::cache::Write>, >, id: Rev, message: Message, reply_to: Option<Rev>, quiet: bool, -) -> Result<(), anyhow::Error> { +) -> Result<(), anyhow::Error> +where + Signer: radicle::crypto::signature::Keypair<VerifyingKey = radicle::crypto::PublicKey>, + Signer: radicle::crypto::signature::Signer<radicle::crypto::Signature>, + Signer: radicle::crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>, + Signer: radicle::crypto::signature::Verifier<radicle::crypto::Signature>, +{ let reply_to = reply_to .map(|rev| rev.resolve::<git::Oid>(repo.raw())) .transpose()?; - let signer = term::signer(profile)?; let issue_id = id.resolve::<cob::ObjectId>(&repo.backend)?; let mut issue = issues.get_mut(&issue_id)?; let (root_comment_id, _) = issue.root(); let body = prompt_comment(message, issue.thread(), reply_to, None)?; - let comment_id = issue.comment(body, reply_to.unwrap_or(*root_comment_id), vec![], &signer)?; + + let comment_id = issue.comment(body, reply_to.unwrap_or(*root_comment_id), vec![])?; if quiet { term::print(comment_id); } else { @@ -38,19 +47,26 @@ pub(super) fn comment( Ok(()) } -pub(super) fn edit( +pub(super) fn edit<Signer>( profile: &Profile, repo: &storage::git::Repository, issues: &mut issue::Cache< - issue::Issues<'_, storage::git::Repository>, + '_, + storage::git::Repository, + WriteAs<'_, Signer>, cob::cache::Store<cob::cache::Write>, >, id: Rev, message: Message, comment_id: Rev, quiet: bool, -) -> Result<(), anyhow::Error> { - let signer = term::signer(profile)?; +) -> Result<(), anyhow::Error> +where + Signer: radicle::crypto::signature::Keypair<VerifyingKey = radicle::crypto::PublicKey>, + Signer: radicle::crypto::signature::Signer<radicle::crypto::Signature>, + Signer: radicle::crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>, + Signer: radicle::crypto::signature::Verifier<radicle::crypto::Signature>, +{ let issue_id = id.resolve::<cob::ObjectId>(&repo.backend)?; let comment_id = comment_id.resolve(&repo.backend)?; let mut issue = issues.get_mut(&issue_id)?; @@ -64,7 +80,7 @@ pub(super) fn edit( comment.reply_to(), Some(comment.body()), )?; - issue.edit_comment(comment_id, body, vec![], &signer)?; + issue.edit_comment(comment_id, body, vec![])?; if quiet { term::print(comment_id); } else { diff --git a/crates/radicle-cli/src/commands/patch/archive.rs b/crates/radicle-cli/src/commands/patch/archive.rs index 6e256e63..6d634c23 100644 --- a/crates/radicle-cli/src/commands/patch/archive.rs +++ b/crates/radicle-cli/src/commands/patch/archive.rs @@ -10,15 +10,15 @@ pub fn run( repository: &Repository, ) -> anyhow::Result<()> { let signer = term::signer(profile)?; - let mut patches = term::cob::patches_mut(profile, repository)?; + let mut patches = term::cob::patches_mut(profile, repository, &signer)?; let Ok(mut patch) = patches.get_mut(patch_id) else { anyhow::bail!("Patch `{patch_id}` not found"); }; if undo { - patch.unarchive(&signer)?; + patch.unarchive()?; } else { - patch.archive(&signer)?; + patch.archive()?; } Ok(()) diff --git a/crates/radicle-cli/src/commands/patch/assign.rs b/crates/radicle-cli/src/commands/patch/assign.rs index af8d3bcc..28445d10 100644 --- a/crates/radicle-cli/src/commands/patch/assign.rs +++ b/crates/radicle-cli/src/commands/patch/assign.rs @@ -15,7 +15,7 @@ pub fn run( repository: &Repository, ) -> anyhow::Result<()> { let signer = term::signer(profile)?; - let mut patches = term::cob::patches_mut(profile, repository)?; + let mut patches = term::cob::patches_mut(profile, repository, &signer)?; let Ok(mut patch) = patches.get_mut(patch_id) else { anyhow::bail!("Patch `{patch_id}` not found"); }; @@ -24,6 +24,6 @@ pub fn run( .filter(|did| !delete.contains(did)) .chain(add) .collect::<BTreeSet<_>>(); - patch.assign(assignees, &signer)?; + patch.assign(assignees)?; Ok(()) } diff --git a/crates/radicle-cli/src/commands/patch/cache.rs b/crates/radicle-cli/src/commands/patch/cache.rs index f04e1c45..da979f22 100644 --- a/crates/radicle-cli/src/commands/patch/cache.rs +++ b/crates/radicle-cli/src/commands/patch/cache.rs @@ -1,6 +1,7 @@ use std::ops::ControlFlow; use radicle::Profile; +use radicle::cob::store::access::ReadOnly; use radicle::patch::PatchId; use radicle::storage::ReadStorage as _; use radicle::storage::git::Repository; @@ -37,7 +38,18 @@ pub fn run(mode: CacheMode, profile: &Profile) -> anyhow::Result<()> { } fn cache(id: Option<PatchId>, repository: &Repository, profile: &Profile) -> anyhow::Result<()> { - let mut patches = term::cob::patches_mut(profile, repository)?; + let mut patches = { + // NOTE: Since we require a cache that is writeable, on top of a store that + // is read-only, we can neither use [`term::cob::patches_mut`] nor [`term::cob::patches`] + // since these convenience functions pair a writeable cache with a writeable + // store, and respectively a read-only cache with a read-only store. + + let db = profile.cobs_db_mut()?; + db.check_version()?; + let store = radicle::cob::patch::Patches::open(repository, ReadOnly)?; + + radicle::cob::patch::Cache::open(store, db) + }; match id { Some(id) => { diff --git a/crates/radicle-cli/src/commands/patch/comment.rs b/crates/radicle-cli/src/commands/patch/comment.rs index 564b403d..b910db2d 100644 --- a/crates/radicle-cli/src/commands/patch/comment.rs +++ b/crates/radicle-cli/src/commands/patch/comment.rs @@ -24,7 +24,7 @@ pub fn run( profile: &Profile, ) -> anyhow::Result<()> { let signer = term::signer(profile)?; - let mut patches = term::cob::patches_mut(profile, repo)?; + let mut patches = term::cob::patches_mut(profile, repo, &signer)?; let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?; let ByRevision { @@ -37,7 +37,7 @@ pub fn run( .ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?; let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches); let (body, reply_to) = prompt(message, reply_to, &revision, repo)?; - let comment_id = patch.comment(revision_id, body, reply_to, None, vec![], &signer)?; + let comment_id = patch.comment(revision_id, body, reply_to, None, vec![])?; let comment = patch .revision(&revision_id) .ok_or(anyhow!("error retrieving revision `{revision_id}`"))? diff --git a/crates/radicle-cli/src/commands/patch/comment/edit.rs b/crates/radicle-cli/src/commands/patch/comment/edit.rs index b8d42846..2aad3f79 100644 --- a/crates/radicle-cli/src/commands/patch/comment/edit.rs +++ b/crates/radicle-cli/src/commands/patch/comment/edit.rs @@ -21,7 +21,7 @@ pub fn run( profile: &Profile, ) -> anyhow::Result<()> { let signer = term::signer(profile)?; - let mut patches = profile.patches_mut(repo)?; + let mut patches = term::cob::patches_mut(profile, repo, &signer)?; let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?; let ByRevision { id: patch_id, @@ -33,7 +33,7 @@ pub fn run( .ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?; let (body, _) = super::prompt(message, None, &revision, repo)?; let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches); - patch.comment_edit(revision_id, comment_id, body, vec![], &signer)?; + patch.comment_edit(revision_id, comment_id, body, vec![])?; if !quiet { let comment = patch diff --git a/crates/radicle-cli/src/commands/patch/comment/react.rs b/crates/radicle-cli/src/commands/patch/comment/react.rs index 3f5f667b..2d5e54e8 100644 --- a/crates/radicle-cli/src/commands/patch/comment/react.rs +++ b/crates/radicle-cli/src/commands/patch/comment/react.rs @@ -21,7 +21,7 @@ pub fn run( profile: &Profile, ) -> anyhow::Result<()> { let signer = term::signer(profile)?; - let mut patches = profile.patches_mut(repo)?; + let mut patches = term::cob::patches_mut(profile, repo, &signer)?; let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?; let ByRevision { id: patch_id, @@ -32,7 +32,7 @@ pub fn run( .find_by_revision(&patch::RevisionId::from(revision_id))? .ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?; let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches); - patch.comment_react(revision_id, comment, reaction, active, &signer)?; + patch.comment_react(revision_id, comment, reaction, active)?; Ok(()) } diff --git a/crates/radicle-cli/src/commands/patch/comment/redact.rs b/crates/radicle-cli/src/commands/patch/comment/redact.rs index d6de9c3c..71164286 100644 --- a/crates/radicle-cli/src/commands/patch/comment/redact.rs +++ b/crates/radicle-cli/src/commands/patch/comment/redact.rs @@ -18,7 +18,7 @@ pub fn run( profile: &Profile, ) -> anyhow::Result<()> { let signer = term::signer(profile)?; - let mut patches = profile.patches_mut(repo)?; + let mut patches = term::cob::patches_mut(profile, repo, &signer)?; let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?; let ByRevision { id: patch_id, @@ -29,7 +29,7 @@ pub fn run( .find_by_revision(&patch::RevisionId::from(revision_id))? .ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?; let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches); - patch.comment_redact(revision_id, comment, &signer)?; + patch.comment_redact(revision_id, comment)?; term::success!("Redacted comment {comment}"); Ok(()) diff --git a/crates/radicle-cli/src/commands/patch/delete.rs b/crates/radicle-cli/src/commands/patch/delete.rs index c1044065..38074ea0 100644 --- a/crates/radicle-cli/src/commands/patch/delete.rs +++ b/crates/radicle-cli/src/commands/patch/delete.rs @@ -4,9 +4,9 @@ use radicle::storage::git::Repository; use super::*; pub fn run(patch_id: &PatchId, profile: &Profile, repository: &Repository) -> anyhow::Result<()> { - let signer = &term::signer(profile)?; - let mut patches = term::cob::patches_mut(profile, repository)?; - patches.remove(patch_id, signer)?; + let signer = term::signer(profile)?; + let mut patches = term::cob::patches_mut(profile, repository, &signer)?; + patches.remove(patch_id)?; Ok(()) } diff --git a/crates/radicle-cli/src/commands/patch/edit.rs b/crates/radicle-cli/src/commands/patch/edit.rs index 4e0c7b58..ccaa32ed 100644 --- a/crates/radicle-cli/src/commands/patch/edit.rs +++ b/crates/radicle-cli/src/commands/patch/edit.rs @@ -4,7 +4,6 @@ use radicle::cob; use radicle::cob::Title; use radicle::cob::patch; use radicle::crypto; -use radicle::node::device::Device; use radicle::prelude::*; use radicle::storage::git::Repository; @@ -18,26 +17,28 @@ pub fn run( repository: &Repository, ) -> anyhow::Result<()> { let signer = term::signer(profile)?; - let mut patches = term::cob::patches_mut(profile, repository)?; + let mut patches = term::cob::patches_mut(profile, repository, &signer)?; 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, &signer), - None => edit_root(patch, title, description, &signer), + Some(id) => edit_revision(patch, id, title, description), + None => edit_root(patch, title, description), } } -fn edit_root<G>( - mut patch: patch::PatchMut<'_, '_, Repository, cob::cache::StoreWriter>, +fn edit_root<Signer>( + mut patch: patch::PatchMut<'_, '_, '_, Repository, Signer, cob::cache::StoreWriter>, title: Title, description: String, - signer: &Device<G>, ) -> anyhow::Result<()> where - G: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { let title = if title.as_ref() != patch.title() { Some(title) @@ -59,7 +60,7 @@ where let target = patch.target(); let embeds = patch.embeds().to_owned(); - patch.transaction("Edit root", signer, |tx| { + patch.transaction("Edit root", |tx| { if let Some(t) = title { tx.edit(t, target)?; } @@ -72,15 +73,17 @@ where Ok(()) } -fn edit_revision<G>( - mut patch: patch::PatchMut<'_, '_, Repository, cob::cache::StoreWriter>, +fn edit_revision<Signer>( + mut patch: patch::PatchMut<'_, '_, '_, Repository, Signer, cob::cache::StoreWriter>, revision: patch::RevisionId, title: Title, description: String, - signer: &Device<G>, ) -> anyhow::Result<()> where - G: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<radicle::crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { let embeds = patch.embeds().to_owned(); let mut message = title.to_string(); @@ -91,7 +94,7 @@ where message.push_str(&description); message }; - patch.transaction("Edit revision", signer, |tx| { + patch.transaction("Edit revision", |tx| { tx.edit_revision(revision, message, embeds)?; Ok(()) })?; diff --git a/crates/radicle-cli/src/commands/patch/label.rs b/crates/radicle-cli/src/commands/patch/label.rs index d2aa7bb1..84fa4bf3 100644 --- a/crates/radicle-cli/src/commands/patch/label.rs +++ b/crates/radicle-cli/src/commands/patch/label.rs @@ -12,7 +12,7 @@ pub fn run( repository: &Repository, ) -> anyhow::Result<()> { let signer = term::signer(profile)?; - let mut patches = term::cob::patches_mut(profile, repository)?; + let mut patches = term::cob::patches_mut(profile, repository, &signer)?; let Ok(mut patch) = patches.get_mut(patch_id) else { anyhow::bail!("Patch `{patch_id}` not found"); }; @@ -22,6 +22,6 @@ pub fn run( .chain(add.iter()) .cloned() .collect::<Vec<_>>(); - patch.label(labels, &signer)?; + patch.label(labels)?; Ok(()) } diff --git a/crates/radicle-cli/src/commands/patch/react.rs b/crates/radicle-cli/src/commands/patch/react.rs index 400ea7bc..f0bc93fa 100644 --- a/crates/radicle-cli/src/commands/patch/react.rs +++ b/crates/radicle-cli/src/commands/patch/react.rs @@ -18,7 +18,7 @@ pub fn run( profile: &Profile, ) -> anyhow::Result<()> { let signer = term::signer(profile)?; - let mut patches = profile.patches_mut(repo)?; + let mut patches = crate::terminal::cob::patches_mut(profile, repo, &signer)?; let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?; let ByRevision { id: patch_id, @@ -29,7 +29,7 @@ pub fn run( .find_by_revision(&patch::RevisionId::from(revision_id))? .ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?; let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches); - patch.react(revision_id, reaction, None, active, &signer)?; + patch.react(revision_id, reaction, None, active)?; Ok(()) } diff --git a/crates/radicle-cli/src/commands/patch/ready.rs b/crates/radicle-cli/src/commands/patch/ready.rs index d40d74f2..6ebe884a 100644 --- a/crates/radicle-cli/src/commands/patch/ready.rs +++ b/crates/radicle-cli/src/commands/patch/ready.rs @@ -10,15 +10,10 @@ pub fn run( repository: &Repository, ) -> anyhow::Result<bool> { let signer = term::signer(profile)?; - let mut patches = term::cob::patches_mut(profile, repository)?; + let mut patches = term::cob::patches_mut(profile, repository, &signer)?; let Ok(mut patch) = patches.get_mut(patch_id) else { anyhow::bail!("Patch `{patch_id}` not found"); }; - if undo { - patch.unready(&signer) - } else { - patch.ready(&signer) - } - .map_err(anyhow::Error::from) + if undo { patch.unready() } else { patch.ready() }.map_err(anyhow::Error::from) } diff --git a/crates/radicle-cli/src/commands/patch/redact.rs b/crates/radicle-cli/src/commands/patch/redact.rs index f66ff18d..ab21bb67 100644 --- a/crates/radicle-cli/src/commands/patch/redact.rs +++ b/crates/radicle-cli/src/commands/patch/redact.rs @@ -13,7 +13,7 @@ pub fn run( repository: &Repository, ) -> anyhow::Result<()> { let signer = &term::signer(profile)?; - let mut patches = term::cob::patches_mut(profile, repository)?; + let mut patches = term::cob::patches_mut(profile, repository, signer)?; let revision_id = revision_id.resolve::<Oid>(&repository.backend)?; let patch::ByRevision { @@ -27,7 +27,7 @@ pub fn run( anyhow::bail!("Patch `{patch_id}` not found"); }; - patch.redact(revision_id, signer)?; + patch.redact(revision_id)?; Ok(()) } diff --git a/crates/radicle-cli/src/commands/patch/resolve.rs b/crates/radicle-cli/src/commands/patch/resolve.rs index 08f34167..c982518b 100644 --- a/crates/radicle-cli/src/commands/patch/resolve.rs +++ b/crates/radicle-cli/src/commands/patch/resolve.rs @@ -15,12 +15,12 @@ pub fn resolve( profile: &Profile, ) -> anyhow::Result<()> { let signer = term::signer(profile)?; - let mut patches = term::cob::patches_mut(profile, repo)?; + let mut patches = term::cob::patches_mut(profile, repo, &signer)?; let patch = patches .get(&patch_id)? .ok_or_else(|| anyhow!("Patch `{patch_id}` not found"))?; let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches); - patch.resolve_review_comment(review, comment, &signer)?; + patch.resolve_review_comment(review, comment)?; Ok(()) } @@ -32,11 +32,11 @@ pub fn unresolve( profile: &Profile, ) -> anyhow::Result<()> { let signer = term::signer(profile)?; - let mut patches = term::cob::patches_mut(profile, repo)?; + let mut patches = term::cob::patches_mut(profile, repo, &signer)?; let patch = patches .get(&patch_id)? .ok_or_else(|| anyhow!("Patch `{patch_id}` not found"))?; let mut patch = patch::PatchMut::new(patch_id, patch, &mut patches); - patch.unresolve_review_comment(review, comment, &signer)?; + patch.unresolve_review_comment(review, comment)?; Ok(()) } diff --git a/crates/radicle-cli/src/commands/patch/review.rs b/crates/radicle-cli/src/commands/patch/review.rs index 07e594bd..9142d037 100644 --- a/crates/radicle-cli/src/commands/patch/review.rs +++ b/crates/radicle-cli/src/commands/patch/review.rs @@ -69,7 +69,7 @@ pub fn run( "couldn't load repository {} from local state", repository.id ))?; - let mut patches = term::cob::patches_mut(profile, repository)?; + let mut patches = term::cob::patches_mut(profile, repository, &signer)?; let mut patch = patches .get_mut(&patch_id) .context(format!("couldn't find patch {patch_id} locally"))?; @@ -111,7 +111,7 @@ pub fn run( } else { Some(message) }; - patch.review(revision_id, verdict, message, vec![], &signer)?; + patch.review(revision_id, verdict, message, vec![])?; match verdict { Some(Verdict::Accept) => { diff --git a/crates/radicle-cli/src/commands/patch/review/builder.rs b/crates/radicle-cli/src/commands/patch/review/builder.rs index 1962326c..7aaa8adb 100644 --- a/crates/radicle-cli/src/commands/patch/review/builder.rs +++ b/crates/radicle-cli/src/commands/patch/review/builder.rs @@ -648,7 +648,7 @@ impl<'a> ReviewBuilder<'a> { }; let diff = self.diff(&brain.accepted, &tree, repo, opts)?; let drafts = DraftStore::new(self.repo, *signer.public_key()); - let mut patches = cob::patch::Cache::no_cache(&drafts)?; + let mut patches = cob::patch::Cache::no_cache(&drafts, signer)?; let mut patch = patches.get_mut(&patch_id)?; let mut queue = ReviewQueue::from(diff); @@ -668,7 +668,6 @@ impl<'a> ReviewBuilder<'a> { Some(Verdict::Reject), None, vec![], - signer, )? }; @@ -719,7 +718,7 @@ impl<'a> ReviewBuilder<'a> { let builder = CommentBuilder::new(revision.head(), path.to_path_buf()); let comments = builder.edit(hunk)?; - patch.transaction("Review comments", signer, |tx| { + patch.transaction("Review comments", |tx| { for comment in comments { tx.review_comment( review, diff --git a/crates/radicle-cli/src/commands/patch/update.rs b/crates/radicle-cli/src/commands/patch/update.rs index 52cac82a..44ea967a 100644 --- a/crates/radicle-cli/src/commands/patch/update.rs +++ b/crates/radicle-cli/src/commands/patch/update.rs @@ -20,7 +20,8 @@ pub fn run( let head_branch = try_branch(workdir.head()?)?; let (_, target_oid) = get_merge_target(repository, &head_branch)?; - let mut patches = term::cob::patches_mut(profile, repository)?; + let signer = term::signer(profile)?; + let mut patches = term::cob::patches_mut(profile, repository, &signer)?; let Ok(mut patch) = patches.get_mut(&patch_id) else { anyhow::bail!("Patch `{patch_id}` not found"); }; @@ -45,8 +46,7 @@ pub fn run( let (_, revision) = patch.latest(); let message = term::patch::get_update_message(message, workdir, revision, &head_oid.into())?; - let signer = term::signer(profile)?; - let revision = patch.update(message, base_oid, head_oid, &signer)?; + let revision = patch.update(message, base_oid, head_oid)?; term::print(revision); diff --git a/crates/radicle-cli/src/commands/publish.rs b/crates/radicle-cli/src/commands/publish.rs index 0cf3f3cf..39e9e3fa 100644 --- a/crates/radicle-cli/src/commands/publish.rs +++ b/crates/radicle-cli/src/commands/publish.rs @@ -21,7 +21,8 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { }; let repo = profile.storage.repository_mut(rid)?; - let mut identity = Identity::load_mut(&repo)?; + let signer = profile.signer()?; + let mut identity = Identity::load_mut(&repo, &signer)?; let doc = identity.doc(); if doc.is_public() { @@ -51,12 +52,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { // SAFETY: the `Title` here is guaranteed to be nonempty and does not // contain `\n` or `\r`. #[allow(clippy::unwrap_used)] - identity.update( - cob::Title::new("Publish repository").unwrap(), - "", - &doc, - &signer, - )?; + identity.update(cob::Title::new("Publish repository").unwrap(), "", &doc)?; repo.sign_refs(&signer)?; repo.set_identity_head()?; let validations = repo.validate()?; diff --git a/crates/radicle-cli/src/terminal/cob.rs b/crates/radicle-cli/src/terminal/cob.rs index 81c4b20a..5267f03d 100644 --- a/crates/radicle-cli/src/terminal/cob.rs +++ b/crates/radicle-cli/src/terminal/cob.rs @@ -3,6 +3,7 @@ use radicle::{ cob::{ self, cache::{MigrateCallback, MigrateProgress}, + store::access::{ReadOnly, WriteAs}, }, prelude::NodeId, profile, @@ -58,47 +59,53 @@ pub mod migrate { } /// Return a read-only handle for the patches cache. -pub fn patches<'a, R>( +pub fn patches<'a, Repo>( profile: &Profile, - repository: &'a R, -) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreReader>, anyhow::Error> + repository: &'a Repo, +) -> Result<cob::patch::Cache<'a, Repo, ReadOnly, cob::cache::StoreReader>, anyhow::Error> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, { profile.patches(repository).map_err(with_hint) } /// Return a read-write handle for the patches cache. -pub fn patches_mut<'a, R>( +/// Prefer this over [`radicle::profile::Home::patches_mut`], +/// to obtain an error hint in case migrations must be run. +pub fn patches_mut<'a, 'b, Repo, Signer>( profile: &Profile, - repository: &'a R, -) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreWriter>, anyhow::Error> + repository: &'a Repo, + signer: &'b Signer, +) -> Result<cob::patch::Cache<'a, Repo, WriteAs<'b, Signer>, cob::cache::StoreWriter>, anyhow::Error> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, { - profile.patches_mut(repository).map_err(with_hint) + profile.patches_mut(repository, signer).map_err(with_hint) } /// Return a read-only handle for the issues cache. -pub fn issues<'a, R>( +pub fn issues<'a, Repo>( profile: &Profile, - repository: &'a R, -) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreReader>, anyhow::Error> + repository: &'a Repo, +) -> Result<cob::issue::Cache<'a, Repo, ReadOnly, cob::cache::StoreReader>, anyhow::Error> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, { profile.issues(repository).map_err(with_hint) } /// Return a read-write handle for the issues cache. -pub fn issues_mut<'a, R>( +/// Prefer this over [`radicle::profile::Home::issues_mut`], +/// to obtain an error hint in case migrations must be run. +pub fn issues_mut<'a, 'b, Repo, Signer>( profile: &Profile, - repository: &'a R, -) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreWriter>, anyhow::Error> + repository: &'a Repo, + signer: &'b Signer, +) -> Result<cob::issue::Cache<'a, Repo, WriteAs<'b, Signer>, cob::cache::StoreWriter>, anyhow::Error> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, { - profile.issues_mut(repository).map_err(with_hint) + profile.issues_mut(repository, signer).map_err(with_hint) } /// Adds a hint to the COB out-of-date database error. diff --git a/crates/radicle-cli/tests/commands/cob.rs b/crates/radicle-cli/tests/commands/cob.rs index 77c93e89..df199892 100644 --- a/crates/radicle-cli/tests/commands/cob.rs +++ b/crates/radicle-cli/tests/commands/cob.rs @@ -2,6 +2,7 @@ use std::path::Path; use crate::util::environment::Environment; use crate::{program_reports_version, test}; +use radicle::cob::store::access::{ReadOnly, WriteAs}; use radicle::node::Handle; use radicle::test::fixtures; @@ -135,7 +136,8 @@ fn test_cob_replication() { .unwrap(); let bob_repo = radicle::storage::ReadStorage::repository(&bob.storage, rid).unwrap(); - let mut bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap(); + let mut bob_issues = + radicle::cob::issue::Issues::open(&bob_repo, WriteAs::new(&bob.signer)).unwrap(); let mut bob_cache = radicle::cob::cache::InMemory::default(); let issue = bob_issues .create( @@ -145,7 +147,6 @@ fn test_cob_replication() { &[], [], &mut bob_cache, - &bob.signer, ) .unwrap(); log::debug!(target: "test", "Issue {} created", issue.id()); @@ -163,7 +164,7 @@ fn test_cob_replication() { .unwrap(); let alice_repo = radicle::storage::ReadStorage::repository(&alice.storage, rid).unwrap(); - let alice_issues = radicle::cob::issue::Issues::open(&alice_repo).unwrap(); + let alice_issues = radicle::cob::issue::Issues::open(&alice_repo, ReadOnly).unwrap(); let alice_issue = alice_issues.get(issue.id()).unwrap().unwrap(); assert_eq!(alice_issue.title(), "Something's fishy"); @@ -192,7 +193,8 @@ fn test_cob_deletion() { bob.routes_to(&[(rid, alice.id)]); let alice_repo = radicle::storage::ReadStorage::repository(&alice.storage, rid).unwrap(); - let mut alice_issues = radicle::cob::issue::Cache::no_cache(&alice_repo).unwrap(); + let mut alice_issues = + radicle::cob::issue::Cache::no_cache(&alice_repo, &alice.signer).unwrap(); let issue = alice_issues .create( radicle::cob::Title::new("Something's fishy").unwrap(), @@ -200,7 +202,6 @@ fn test_cob_deletion() { &[], &[], [], - &alice.signer, ) .unwrap(); let issue_id = issue.id(); @@ -210,11 +211,12 @@ fn test_cob_deletion() { .unwrap(); let bob_repo = radicle::storage::ReadStorage::repository(&bob.storage, rid).unwrap(); - let bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap(); + let bob_issues = radicle::cob::issue::Issues::open(&bob_repo, ReadOnly).unwrap(); assert!(bob_issues.get(issue_id).unwrap().is_some()); - let mut alice_issues = radicle::cob::issue::Cache::no_cache(&alice_repo).unwrap(); - alice_issues.remove(issue_id, &alice.signer).unwrap(); + let mut alice_issues = + radicle::cob::issue::Cache::no_cache(&alice_repo, &alice.signer).unwrap(); + alice_issues.remove(issue_id).unwrap(); log::debug!(target: "test", "Removing issue.."); @@ -225,6 +227,6 @@ fn test_cob_deletion() { radicle::node::FetchResult::Success { .. } ); let bob_repo = radicle::storage::ReadStorage::repository(&bob.storage, rid).unwrap(); - let bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap(); + let bob_issues = radicle::cob::issue::Issues::open(&bob_repo, ReadOnly).unwrap(); assert!(bob_issues.get(issue_id).unwrap().is_none()); } diff --git a/crates/radicle-node/src/test/node.rs b/crates/radicle-node/src/test/node.rs index a7eb9608..52d7f3f7 100644 --- a/crates/radicle-node/src/test/node.rs +++ b/crates/radicle-node/src/test/node.rs @@ -11,6 +11,10 @@ use std::{ use crossbeam_channel as chan; +use crate::node::NodeId; +use crate::node::device::Device; +use crate::storage::git::transport; +use crate::{Runtime, runtime, runtime::Handle, service}; use radicle::Storage; use radicle::cob; use radicle::cob::issue; @@ -34,11 +38,6 @@ use radicle::rad; use radicle::storage::{ReadStorage as _, RemoteRepository as _, SignRepository as _}; use radicle::test::fixtures; -use crate::node::NodeId; -use crate::node::device::Device; -use crate::storage::git::transport; -use crate::{Runtime, runtime, runtime::Handle, service}; - /// A node that can be run. pub struct Node<G> { pub id: NodeId, @@ -360,13 +359,10 @@ impl<G: Signer<Signature> + cyphernet::Ecdh> NodeHandle<G> { } /// Create an [`issue::Issue`] in the `NodeHandle`'s storage. - pub fn issue(&self, rid: RepoId, title: cob::Title, desc: &str) -> cob::ObjectId { + pub fn issue(&mut self, rid: RepoId, title: cob::Title, desc: &str) -> cob::ObjectId { let repo = self.storage.repository(rid).unwrap(); - let mut issues = issue::Cache::no_cache(&repo).unwrap(); - *issues - .create(title, desc, &[], &[], [], &self.signer) - .unwrap() - .id() + let mut issues = issue::Cache::no_cache(&repo, &self.signer).unwrap(); + *issues.create(title, desc, &[], &[], []).unwrap().id() } /// Perform a commit to `refname` by generating a blob of random data to a diff --git a/crates/radicle-node/src/test/peer.rs b/crates/radicle-node/src/test/peer.rs index e5ec740c..80378b37 100644 --- a/crates/radicle-node/src/test/peer.rs +++ b/crates/radicle-node/src/test/peer.rs @@ -134,7 +134,6 @@ impl Default for Config<MockSigner> { impl<G: crypto::signature::Signer<crypto::Signature>> Peer<Storage, G> { pub fn project(&mut self, name: &str, description: &str) -> RepoId { radicle::storage::git::transport::local::register(self.storage().clone()); - let (repo, _) = fixtures::repository(self.tempdir.path().join(name)); let (rid, _, _) = rad::init( &repo, diff --git a/crates/radicle-node/src/tests.rs b/crates/radicle-node/src/tests.rs index 9dc855c6..789ad4fd 100644 --- a/crates/radicle-node/src/tests.rs +++ b/crates/radicle-node/src/tests.rs @@ -1021,7 +1021,7 @@ fn test_refs_announcement_offline() { // Create an issue without telling the node. let repo = alice.storage().repository(rid).unwrap(); let old_refs = RefsAt::new(&repo, alice.id).unwrap(); - let mut issues = radicle::issue::Cache::no_cache(&repo).unwrap(); + let mut issues = radicle::issue::Cache::no_cache(&repo, alice.signer()).unwrap(); issues .create( cob::Title::new("Issue while offline!").unwrap(), @@ -1029,7 +1029,6 @@ fn test_refs_announcement_offline() { &[], &[], [], - alice.signer(), ) .unwrap(); let new_refs = RefsAt::new(&repo, alice.id).unwrap(); diff --git a/crates/radicle-node/src/tests/e2e.rs b/crates/radicle-node/src/tests/e2e.rs index c2b4484a..c53fa015 100644 --- a/crates/radicle-node/src/tests/e2e.rs +++ b/crates/radicle-node/src/tests/e2e.rs @@ -2,6 +2,7 @@ use std::{collections::HashSet, thread, time}; use radicle::cob; use radicle::cob::Title; +use radicle::cob::store::access::{ReadOnly, WriteAs}; use radicle_crypto::test::signer::MockSigner; use test_log::test; @@ -514,6 +515,7 @@ fn test_missing_remote() { .unwrap(); assert!(result.is_success()); log::debug!(target: "test", "Fetch complete with {}", bob.id); + rad::fork_remote(acme, &alice.id, &carol, &bob.storage).unwrap(); alice.issue( @@ -935,7 +937,7 @@ fn test_non_fastforward_sigrefs() { let rid = bob.project("acme", ""); let mut alice = alice.spawn(); - let bob = bob.spawn(); + let mut bob = bob.spawn(); let mut eve = eve.spawn(); alice.handle.seed(rid, Scope::All).unwrap(); @@ -1114,7 +1116,7 @@ fn test_outdated_sigrefs() { FetchResult::Success { .. } ); let repo = alice.storage.repository(rid).unwrap(); - let issues = issue::Issues::open(&repo).unwrap(); + let issues = issue::Issues::open(&repo, WriteAs::new(&alice.signer)).unwrap(); assert!( issues.get(&issue_id).unwrap().is_some(), "Alice did not fetch issue {issue_id}" @@ -1348,12 +1350,14 @@ fn missing_delegate_default_branch() { .unwrap(); assert!(bob.storage.contains(&rid).unwrap()); + let bob_key = *bob.signer.public_key(); + // Helper to assert that Bob's default branch is not in storage let assert_bobs_default_is_missing = |repo: &Repository| { let doc = repo.identity_doc().unwrap(); let project = doc.project().unwrap(); let default_branch = repo.reference( - bob.signer.public_key(), + &bob_key, &radicle::git::refs::branch(project.default_branch()), ); assert!(matches!( @@ -1365,7 +1369,7 @@ fn missing_delegate_default_branch() { // Add Bob as a delegate to the identity document { let repo = alice.storage.repository(rid).unwrap(); - let mut identity = Identity::load_mut(&repo).unwrap(); + let mut identity = Identity::load_mut(&repo, &alice.signer).unwrap(); let doc = repo .identity_doc() .unwrap() @@ -1375,7 +1379,7 @@ fn missing_delegate_default_branch() { }) .unwrap(); let rev = identity - .update(Title::new("Add Bob").unwrap(), "", &doc, &alice.signer) + .update(Title::new("Add Bob").unwrap(), "", &doc) .unwrap(); repo.set_identity_head_to(rev).unwrap(); @@ -1735,6 +1739,7 @@ fn test_non_fastforward_identity_doc() { .handle .fetch(rid, alice.id, DEFAULT_TIMEOUT, None) .unwrap(); + // Alice pushes new references to her laptop let issue = alice_laptop.issue( rid, @@ -1752,7 +1757,7 @@ fn test_non_fastforward_identity_doc() { // Alice updates the identity of the document to include her laptop let (prev, next) = { let repo = alice.storage.repository(rid).unwrap(); - let mut identity = Identity::load_mut(&repo).unwrap(); + let mut identity = Identity::load_mut(&repo, &alice.signer).unwrap(); let prev = identity.current; let doc = repo .identity_doc() @@ -1761,7 +1766,7 @@ fn test_non_fastforward_identity_doc() { .with_edits(|raw| raw.delegate(alice_laptop.id.into())) .unwrap(); let rev = identity - .update(Title::new("Add Laptop").unwrap(), "", &doc, &alice.signer) + .update(Title::new("Add Laptop").unwrap(), "", &doc) .unwrap(); repo.set_identity_head_to(rev).unwrap(); (prev, rev) @@ -1780,7 +1785,7 @@ fn test_non_fastforward_identity_doc() { assert!(matches!(result, FetchResult::Success { .. })); assert!(!has_issue(&bob, &issue)); let repo = bob.storage.repository(rid).unwrap(); - let identity = Identity::load_mut(&repo).unwrap(); + let identity = Identity::load_mut(&repo, &bob.signer).unwrap(); assert_eq!(identity.current, next); assert_eq!(identity.parent, Some(prev)); @@ -1793,7 +1798,7 @@ fn test_non_fastforward_identity_doc() { assert!(matches!(result, FetchResult::Success { .. })); assert!(has_issue(&bob, &issue)); let repo = bob.storage.repository(rid).unwrap(); - let identity = Identity::load_mut(&repo).unwrap(); + let identity = Identity::load_mut(&repo, &bob.signer).unwrap(); assert_eq!(identity.current, next); assert_eq!(identity.parent, Some(prev)); } @@ -1917,7 +1922,7 @@ fn fetch_does_not_contain_rad_sigrefs_parent() { FetchResult::Success { .. } ); let repo = bob.storage.repository(rid).unwrap(); - let issues = issue::Issues::open(&repo).unwrap(); + let issues = issue::Issues::open(&repo, ReadOnly).unwrap(); assert!( issues.get(&issue_id).unwrap().is_some(), "Bob did not fetch issue {issue_id}" diff --git a/crates/radicle-node/src/worker/fetch.rs b/crates/radicle-node/src/worker/fetch.rs index e500f5d1..be0758b0 100644 --- a/crates/radicle-node/src/worker/fetch.rs +++ b/crates/radicle-node/src/worker/fetch.rs @@ -1,3 +1,4 @@ +use radicle::cob::store::access::ReadOnly; use radicle::identity::CanonicalRefs; use radicle::identity::doc::CanonicalRefsError; use radicle::storage::git::TempRepository; @@ -272,8 +273,8 @@ where C: cob::cache::Update<cob::issue::Issue> + cob::cache::Update<cob::patch::Patch>, C: cob::cache::Remove<cob::issue::Issue> + cob::cache::Remove<cob::patch::Patch>, { - let mut issues = cob::store::Store::<cob::issue::Issue, _>::open(storage)?; - let mut patches = cob::store::Store::<cob::patch::Patch, _>::open(storage)?; + let mut issues = cob::store::Store::<cob::issue::Issue, _, _>::open(storage, ReadOnly)?; + let mut patches = cob::store::Store::<cob::patch::Patch, _, _>::open(storage, ReadOnly)?; for update in refs { match update { @@ -303,15 +304,15 @@ where } /// Update or remove a cache entry. -fn update_or_remove<R, C, T>( - store: &mut cob::store::Store<T, R>, +fn update_or_remove<T, Repo, C>( + store: &mut cob::store::Store<T, Repo, ReadOnly>, cache: &mut C, rid: &RepoId, tid: TypedId, ) -> Result<(), error::Cache> where - R: cob::Store + ReadRepository, - T: cob::Evaluate<R> + cob::store::Cob + cob::store::CobWithType, + T: cob::Evaluate<Repo> + cob::store::Cob + cob::store::CobWithType, + Repo: cob::Store<Namespace = NodeId> + ReadRepository, C: cob::cache::Update<T> + cob::cache::Remove<T>, { match store.get(&tid.id) { diff --git a/crates/radicle-remote-helper/src/main.rs b/crates/radicle-remote-helper/src/main.rs index aca17228..1883459f 100644 --- a/crates/radicle-remote-helper/src/main.rs +++ b/crates/radicle-remote-helper/src/main.rs @@ -28,6 +28,7 @@ use std::process; use std::str::FromStr; use std::{env, fmt}; +use radicle::cob::store::access::{ReadOnly, WriteAs}; use thiserror::Error; use radicle::prelude::NodeId; @@ -458,10 +459,10 @@ pub(crate) fn warn(s: impl fmt::Display) { } /// Get the patch store. -pub(crate) fn patches<'a, R: ReadRepository + cob::Store<Namespace = NodeId>>( +pub(crate) fn patches<'a, Repo: ReadRepository + cob::Store<Namespace = NodeId>>( profile: &Profile, - repo: &'a R, -) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreReader>, list::Error> { + repo: &'a Repo, +) -> Result<cob::patch::Cache<'a, Repo, ReadOnly, cob::cache::StoreReader>, list::Error> { match profile.patches(repo) { Ok(patches) => Ok(patches), Err(err @ profile::Error::CobsCache(cob::cache::Error::OutOfDate)) => { @@ -473,14 +474,17 @@ pub(crate) fn patches<'a, R: ReadRepository + cob::Store<Namespace = NodeId>>( } /// Get the mutable patch store. -pub(crate) fn patches_mut<'a>( +pub(crate) fn patches_mut<'a, 'b, Signer>( profile: &Profile, repo: &'a storage::git::Repository, + signer: &'b Signer, ) -> Result< - cob::patch::Cache<cob::patch::Patches<'a, storage::git::Repository>, cob::cache::StoreWriter>, + cob::patch::Cache<'a, storage::git::Repository, WriteAs<'b, Signer>, cob::cache::StoreWriter>, push::Error, -> { - match profile.patches_mut(repo) { +> +where +{ + match profile.patches_mut(repo, signer) { Ok(patches) => Ok(patches), Err(err @ profile::Error::CobsCache(cob::cache::Error::OutOfDate)) => { hint(cli::cob::MIGRATION_HINT); diff --git a/crates/radicle-remote-helper/src/push.rs b/crates/radicle-remote-helper/src/push.rs index 56d5f1b9..972b062a 100644 --- a/crates/radicle-remote-helper/src/push.rs +++ b/crates/radicle-remote-helper/src/push.rs @@ -8,9 +8,9 @@ use std::process::ExitStatus; use std::str::FromStr; use std::{assert_eq, io}; +use radicle::cob::store::access::WriteAs; use radicle::identity::crefs::GetCanonicalRefs as _; use radicle::identity::doc::CanonicalRefsError; -use radicle::node::device::Device; use thiserror::Error; use radicle::Profile; @@ -321,7 +321,8 @@ pub(super) fn run( .map_err(Error::from) } Command::Push(git::fmt::refspec::Refspec { src, dst, force }) => { - let patches = crate::patches_mut(profile, stored)?; + let signer = profile.signer()?; + let patches = crate::patches_mut(profile, stored, &signer)?; let action = PushAction::new(dst)?; match action { @@ -332,7 +333,6 @@ pub(super) fn run( &working, stored, patches, - &signer, profile, opts.clone(), git, @@ -560,24 +560,28 @@ impl<'a, G> Drop for TempPatchRef<'a, G> { } /// Open a new patch. -fn patch_open<G, S>( +fn patch_open<S, Signer>( head: &git::Oid, upstream: &Option<git::fmt::RefString>, nid: &NodeId, working: &git::raw::Repository, stored: &storage::git::Repository, mut patches: patch::Cache< - patch::Patches<'_, storage::git::Repository>, + '_, + storage::git::Repository, + WriteAs<'_, Signer>, cob::cache::StoreWriter, >, - signer: &Device<G>, profile: &Profile, opts: Options, git: &S, ) -> Result<Option<ExplorerResource>, Error> where - G: crypto::signature::Signer<crypto::Signature>, S: GitService, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { let temp = TempPatchRef::new(stored, head, nid, git); temp.push(head, opts.verbosity)?; @@ -601,7 +605,6 @@ where base, *head, &[], - signer, ) } else { patches.create( @@ -611,7 +614,6 @@ where base, *head, &[], - signer, ) }?; @@ -698,7 +700,7 @@ where /// Update an existing patch. #[allow(clippy::too_many_arguments)] -fn patch_update<G, S>( +fn patch_update<S, Signer>( head: &git::Oid, dst: &git::fmt::Qualified, force: bool, @@ -707,17 +709,22 @@ fn patch_update<G, S>( working: &git::raw::Repository, stored: &storage::git::Repository, mut patches: patch::Cache< - patch::Patches<'_, storage::git::Repository>, + '_, + storage::git::Repository, + WriteAs<'_, Signer>, cob::cache::StoreWriter, >, - signer: &Device<G>, + signer: &Signer, opts: Options, expected_refs: &[String], git: &S, ) -> Result<Option<ExplorerResource>, Error> where - G: crypto::signature::Signer<crypto::Signature>, S: GitService, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { let Ok(Some(patch)) = patches.get(&patch_id) else { return Err(Error::NotFound(patch_id)); @@ -754,7 +761,7 @@ where )?; let mut patch_mut = patch::PatchMut::new(patch_id, patch, &mut patches); - let revision = patch_mut.update(message, base, *head, signer)?; + let revision = patch_mut.update(message, base, *head)?; let Some(revision) = patch_mut.revision(&revision).cloned() else { return Err(Error::RevisionNotFound(revision)); }; @@ -787,7 +794,7 @@ where Ok(Some(ExplorerResource::Patch { id: patch_id })) } -fn push<G, S>( +fn push<S, Signer>( src: &git::Oid, dst: &git::fmt::Qualified, force: bool, @@ -795,17 +802,22 @@ fn push<G, S>( working: &git::raw::Repository, stored: &storage::git::Repository, mut patches: patch::Cache< - patch::Patches<'_, storage::git::Repository>, + '_, + storage::git::Repository, + WriteAs<'_, Signer>, cob::cache::StoreWriter, >, - signer: &Device<G>, + signer: &Signer, verbosity: Verbosity, expected_refs: &[String], git: &S, ) -> Result<Option<ExplorerResource>, Error> where - G: crypto::signature::Signer<crypto::Signature>, S: GitService, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { let head = *src; let dst = dst.with_namespace(nid.into()); @@ -832,7 +844,7 @@ where let old = old.peel_to_commit()?.id(); // Only delegates affect the merge state of the COB. if stored.delegates()?.contains(&nid.into()) { - patch_revert_all(old.into(), head, &stored.backend, &mut patches, signer)?; + patch_revert_all(old.into(), head, &stored.backend, &mut patches)?; patch_merge_all(old.into(), head, working, &mut patches, signer)?; } } @@ -841,18 +853,20 @@ where } /// Revert all patches that are no longer included in the base branch. -fn patch_revert_all<G>( +fn patch_revert_all<Signer>( old: git::Oid, new: git::Oid, stored: &git::raw::Repository, patches: &mut patch::Cache< - patch::Patches<'_, storage::git::Repository>, + '_, + storage::git::Repository, + WriteAs<'_, Signer>, cob::cache::StoreWriter, >, - _signer: &Device<G>, ) -> Result<(), Error> where - G: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, { // Find all commits reachable from the old OID but not from the new OID. let mut revwalk = stored.revwalk()?; @@ -906,18 +920,23 @@ where } /// Merge all patches that have been included in the base branch. -fn patch_merge_all<G>( +fn patch_merge_all<Signer>( old: git::Oid, new: git::Oid, working: &git::raw::Repository, patches: &mut patch::Cache< - patch::Patches<'_, storage::git::Repository>, + '_, + storage::git::Repository, + WriteAs<'_, Signer>, cob::cache::StoreWriter, >, - signer: &Device<G>, + signer: &Signer, ) -> Result<(), Error> where - G: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { let mut revwalk = working.revwalk()?; revwalk.push_range(&format!("{old}..{new}"))?; @@ -959,19 +978,22 @@ where Ok(()) } -fn patch_merge<C, G>( - mut patch: patch::PatchMut<storage::git::Repository, C>, +fn patch_merge<Signer, C>( + mut patch: patch::PatchMut<'_, '_, '_, storage::git::Repository, Signer, C>, revision: patch::RevisionId, commit: git::Oid, working: &git::raw::Repository, - signer: &Device<G>, + signer: &Signer, ) -> Result<(), Error> where + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, C: cob::cache::Update<patch::Patch>, - G: crypto::signature::Signer<crypto::Signature>, { let (latest, _) = patch.latest(); - let merged = patch.merge(revision, commit, signer)?; + let merged = patch.merge(revision, commit)?; if revision == latest { eprintln!( diff --git a/crates/radicle/src/cob/cache.rs b/crates/radicle/src/cob/cache.rs index 708356ca..81aa6834 100644 --- a/crates/radicle/src/cob/cache.rs +++ b/crates/radicle/src/cob/cache.rs @@ -112,9 +112,10 @@ pub enum Error { OutOfDate, } -/// Read and write to the store. +/// Read from and write to the store. pub type StoreWriter = Store<Write>; -/// Write to the store. + +/// Read from the store. pub type StoreReader = Store<Read>; /// Read-only type witness. diff --git a/crates/radicle/src/cob/identity.rs b/crates/radicle/src/cob/identity.rs index e8bbadf9..31d8aab2 100644 --- a/crates/radicle/src/cob/identity.rs +++ b/crates/radicle/src/cob/identity.rs @@ -3,15 +3,16 @@ use std::sync::LazyLock; use std::{fmt, ops::Deref, str::FromStr}; use crypto::{PublicKey, Signature}; +use nonempty::NonEmpty; use radicle_cob::{Embed, ObjectId, TypeName}; use serde::{Deserialize, Serialize}; use thiserror::Error; +use crate::cob::store::access::WriteAs; use crate::git; use crate::git::Oid; use crate::identity::doc::Doc; use crate::node::NodeId; -use crate::node::device::Device; use crate::storage; use crate::{ cob, @@ -219,33 +220,34 @@ impl Identity { } } - pub fn initialize<'a, R, G>( + pub fn initialize<'a, 'b, Repo, Signer>( doc: &Doc, - store: &'a R, - signer: &Device<G>, - ) -> Result<IdentityMut<'a, R>, cob::store::Error> + store: &'a Repo, + signer: &'b Signer, + ) -> Result<IdentityMut<'a, 'b, Repo, Signer>, cob::store::Error> where - G: crypto::signature::Signer<crypto::Signature>, - R: WriteRepository + cob::Store<Namespace = NodeId>, + Repo: WriteRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { - let mut store = cob::store::Store::open(store)?; - let (id, identity) = Transaction::<Identity, _>::initial( - "Initialize identity", - &mut store, - signer, - |tx, repo| { - tx.revision( - // SAFETY: "Initial revision" is a valid title - #[allow(clippy::unwrap_used)] - cob::Title::new("Initial revision").unwrap(), - "", - doc, - None, - repo, - signer, - ) - }, - )?; + let mut store = cob::store::Store::open(store, WriteAs::new(signer))?; + + #[allow(clippy::unwrap_used)] + let title = cob::Title::new("Initial revision").unwrap(); + + #[allow(deprecated)] + let (actions, embeds) = { + let repo = store.repo(); + let signer = store.signer(); + Transaction::new_revision(title, "", doc, None, repo, signer)?.into_inner() + }; + + let actions = NonEmpty::from_vec(actions) + .expect("Transaction::initial: transaction must contain at least one action"); + + let (id, identity) = store.create("Initialize identity", actions, embeds)?; Ok(IdentityMut { id, @@ -254,10 +256,10 @@ impl Identity { }) } - pub fn get<R: ReadRepository + cob::Store>( - object: &ObjectId, - repo: &R, - ) -> Result<Identity, store::Error> { + pub fn get<Repo>(object: &ObjectId, repo: &Repo) -> Result<Identity, store::Error> + where + Repo: ReadRepository + cob::Store, + { use cob::store::CobWithType; cob::get::<Self, _>(repo, Self::type_name(), object) @@ -266,12 +268,17 @@ impl Identity { } /// Get a proposal mutably. - pub fn get_mut<'a, R: WriteRepository + cob::Store<Namespace = NodeId>>( + pub fn get_mut<'a, 'b, Repo, Signer>( id: &ObjectId, - repo: &'a R, - ) -> Result<IdentityMut<'a, R>, store::Error> { + repo: &'a Repo, + signer: &'b Signer, + ) -> Result<IdentityMut<'a, 'b, Repo, Signer>, store::Error> + where + Repo: WriteRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Signer<crypto::Signature>, + { let obj = Self::get(id, repo)?; - let store = cob::store::Store::open(repo)?; + let store = cob::store::Store::open(repo, WriteAs::new(signer))?; Ok(IdentityMut { id: *id, @@ -287,13 +294,18 @@ impl Identity { Self::get(&oid, repo).map_err(RepositoryError::from) } - pub fn load_mut<R: WriteRepository + cob::Store<Namespace = NodeId>>( - repo: &R, - ) -> Result<IdentityMut<'_, R>, RepositoryError> { + pub fn load_mut<'a, 'b, Repo, Signer>( + repo: &'a Repo, + signer: &'b Signer, + ) -> Result<IdentityMut<'a, 'b, Repo, Signer>, RepositoryError> + where + Repo: WriteRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Signer<crypto::Signature>, + { let oid = repo.identity_root()?; let oid = ObjectId::from(oid); - Self::get_mut(&oid, repo).map_err(RepositoryError::from) + Self::get_mut(&oid, repo, signer).map_err(RepositoryError::from) } } @@ -885,43 +897,47 @@ impl<R: ReadRepository> store::Transaction<Identity, R> { } impl<R: WriteRepository> store::Transaction<Identity, R> { - pub fn revision<G: crypto::signature::Signer<crypto::Signature>>( - &mut self, + pub fn new_revision<G: crypto::signature::Signer<crypto::Signature>>( title: cob::Title, description: impl ToString, doc: &Doc, parent: Option<RevisionId>, repo: &R, - signer: &Device<G>, - ) -> Result<(), store::Error> { + signer: &G, + ) -> Result<Self, store::Error> { + let mut tx = Transaction::default(); + 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([embed])?; + tx.embed([embed])?; // Revision metadata. - self.push(Action::Revision { + tx.push(Action::Revision { title, description: description.to_string(), blob, parent, signature, - }) + })?; + + Ok(tx) } } -pub struct IdentityMut<'a, R> { +pub struct IdentityMut<'a, 'b, Repo, Signer> { pub id: ObjectId, identity: Identity, - store: store::Store<'a, Identity, R>, + store: store::Store<'a, Identity, Repo, WriteAs<'b, Signer>>, } -impl<R> fmt::Debug for IdentityMut<'_, R> { +impl<Repo, Signer> fmt::Debug for IdentityMut<'_, '_, Repo, Signer> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("IdentityMut") .field("id", &self.id) @@ -930,11 +946,16 @@ impl<R> fmt::Debug for IdentityMut<'_, R> { } } -impl<R> IdentityMut<'_, R> +impl<Repo, Signer> IdentityMut<'_, '_, Repo, Signer> where - R: WriteRepository + cob::Store<Namespace = NodeId>, + Repo: WriteRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { /// Reload the identity data from storage. + #[cfg(test)] pub fn reload(&mut self) -> Result<(), store::Error> { self.identity = self .store @@ -944,20 +965,14 @@ where Ok(()) } - pub fn transaction<G, F>( - &mut self, - message: &str, - signer: &Device<G>, - operations: F, - ) -> Result<EntryId, Error> + pub fn transaction<F>(&mut self, message: &str, operations: F) -> Result<EntryId, Error> where - G: crypto::signature::Signer<crypto::Signature>, - F: FnOnce(&mut Transaction<Identity, R>, &R) -> Result<(), store::Error>, + F: FnOnce(&mut Transaction<Identity, Repo>, &Repo) -> Result<(), store::Error>, { let mut tx = Transaction::default(); 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)?; self.identity = doc; Ok(commit) @@ -965,70 +980,61 @@ where /// Update the identity by proposing a new revision. /// If the signer is the only delegate, the revision is accepted automatically. - pub fn update<G>( + pub fn update( &mut self, title: cob::Title, description: impl ToString, doc: &Doc, - signer: &Device<G>, - ) -> Result<RevisionId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - let parent = self.current; - let id = self.transaction("Propose revision", signer, |tx, repo| { - tx.revision(title, description, doc, Some(parent), repo, signer) - })?; + ) -> Result<RevisionId, Error> { + let parent = Some(self.current); - Ok(id) + #[allow(deprecated)] + let tx = { + let signer = self.store.signer(); + let repo = self.store.repo(); + Transaction::new_revision(title, description, doc, parent, repo, signer)? + }; + let (doc, commit) = tx.commit("Propose revision", self.id, &mut self.store)?; + self.identity = doc; + + Ok(commit) } /// Accept an active revision. - pub fn accept<G>(&mut self, revision: &RevisionId, signer: &Device<G>) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { + pub fn accept(&mut self, revision: &RevisionId) -> Result<EntryId, Error> { let id = *revision; 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)) + #[allow(deprecated)] + let signature = revision.sign(self.store.signer())?; + + self.transaction("Accept revision", |tx, _| tx.accept(id, signature)) } /// Reject an active revision. - pub fn reject<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Reject revision", signer, |tx, _| tx.reject(revision)) + pub fn reject(&mut self, revision: RevisionId) -> Result<EntryId, Error> { + self.transaction("Reject revision", |tx, _| tx.reject(revision)) } /// Redact a revision. - pub fn redact<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Redact revision", signer, |tx, _| tx.redact(revision)) + pub fn redact(&mut self, revision: RevisionId) -> Result<EntryId, Error> { + self.transaction("Redact revision", |tx, _| tx.redact(revision)) } /// Edit an active revision's title or description. - pub fn edit<G>( + pub fn edit( &mut self, revision: RevisionId, title: cob::Title, description: String, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Edit revision", signer, |tx, _| { + ) -> Result<EntryId, Error> { + self.transaction("Edit revision", |tx, _| { tx.edit(revision, title, description) }) } } -impl<R> Deref for IdentityMut<'_, R> { +impl<Repo, Signer> Deref for IdentityMut<'_, '_, Repo, Signer> { type Target = Identity; fn deref(&self) -> &Self::Target { @@ -1076,6 +1082,7 @@ mod test { use crate::identity::Visibility; use crate::identity::did::Did; use crate::identity::doc::PayloadId; + use crate::node::device::Device; use crate::rad; use crate::storage::ReadStorage; use crate::storage::git::Storage; @@ -1101,7 +1108,7 @@ mod test { let NodeWithRepo { node, repo } = NodeWithRepo::default(); let bob = Device::mock(); let signer = &node.signer; - let mut identity = Identity::load_mut(&*repo).unwrap(); + let mut identity = Identity::load_mut(&*repo, signer).unwrap(); let mut doc = identity.doc().clone().edit(); let title = Title::new("Identity update").unwrap(); let description = ""; @@ -1111,12 +1118,7 @@ mod test { assert!(identity.current().is_accepted()); // Using an identical document to the current one fails. identity - .update( - title.clone(), - description, - &doc.clone().verified().unwrap(), - signer, - ) + .update(title.clone(), description, &doc.clone().verified().unwrap()) .unwrap_err(); assert_eq!(identity.current, r0); @@ -1129,12 +1131,7 @@ mod test { doc.delegate(bob.public_key().into()); // The update should go through now. let r1 = identity - .update( - title.clone(), - description, - &doc.clone().verified().unwrap(), - signer, - ) + .update(title.clone(), description, &doc.clone().verified().unwrap()) .unwrap(); assert!(identity.revision(&r1).unwrap().is_accepted()); assert_eq!(identity.current, r1); @@ -1143,12 +1140,7 @@ mod test { // signs it. doc.visibility = Visibility::private([]); let r2 = identity - .update( - title.clone(), - description, - &doc.clone().verified().unwrap(), - signer, - ) + .update(title.clone(), description, &doc.clone().verified().unwrap()) .unwrap(); // R1 is still the head. assert_eq!(identity.current, r1); @@ -1159,7 +1151,10 @@ mod test { &Visibility::Public ); // Now let's add a signature on R2 from Bob. - identity.accept(&r2, &bob).unwrap(); + let mut bob_identity = Identity::load_mut(&*repo, &bob).unwrap(); + bob_identity.accept(&r2).unwrap(); + + identity.reload().unwrap(); // R2 is now the head. assert_eq!(identity.current, r2); @@ -1177,7 +1172,8 @@ mod test { let bob = Device::mock(); let eve = Device::mock(); let signer = &node.signer; - let mut identity = Identity::load_mut(&*repo).unwrap(); + + let mut identity = Identity::load_mut(&*repo, signer).unwrap(); let mut doc = identity.doc().clone().edit(); let description = ""; @@ -1188,7 +1184,6 @@ mod test { cob::Title::new("Identity update").unwrap(), description, &doc.clone().verified().unwrap(), - signer, ) .unwrap(); assert_eq!(identity.current, r1); @@ -1199,13 +1194,14 @@ mod test { cob::Title::new("Make private").unwrap(), description, &doc.clone().verified().unwrap(), - &node.signer, ) .unwrap(); + let mut bob_identity = Identity::load_mut(&*repo, &bob).unwrap(); + // 1/2 rejected means that we can never reach the required 2/2 votes. - identity.reject(r2, &bob).unwrap(); - let r2 = identity.revision(&r2).unwrap(); + bob_identity.reject(r2).unwrap(); + let r2 = bob_identity.revision(&r2).unwrap(); assert_eq!(r2.state, State::Rejected); // Now let's add another delegate. @@ -1215,10 +1211,13 @@ mod test { cob::Title::new("Add Eve").unwrap(), description, &doc.clone().verified().unwrap(), - &node.signer, ) .unwrap(); - let _ = identity.accept(&r3, &bob).unwrap(); + + bob_identity.reload().unwrap(); + let _ = bob_identity.accept(&r3).unwrap(); + + identity.reload().unwrap(); assert_eq!(identity.current, r3); doc.visibility = Visibility::Public; @@ -1227,18 +1226,19 @@ mod test { cob::Title::new("Make public").unwrap(), description, &doc.verified().unwrap(), - &node.signer, ) .unwrap(); // 1/3 rejected means that we can still reach the 2/3 required votes. - identity.reject(r3, &bob).unwrap(); + bob_identity.reject(r3).unwrap(); let r3 = identity.revision(&r3).unwrap().clone(); assert_eq!(r3.state, State::Active); // Still active. + let mut eve_identity = Identity::load_mut(&*repo, &eve).unwrap(); + // 2/3 rejected means that we can no longer reach the 2/3 required votes. - identity.reject(r3.id, &eve).unwrap(); - let r3 = identity.revision(&r3.id).unwrap(); + eve_identity.reject(r3.id).unwrap(); + let r3 = eve_identity.revision(&r3.id).unwrap(); assert_eq!(r3.state, State::Rejected); } @@ -1248,7 +1248,7 @@ mod test { let alice = &network.alice; let bob = &network.bob; - let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap(); + let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap(); let mut alice_doc = alice_identity.doc().clone().edit(); alice_doc.delegate(bob.signer.public_key().into()); @@ -1257,13 +1257,12 @@ mod test { cob::Title::new("Add Bob").unwrap(), "", &alice_doc.clone().verified().unwrap(), - &alice.signer, ) .unwrap(); bob.repo.fetch(alice); - let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap(); + let bob_identity = Identity::load(&*bob.repo).unwrap(); let bob_doc = bob_identity.doc().clone(); assert!(bob_doc.is_delegate(&bob.signer.public_key().into())); @@ -1274,16 +1273,19 @@ mod test { cob::Title::new("Change visibility").unwrap(), "", &alice_doc.clone().clone().verified().unwrap(), - &alice.signer, ) .unwrap(); + + let bob_identity_mut = Identity::load_mut(&*bob.repo, &bob.signer).unwrap(); + assert_eq!(*bob_identity_mut, bob_identity); + let mut bob_identity = bob_identity_mut; + // Bob makes the same change without knowing Alice already did. let b1 = bob_identity .update( cob::Title::new("Make private").unwrap(), "", &alice_doc.verified().unwrap(), - &bob.signer, ) .unwrap(); @@ -1301,7 +1303,7 @@ mod test { assert_eq!(bob_identity.revision(&b1).unwrap().state, State::Active); // Now Bob accepts Alice's proposal. This voids his own. - bob_identity.accept(&a2, &bob.signer).unwrap(); + bob_identity.accept(&a2).unwrap(); assert_eq!(bob_identity.current, a2); assert_eq!(bob_identity.revision(&a1).unwrap().state, State::Accepted); assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Accepted); @@ -1315,7 +1317,7 @@ mod test { let bob = &network.bob; let eve = &network.eve; - let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap(); + let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap(); let mut alice_doc = alice_identity.doc().clone().edit(); alice_doc.delegate(bob.signer.public_key().into()); @@ -1325,7 +1327,6 @@ mod test { cob::Title::new("Add Bob").unwrap(), "Eh.", &alice_doc.clone().clone().verified().unwrap(), - &alice.signer, ) .unwrap(); @@ -1335,20 +1336,16 @@ mod test { cob::Title::new("Change visibility").unwrap(), "Eh.", &alice_doc.verified().unwrap(), - &alice.signer, ) .unwrap(); bob.repo.fetch(alice); - let a3 = cob::stable::with_advanced_timestamp(|| { - alice_identity.redact(a2, &alice.signer).unwrap() - }); + let a3 = cob::stable::with_advanced_timestamp(|| alice_identity.redact(a2).unwrap()); assert!(alice_identity.revision(&a1).is_some()); assert_eq!(alice_identity.timeline, vec![a0, a1, a2, a3]); - let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap(); - let b1 = - cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2, &bob.signer).unwrap()); + let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap(); + let b1 = cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2).unwrap()); assert_eq!(bob_identity.timeline, vec![a0, a1, a2, b1]); assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Accepted); @@ -1367,7 +1364,7 @@ mod test { let bob = &network.bob; let eve = &network.eve; - let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap(); + let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap(); let mut alice_doc = alice_identity.doc().clone().edit(); alice_doc.delegate(bob.signer.public_key().into()); @@ -1378,7 +1375,6 @@ mod test { cob::Title::new("Add Bob and Eve").unwrap(), "Eh#!", &alice_doc.clone().verified().unwrap(), - &alice.signer, ) .unwrap(); @@ -1388,7 +1384,6 @@ mod test { cob::Title::new("Remove Eve").unwrap(), "", &alice_doc.verified().unwrap(), - &alice.signer, ) .unwrap(); @@ -1396,12 +1391,11 @@ mod test { bob.repo.fetch(alice); eve.repo.fetch(bob); - let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap(); - let b1 = - cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2, &bob.signer).unwrap()); + let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap(); + let b1 = cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2).unwrap()); assert_eq!(bob_identity.current, a2); - let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap(); + let mut eve_identity = Identity::load_mut(&*eve.repo, &eve.signer).unwrap(); let mut eve_doc = eve_identity.doc().clone().edit(); eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]); let e1 = cob::stable::with_advanced_timestamp(|| { @@ -1410,7 +1404,6 @@ mod test { cob::Title::new("Change visibility").unwrap(), "", &eve_doc.verified().unwrap(), - &eve.signer, ) .unwrap() }); @@ -1443,7 +1436,7 @@ mod test { let bob = &network.bob; let eve = &network.eve; - let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap(); + let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap(); let mut alice_doc = alice_identity.doc().clone().edit(); alice_doc.delegate(bob.signer.public_key().into()); @@ -1454,7 +1447,6 @@ mod test { cob::Title::new("Add Bob and Eve").unwrap(), "Eh!#", &alice_doc.clone().verified().unwrap(), - &alice.signer, ) .unwrap(); @@ -1464,7 +1456,6 @@ mod test { cob::Title::new("Change visibility").unwrap(), "", &alice_doc.verified().unwrap(), - &alice.signer, ) .unwrap(); @@ -1473,14 +1464,12 @@ mod test { eve.repo.fetch(bob); // Bob accepts alice's revision. - let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap(); - let b1 = - cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2, &bob.signer).unwrap()); + let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap(); + let b1 = cob::stable::with_advanced_timestamp(|| bob_identity.accept(&a2).unwrap()); // Eve rejects the revision, not knowing. - let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap(); - let e1 = - cob::stable::with_advanced_timestamp(|| eve_identity.reject(a2, &eve.signer).unwrap()); + let mut eve_identity = Identity::load_mut(&*eve.repo, &eve.signer).unwrap(); + let e1 = cob::stable::with_advanced_timestamp(|| eve_identity.reject(a2).unwrap()); assert!(eve_identity.revision(&a2).unwrap().is_active()); // Then she submits a new revision. @@ -1491,7 +1480,6 @@ mod test { cob::Title::new("Change visibility").unwrap(), "", &eve_doc.verified().unwrap(), - &eve.signer, ) .unwrap(); assert!(eve_identity.revision(&e2).unwrap().is_active()); @@ -1529,7 +1517,7 @@ mod test { let bob = &network.bob; let eve = &network.eve; - let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap(); + let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap(); let mut alice_doc = alice_identity.doc().clone().edit(); alice.repo.fetch(bob); @@ -1542,14 +1530,13 @@ mod test { cob::Title::new("Add Bob and Eve").unwrap(), "", &alice_doc.verified().unwrap(), - &alice.signer, ) .unwrap(); bob.repo.fetch(alice); eve.repo.fetch(alice); - let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap(); + let mut bob_identity = Identity::load_mut(&*bob.repo, &bob.signer).unwrap(); let mut bob_doc = bob_identity.doc().clone().edit(); assert!(bob_doc.is_delegate(&bob.signer.public_key().into())); @@ -1568,14 +1555,14 @@ mod test { cob::Title::new("Change visibility #1").unwrap(), "", &bob_doc.verified().unwrap(), - &bob.signer, ) .unwrap(); + alice.repo.fetch(bob); eve.repo.fetch(bob); // In the meantime, Eve does the same thing on her side. - let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap(); + let mut eve_identity = Identity::load_mut(&*eve.repo, &eve.signer).unwrap(); let mut eve_doc = eve_identity.doc().clone().edit(); eve_doc.visibility = Visibility::private([]); let e1 = eve_identity @@ -1583,18 +1570,16 @@ mod test { cob::Title::new("Change visibility #2").unwrap(), "Woops", &eve_doc.verified().unwrap(), - &eve.signer, ) .unwrap(); assert_eq!(eve_identity.revisions().count(), 4); assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Active); alice_identity.reload().unwrap(); - let a2 = cob::stable::with_advanced_timestamp(|| { - alice_identity.accept(&b1, &alice.signer).unwrap() - }); + let a2 = cob::stable::with_advanced_timestamp(|| alice_identity.accept(&b1).unwrap()); eve.repo.fetch(alice); + eve_identity.reload().unwrap(); assert_eq!(eve_identity.timeline, vec![a0, a1, b1, e1, a2]); @@ -1619,7 +1604,7 @@ mod test { rad::fork_remote(id, alice.public_key(), &eve, &storage).unwrap(); let repo = storage.repository(id).unwrap(); - let mut identity = Identity::load_mut(&repo).unwrap(); + let mut identity = Identity::load_mut(&repo, &alice).unwrap(); let doc = identity.doc().clone(); let prj = doc.project().unwrap(); let mut doc = doc.edit(); @@ -1633,7 +1618,6 @@ mod test { cob::Title::new("Update description").unwrap(), "", &doc.clone().verified().unwrap(), - &alice, ) .unwrap(); @@ -1645,7 +1629,6 @@ mod test { cob::Title::new("Add bob").unwrap(), "", &doc.clone().verified().unwrap(), - &alice, ) .unwrap(); @@ -1658,25 +1641,26 @@ mod test { cob::Title::new("Add eve").unwrap(), "", &doc.clone().verified().unwrap(), - &alice, ) .unwrap(); - identity.accept(&revision, &bob).unwrap(); + + let mut bob_identity = Identity::load_mut(&repo, &bob).unwrap(); + bob_identity.accept(&revision).unwrap(); // Update description again with signatures by Eve and Bob. let desc = prj.description().to_owned() + "?"; let prj = prj.update(None, desc, None).unwrap(); doc.payload.insert(PayloadId::project(), prj.into()); - - let revision = identity + let revision = bob_identity .update( cob::Title::new("Update description again").unwrap(), "Bob's repository", &doc.verified().unwrap(), - &bob, ) .unwrap(); - identity.accept(&revision, &eve).unwrap(); + + let mut eve_identity = Identity::load_mut(&repo, &eve).unwrap(); + eve_identity.accept(&revision).unwrap(); let identity: Identity = Identity::load(&repo).unwrap(); let root = repo.identity_root().unwrap(); diff --git a/crates/radicle/src/cob/issue.rs b/crates/radicle/src/cob/issue.rs index ee438c6e..5237bcbb 100644 --- a/crates/radicle/src/cob/issue.rs +++ b/crates/radicle/src/cob/issue.rs @@ -11,13 +11,13 @@ use thiserror::Error; use crate::cob; use crate::cob::common::{Author, Authorization, Label, Reaction, Timestamp, Uri}; use crate::cob::store::Transaction; +use crate::cob::store::access::WriteAs; use crate::cob::store::{Cob, CobAction}; use crate::cob::thread::{Comment, CommentId, Thread}; use crate::cob::{ActorId, Embed, EntryId, ObjectId, TypeName, op, store}; use crate::cob::{TitleError, thread}; use crate::identity::doc::DocError; use crate::node::NodeId; -use crate::node::device::Device; use crate::prelude::{Did, Doc, ReadRepository, RepoId}; use crate::storage; use crate::storage::{HasRepoId, RepositoryError, WriteRepository}; @@ -472,8 +472,10 @@ impl Issue { } } -impl<'a, 'g, R, C> From<IssueMut<'a, 'g, R, C>> for (IssueId, Issue) { - fn from(value: IssueMut<'a, 'g, R, C>) -> Self { +impl<'a, 'b, 'g, Repo, Signer, Cache> From<IssueMut<'a, 'b, 'g, Repo, Signer, Cache>> + for (IssueId, Issue) +{ + fn from(value: IssueMut<'a, 'b, 'g, Repo, Signer, Cache>) -> Self { (value.id, value.issue) } } @@ -579,14 +581,14 @@ impl<R: ReadRepository> store::Transaction<Issue, R> { } } -pub struct IssueMut<'a, 'g, R, C> { +pub struct IssueMut<'a, 'b, 'g, Repo, Signer, Cache> { id: ObjectId, issue: Issue, - store: &'g mut Issues<'a, R>, - cache: &'g mut C, + store: &'g mut Issues<'a, Repo, WriteAs<'b, Signer>>, + cache: &'g mut Cache, } -impl<R, C> std::fmt::Debug for IssueMut<'_, '_, R, C> { +impl<Repo, Signer, Cache> std::fmt::Debug for IssueMut<'_, '_, '_, Repo, Signer, Cache> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_struct("IssueMut") .field("id", &self.id) @@ -595,10 +597,14 @@ impl<R, C> std::fmt::Debug for IssueMut<'_, '_, R, C> { } } -impl<R, C> IssueMut<'_, '_, R, C> +impl<Repo, Signer, Cache> IssueMut<'_, '_, '_, Repo, Signer, Cache> where - R: WriteRepository + cob::Store<Namespace = NodeId>, - C: cob::cache::Update<Issue>, + Repo: WriteRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, + Cache: cob::cache::Update<Issue>, { /// Reload the issue data from storage. pub fn reload(&mut self) -> Result<(), store::Error> { @@ -606,7 +612,6 @@ where .store .get(&self.id)? .ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), self.id))?; - Ok(()) } @@ -616,132 +621,89 @@ where } /// Assign one or more actors to an issue. - pub fn assign<G>( - &mut self, - assignees: impl IntoIterator<Item = Did>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Assign", signer, |tx| tx.assign(assignees)) + pub fn assign(&mut self, assignees: impl IntoIterator<Item = Did>) -> Result<EntryId, Error> { + self.transaction("Assign", |tx| tx.assign(assignees)) } /// Set the issue title. - pub fn edit<G>(&mut self, title: cob::Title, signer: &Device<G>) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Edit", signer, |tx| tx.edit(title)) + pub fn edit(&mut self, title: cob::Title) -> Result<EntryId, Error> { + self.transaction("Edit", |tx| tx.edit(title)) } /// Set the issue description. - pub fn edit_description<G>( + pub fn edit_description( &mut self, description: impl ToString, embeds: impl IntoIterator<Item = Embed<Uri>>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - let (id, _) = self.root(); + ) -> Result<EntryId, Error> { + let (id, _) = self.issue.root(); let id = *id; - self.transaction("Edit description", signer, |tx| { + self.transaction("Edit description", |tx| { tx.edit_comment(id, description, embeds.into_iter().collect()) }) } /// Lifecycle an issue. - pub fn lifecycle<G>(&mut self, state: State, signer: &Device<G>) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Lifecycle", signer, |tx| tx.lifecycle(state)) + pub fn lifecycle(&mut self, state: State) -> Result<EntryId, Error> { + self.transaction("Lifecycle", |tx| tx.lifecycle(state)) } /// Comment on an issue. - pub fn comment<G, S>( + pub fn comment( &mut self, - body: S, + body: impl ToString, reply_to: CommentId, embeds: impl IntoIterator<Item = Embed<Uri>>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - S: ToString, - { - self.transaction("Comment", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("Comment", |tx| { tx.comment(body, reply_to, embeds.into_iter().collect()) }) } /// Edit a comment. - pub fn edit_comment<G, S>( + pub fn edit_comment( &mut self, id: CommentId, - body: S, + body: impl ToString, embeds: impl IntoIterator<Item = Embed<Uri>>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - S: ToString, - { - self.transaction("Edit comment", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("Edit comment", |tx| { tx.edit_comment(id, body, embeds.into_iter().collect()) }) } /// Redact a comment. - pub fn redact_comment<G>(&mut self, id: CommentId, signer: &Device<G>) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Redact comment", signer, |tx| tx.redact_comment(id)) + pub fn redact_comment(&mut self, id: CommentId) -> Result<EntryId, Error> { + self.transaction("Redact comment", |tx| tx.redact_comment(id)) } /// Label an issue. - pub fn label<G>( - &mut self, - labels: impl IntoIterator<Item = Label>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Label", signer, |tx| tx.label(labels)) + pub fn label(&mut self, labels: impl IntoIterator<Item = Label>) -> Result<EntryId, Error> { + self.transaction("Label", |tx| tx.label(labels)) } /// React to an issue comment. - pub fn react<G>( + pub fn react( &mut self, to: CommentId, reaction: Reaction, active: bool, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("React", signer, |tx| tx.react(to, reaction, active)) + ) -> Result<EntryId, Error> { + self.transaction("React", |tx| tx.react(to, reaction, active)) } - pub fn transaction<G, F>( - &mut self, - message: &str, - signer: &Device<G>, - operations: F, - ) -> Result<EntryId, Error> + pub fn transaction<F>(&mut self, message: &str, operations: F) -> Result<EntryId, Error> where - G: crypto::signature::Signer<crypto::Signature>, - F: FnOnce(&mut Transaction<Issue, R>) -> Result<(), store::Error>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, + F: FnOnce(&mut Transaction<Issue, Repo>) -> Result<(), store::Error>, { let mut tx = Transaction::default(); operations(&mut tx)?; - let (issue, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?; + let (issue, commit) = tx.commit(message, self.id, &mut self.store.raw)?; self.cache .update(&self.store.as_ref().id(), &self.id, &issue) .map_err(|e| Error::CacheUpdate { @@ -754,7 +716,7 @@ where } } -impl<R, C> Deref for IssueMut<'_, '_, R, C> { +impl<Repo, Signer, Cache> Deref for IssueMut<'_, '_, '_, Repo, Signer, Cache> { type Target = Issue; fn deref(&self) -> &Self::Target { @@ -762,24 +724,24 @@ impl<R, C> Deref for IssueMut<'_, '_, R, C> { } } -pub struct Issues<'a, R> { - raw: store::Store<'a, Issue, R>, +pub struct Issues<'a, Repo, Access> { + raw: store::Store<'a, Issue, Repo, Access>, } -impl<'a, R> Deref for Issues<'a, R> { - type Target = store::Store<'a, Issue, R>; +impl<'a, Repo, Access> Deref for Issues<'a, Repo, Access> { + type Target = store::Store<'a, Issue, Repo, Access>; fn deref(&self) -> &Self::Target { &self.raw } } -impl<R> HasRepoId for Issues<'_, R> +impl<Repo, Access> HasRepoId for Issues<'_, Repo, Access> where - R: ReadRepository, + Repo: HasRepoId, { fn rid(&self) -> RepoId { - self.raw.as_ref().id() + self.raw.rid() } } @@ -798,39 +760,61 @@ impl IssueCounts { } } -impl<'a, R> Issues<'a, R> +impl<'a, Repo, Access> Issues<'a, Repo, Access> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, + Access: store::access::Access, { /// Open an issues store. - pub fn open(repository: &'a R) -> Result<Self, RepositoryError> { + pub fn open(repository: &'a Repo, access: Access) -> Result<Self, RepositoryError> { let identity = repository.identity_head()?; - let raw = store::Store::open(repository)?.identity(identity); + let raw = store::Store::open(repository, access)?.identity(identity); Ok(Self { raw }) } } -impl<'a, R> Issues<'a, R> +impl<'a, 'b, Repo, Signer> Issues<'a, Repo, WriteAs<'b, Signer>> where - R: WriteRepository + cob::Store<Namespace = NodeId>, + Repo: WriteRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { + /// Get an issue mutably. + pub fn get_mut<'g, Cache>( + &'g mut self, + id: &ObjectId, + cache: &'g mut Cache, + ) -> Result<IssueMut<'a, 'b, 'g, Repo, Signer, Cache>, Error> { + let issue = self + .raw + .get(id)? + .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *id))?; + + Ok(IssueMut { + id: *id, + issue, + store: self, + cache, + }) + } + /// Create a new issue. - pub fn create<'g, G, C>( + pub fn create<'g, Cache>( &'g mut self, title: cob::Title, description: impl ToString, labels: &[Label], assignees: &[Did], embeds: impl IntoIterator<Item = Embed<Uri>>, - cache: &'g mut C, - signer: &Device<G>, - ) -> Result<IssueMut<'a, 'g, R, C>, Error> + cache: &'g mut Cache, + ) -> Result<IssueMut<'a, 'b, 'g, Repo, Signer, Cache>, Error> where - G: crypto::signature::Signer<crypto::Signature>, - C: cob::cache::Update<Issue>, + Cache: 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, |tx, _| { tx.thread(description, embeds)?; tx.edit(title)?; @@ -855,43 +839,24 @@ where } /// Remove an issue. - pub fn remove<C, G>(&self, id: &ObjectId, signer: &Device<G>) -> Result<(), store::Error> + pub fn remove<C, G>(&mut self, id: &ObjectId) -> Result<(), store::Error> where C: cob::cache::Remove<Issue>, - G: crypto::signature::Signer<crypto::Signature>, { - self.raw.remove(id, signer) + self.raw.remove(id) } } -impl<'a, R> Issues<'a, R> +impl<'a, Repo, Access> Issues<'a, Repo, Access> where - R: ReadRepository + cob::Store, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, + Access: store::access::Access, { /// Get an issue. pub fn get(&self, id: &ObjectId) -> Result<Option<Issue>, store::Error> { self.raw.get(id) } - /// Get an issue mutably. - pub fn get_mut<'g, C>( - &'g mut self, - id: &ObjectId, - cache: &'g mut C, - ) -> Result<IssueMut<'a, 'g, R, C>, store::Error> { - let issue = self - .raw - .get(id)? - .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *id))?; - - Ok(IssueMut { - id: *id, - issue, - store: self, - cache, - }) - } - /// Issues count by state. pub fn counts(&self) -> Result<IssueCounts, Error> { let all = self.all()?; @@ -984,23 +949,24 @@ mod test { use crate::cob::{ActorId, Reaction, store::CobWithType}; use crate::git::Oid; use crate::issue::cache::Issues as _; + use crate::node::device::Device; use crate::test::arbitrary; use crate::{assert_matches, test}; #[test] fn test_concurrency() { let t = test::setup::Network::default(); - let mut issues_alice = Cache::no_cache(&*t.alice.repo).unwrap(); - let mut bob_issues = Cache::no_cache(&*t.bob.repo).unwrap(); - let mut eve_issues = Cache::no_cache(&*t.eve.repo).unwrap(); - let mut issue_alice = issues_alice + + let mut alice_issues = Cache::no_cache(&*t.alice.repo, &t.alice.signer).unwrap(); + let mut bob_issues = Cache::no_cache(&*t.bob.repo, &t.bob.signer).unwrap(); + let mut eve_issues = Cache::no_cache(&*t.eve.repo, &t.eve.signer).unwrap(); + let mut issue_alice = alice_issues .create( cob::Title::new("Alice Issue").unwrap(), "Alice's comment", &[], &[], [], - &t.alice.signer, ) .unwrap(); let id = *issue_alice.id(); @@ -1011,12 +977,8 @@ mod test { let mut issue_eve = eve_issues.get_mut(&id).unwrap(); let mut issue_bob = bob_issues.get_mut(&id).unwrap(); - issue_bob - .comment("Bob's reply", *id, vec![], &t.bob.signer) - .unwrap(); - issue_alice - .comment("Alice's reply", *id, vec![], &t.alice.signer) - .unwrap(); + issue_bob.comment("Bob's reply", *id, vec![]).unwrap(); + issue_alice.comment("Alice's reply", *id, vec![]).unwrap(); assert_eq!(issue_bob.comments().count(), 2); assert_eq!(issue_alice.comments().count(), 2); @@ -1042,9 +1004,7 @@ mod test { t.eve.repo.fetch(&t.alice); - let eve_reply = issue_eve - .comment("Eve's reply", *id, vec![], &t.eve.signer) - .unwrap(); + let eve_reply = issue_eve.comment("Eve's reply", *id, vec![]).unwrap(); t.bob.repo.fetch(&t.eve); t.alice.repo.fetch(&t.eve); @@ -1078,7 +1038,7 @@ mod test { #[test] fn test_issue_create_and_assign() { 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, &node.signer).unwrap(); let assignee = Did::from(arbitrary::r#gen::<ActorId>(1)); let assignee_two = Did::from(arbitrary::r#gen::<ActorId>(1)); @@ -1089,7 +1049,6 @@ mod test { &[], &[assignee], [], - &node.signer, ) .unwrap(); @@ -1101,9 +1060,7 @@ mod test { assert!(assignees.contains(&assignee)); let mut issue = issues.get_mut(&id).unwrap(); - issue - .assign([assignee, assignee_two], &node.signer) - .unwrap(); + issue.assign([assignee, assignee_two]).unwrap(); let id = issue.id; let issue = issues.get(&id).unwrap().unwrap(); @@ -1117,7 +1074,7 @@ mod test { #[test] fn test_issue_create_and_reassign() { 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, &node.signer).unwrap(); let assignee = Did::from(arbitrary::r#gen::<ActorId>(1)); let assignee_two = Did::from(arbitrary::r#gen::<ActorId>(1)); @@ -1128,12 +1085,11 @@ mod test { &[], &[assignee, assignee_two], [], - &node.signer, ) .unwrap(); - issue.assign([assignee_two], &node.signer).unwrap(); - issue.assign([assignee_two], &node.signer).unwrap(); + issue.assign([assignee_two]).unwrap(); + issue.assign([assignee_two]).unwrap(); issue.reload().unwrap(); let assignees: Vec<_> = issue.assignees().cloned().collect::<Vec<_>>(); @@ -1141,7 +1097,7 @@ mod test { assert_eq!(1, assignees.len()); assert!(assignees.contains(&assignee_two)); - issue.assign([], &node.signer).unwrap(); + issue.assign([]).unwrap(); issue.reload().unwrap(); assert_eq!(0, issue.assignees().count()); @@ -1150,7 +1106,7 @@ mod test { #[test] fn test_issue_create_and_get() { 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, &node.signer).unwrap(); let created = issues .create( cob::Title::new("My first issue").unwrap(), @@ -1158,7 +1114,6 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); @@ -1176,7 +1131,7 @@ mod test { #[test] fn test_issue_create_and_change_state() { 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, &node.signer).unwrap(); let mut issue = issues .create( cob::Title::new("My first issue").unwrap(), @@ -1184,17 +1139,13 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); issue - .lifecycle( - State::Closed { - reason: CloseReason::Other, - }, - &node.signer, - ) + .lifecycle(State::Closed { + reason: CloseReason::Other, + }) .unwrap(); let id = issue.id; @@ -1207,7 +1158,7 @@ mod test { } ); - issue.lifecycle(State::Open, &node.signer).unwrap(); + issue.lifecycle(State::Open).unwrap(); let issue = issues.get(&id).unwrap().unwrap(); assert_eq!(*issue.state(), State::Open); @@ -1216,7 +1167,7 @@ mod test { #[test] fn test_issue_create_and_unassign() { 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, &node.signer).unwrap(); let assignee = Did::from(arbitrary::r#gen::<ActorId>(1)); let assignee_two = Did::from(arbitrary::r#gen::<ActorId>(1)); @@ -1227,12 +1178,11 @@ mod test { &[], &[assignee, assignee_two], [], - &node.signer, ) .unwrap(); assert_eq!(2, issue.assignees().count()); - issue.assign([assignee_two], &node.signer).unwrap(); + issue.assign([assignee_two]).unwrap(); issue.reload().unwrap(); let assignees: Vec<_> = issue.assignees().cloned().collect::<Vec<_>>(); @@ -1244,7 +1194,7 @@ mod test { #[test] fn test_issue_edit() { 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, &node.signer).unwrap(); let mut issue = issues .create( @@ -1253,13 +1203,10 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); - issue - .edit(cob::Title::new("Sorry typo").unwrap(), &node.signer) - .unwrap(); + issue.edit(cob::Title::new("Sorry typo").unwrap()).unwrap(); let id = issue.id; let issue = issues.get(&id).unwrap().unwrap(); @@ -1271,7 +1218,7 @@ mod test { #[test] fn test_issue_edit_description() { 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, &node.signer).unwrap(); let mut issue = issues .create( cob::Title::new("My first issue").unwrap(), @@ -1279,12 +1226,11 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); issue - .edit_description("Bob Loblaw law blog", vec![], &node.signer) + .edit_description("Bob Loblaw law blog", vec![]) .unwrap(); let id = issue.id; @@ -1297,7 +1243,7 @@ mod test { #[test] fn test_issue_react() { 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, &node.signer).unwrap(); let mut issue = issues .create( cob::Title::new("My first issue").unwrap(), @@ -1305,14 +1251,13 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); let (comment, _) = issue.root(); let comment = *comment; let reaction = Reaction::new('🥳').unwrap(); - issue.react(comment, reaction, true, &node.signer).unwrap(); + issue.react(comment, reaction, true).unwrap(); let id = issue.id; let issue = issues.get(&id).unwrap().unwrap(); @@ -1327,7 +1272,7 @@ mod test { #[test] fn test_issue_reply() { 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, &node.signer).unwrap(); let mut issue = issues .create( cob::Title::new("My first issue").unwrap(), @@ -1335,18 +1280,13 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); let (root, _) = issue.root(); let root = *root; - let c1 = issue - .comment("Hi hi hi.", root, vec![], &node.signer) - .unwrap(); - let c2 = issue - .comment("Ha ha ha.", root, vec![], &node.signer) - .unwrap(); + let c1 = issue.comment("Hi hi hi.", root, vec![]).unwrap(); + let c2 = issue.comment("Ha ha ha.", root, vec![]).unwrap(); let id = issue.id; let mut issue = issues.get_mut(&id).unwrap(); @@ -1356,14 +1296,10 @@ mod test { assert_eq!(reply1.body(), "Hi hi hi."); assert_eq!(reply2.body(), "Ha ha ha."); - issue.comment("Re: Hi.", c1, vec![], &node.signer).unwrap(); - issue.comment("Re: Ha.", c2, vec![], &node.signer).unwrap(); - issue - .comment("Re: Ha. Ha.", c2, vec![], &node.signer) - .unwrap(); - issue - .comment("Re: Ha. Ha. Ha.", c2, vec![], &node.signer) - .unwrap(); + issue.comment("Re: Hi.", c1, vec![]).unwrap(); + issue.comment("Re: Ha.", c2, vec![]).unwrap(); + issue.comment("Re: Ha. Ha.", c2, vec![]).unwrap(); + issue.comment("Re: Ha. Ha. Ha.", c2, vec![]).unwrap(); let issue = issues.get(&id).unwrap().unwrap(); @@ -1382,7 +1318,7 @@ mod test { #[test] fn test_issue_label() { 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, &node.signer).unwrap(); let bug_label = Label::new("bug").unwrap(); let ux_label = Label::new("ux").unwrap(); let wontfix_label = Label::new("wontfix").unwrap(); @@ -1393,18 +1329,12 @@ mod test { std::slice::from_ref(&ux_label), &[], [], - &node.signer, ) .unwrap(); + issue.label([ux_label.clone(), bug_label.clone()]).unwrap(); issue - .label([ux_label.clone(), bug_label.clone()], &node.signer) - .unwrap(); - issue - .label( - [ux_label.clone(), bug_label.clone(), wontfix_label.clone()], - &node.signer, - ) + .label([ux_label.clone(), bug_label.clone(), wontfix_label.clone()]) .unwrap(); let id = issue.id; @@ -1420,7 +1350,7 @@ mod test { fn test_issue_comment() { let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default(); let author = *node.signer.public_key(); - let mut issues = Cache::no_cache(&*repo).unwrap(); + let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap(); let mut issue = issues .create( cob::Title::new("My first issue").unwrap(), @@ -1428,7 +1358,6 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); @@ -1436,12 +1365,8 @@ mod test { let (c0, _) = issue.root(); let c0 = *c0; - issue - .comment("Ho ho ho.", c0, vec![], &node.signer) - .unwrap(); - issue - .comment("Ha ha ha.", c0, vec![], &node.signer) - .unwrap(); + issue.comment("Ho ho ho.", c0, vec![]).unwrap(); + issue.comment("Ha ha ha.", c0, vec![]).unwrap(); let id = issue.id; let issue = issues.get(&id).unwrap().unwrap(); @@ -1460,7 +1385,7 @@ mod test { #[test] fn test_issue_comment_redact() { 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, &node.signer).unwrap(); let mut issue = issues .create( cob::Title::new("My first issue").unwrap(), @@ -1468,7 +1393,6 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); @@ -1476,17 +1400,15 @@ mod test { let (c0, _) = issue.root(); let c0 = *c0; - let comment = issue - .comment("Ho ho ho.", c0, vec![], &node.signer) - .unwrap(); + let comment = issue.comment("Ho ho ho.", c0, vec![]).unwrap(); issue.reload().unwrap(); assert_eq!(issue.comments().count(), 2); - issue.redact_comment(comment, &node.signer).unwrap(); + issue.redact_comment(comment).unwrap(); assert_eq!(issue.comments().count(), 1); // Can't redact root comment. - issue.redact_comment(*issue.id, &node.signer).unwrap_err(); + issue.redact_comment(*issue.id).unwrap_err(); } #[test] @@ -1508,36 +1430,15 @@ mod test { #[test] fn test_issue_all() { 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, &node.signer).unwrap(); issues - .create( - cob::Title::new("First").unwrap(), - "Blah", - &[], - &[], - [], - &node.signer, - ) + .create(cob::Title::new("First").unwrap(), "Blah", &[], &[], []) .unwrap(); issues - .create( - cob::Title::new("Second").unwrap(), - "Blah", - &[], - &[], - [], - &node.signer, - ) + .create(cob::Title::new("Second").unwrap(), "Blah", &[], &[], []) .unwrap(); issues - .create( - cob::Title::new("Third").unwrap(), - "Blah", - &[], - &[], - [], - &node.signer, - ) + .create(cob::Title::new("Third").unwrap(), "Blah", &[], &[], []) .unwrap(); let issues = issues @@ -1557,7 +1458,7 @@ mod test { #[test] fn test_issue_multilines() { 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, &node.signer).unwrap(); let created = issues .create( cob::Title::new("My first issue").unwrap(), @@ -1565,7 +1466,6 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); @@ -1583,7 +1483,7 @@ mod test { #[test] fn test_embeds() { 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, &node.signer).unwrap(); let content1 = repo.backend.blob(b"<html>Hello World!</html>").unwrap(); let content2 = repo.backend.blob(b"<html>Hello Radicle!</html>").unwrap(); @@ -1608,17 +1508,11 @@ mod test { &[], &[], [embed1.clone(), embed2.clone()], - &node.signer, ) .unwrap(); issue - .comment( - "Here's a binary file", - *issue.id, - [embed3.clone()], - &node.signer, - ) + .comment("Here's a binary file", *issue.id, [embed3.clone()]) .unwrap(); issue.reload().unwrap(); @@ -1642,7 +1536,7 @@ mod test { #[test] fn test_embeds_edit() { 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, &node.signer).unwrap(); let content1 = repo.backend.blob(b"<html>Hello World!</html>").unwrap(); let content1_edited = repo.backend.blob(b"<html>Hello Radicle!</html>").unwrap(); @@ -1667,13 +1561,12 @@ mod test { &[], &[], [embed1, embed2], - &node.signer, ) .unwrap(); issue.reload().unwrap(); issue - .edit_description("My first issue", [embed1_edited.clone()], &node.signer) + .edit_description("My first issue", [embed1_edited.clone()]) .unwrap(); issue.reload().unwrap(); @@ -1691,7 +1584,7 @@ mod test { #[test] fn test_invalid_actions() { 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, &node.signer).unwrap(); let mut issue = issues .create( cob::Title::new("My first issue").unwrap(), @@ -1699,14 +1592,11 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); let missing = arbitrary::oid(); - issue - .comment("Invalid", missing, [], &node.signer) - .unwrap_err(); + issue.comment("Invalid", missing, []).unwrap_err(); assert_eq!(issue.comments().count(), 1); issue.reload().unwrap(); assert_eq!(issue.comments().count(), 1); @@ -1725,7 +1615,7 @@ mod test { #[test] fn test_invalid_tx() { 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, &node.signer).unwrap(); let mut issue = issues .create( cob::Title::new("My first issue").unwrap(), @@ -1733,7 +1623,6 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); let missing = arbitrary::oid(); @@ -1742,7 +1631,7 @@ mod test { // Even creating it via a transaction will trigger an error. let mut tx = Transaction::<Issue, _>::default(); tx.comment("Invalid comment", missing, vec![]).unwrap(); - tx.commit("Add comment", issue.id, &mut issue.store.raw, &node.signer) + tx.commit("Add comment", issue.id, &mut issue.store.raw) .unwrap_err(); issue.reload().unwrap(); @@ -1752,7 +1641,7 @@ mod test { #[test] fn test_invalid_tx_reference() { 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, &node.signer).unwrap(); let issue = issues .create( cob::Title::new("My first issue").unwrap(), @@ -1760,7 +1649,6 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); @@ -1783,7 +1671,7 @@ mod test { let identity = repo.identity().unwrap().head(); let missing = arbitrary::oid(); let type_name = Issue::type_name().clone(); - let mut issues = Cache::no_cache(&*repo).unwrap(); + let mut issues = Cache::no_cache(&*repo, &node.signer).unwrap(); let mut issue = issues .create( cob::Title::new("My first issue").unwrap(), @@ -1791,7 +1679,6 @@ mod test { &[], &[], [], - &node.signer, ) .unwrap(); @@ -1848,9 +1735,7 @@ mod test { // Additionally, when adding a *valid* comment, it does not build upon the bad operation. issue.reload().unwrap(); - issue - .comment("Valid comment", *issue.id, vec![], &node.signer) - .unwrap(); + issue.comment("Valid comment", *issue.id, vec![]).unwrap(); issue.reload().unwrap(); assert_eq!(issue.comments().count(), 2); assert_eq!(issue.thread.timeline().count(), 2); @@ -1864,10 +1749,13 @@ mod test { assert_eq!(cob.history.len(), 3); assert_eq!(cob.object.len(), 3); - // If Eve now writes a valid comment via the `Issue` type, it will overwrite her invalid + let mut eve_issues = Cache::no_cache(&*repo, &eve).unwrap(); + let mut eve_issue = eve_issues.get_mut(issue.id()).unwrap(); + + // If Eve now writes a valid comment via the `IssueMut` type, it will overwrite her invalid // one, since it won't be loaded as a tip. - issue - .comment("Eve's comment", *issue.id, vec![], &eve) + eve_issue + .comment("Eve's comment", *issue.id, vec![]) .unwrap(); let cob = cob::get::<NonEmpty<cob::Entry>, _>(&*repo, &type_name, issue.id()) @@ -1876,7 +1764,7 @@ mod test { // There are three nodes still, but they are all valid comments. // The invalid comment of Eve was replaced with a valid one. - assert_eq!(issue.comments().count(), 3); + assert_eq!(eve_issue.comments().count(), 3); assert_eq!(cob.history.len(), 3); assert_eq!(cob.object.len(), 3); } diff --git a/crates/radicle/src/cob/issue/cache.rs b/crates/radicle/src/cob/issue/cache.rs index 08cef436..858a7e24 100644 --- a/crates/radicle/src/cob/issue/cache.rs +++ b/crates/radicle/src/cob/issue/cache.rs @@ -8,9 +8,9 @@ use crate::cob; use crate::cob::cache; use crate::cob::cache::{Remove, StoreReader, StoreWriter, Update}; use crate::cob::store; +use crate::cob::store::access::{ReadOnly, WriteAs}; use crate::cob::{Embed, Label, ObjectId, TypeName, Uri}; use crate::node::NodeId; -use crate::node::device::Device; use crate::prelude::{Did, RepoId}; use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository}; @@ -77,39 +77,45 @@ impl<T> IssuesMut for T where T: Issues + Update<Issue> + Remove<Issue> {} /// The `store` is used for the main storage when performing a /// write-through. It is also used for identifying which `RepoId` is /// being used for the `cache`. -pub struct Cache<R, C> { - store: R, +pub struct Cache<'a, Repo, Access, C> { + store: super::Issues<'a, Repo, Access>, cache: C, } -impl<R, C> Cache<R, C> { - pub fn new(store: R, cache: C) -> Self { +impl<'a, Repo, Access, C> Cache<'a, Repo, Access, C> { + pub fn new(store: super::Issues<'a, Repo, Access>, cache: C) -> Self { Self { store, cache } } +} - pub fn rid(&self) -> RepoId - where - R: HasRepoId, - { +impl<'a, Repo, Access, C> HasRepoId for Cache<'a, Repo, Access, C> +where + Repo: HasRepoId, +{ + fn rid(&self) -> RepoId { self.store.rid() } } -impl<'a, R, C> Cache<super::Issues<'a, R>, C> { +impl<'a, 'b, Repo, Signer, C> Cache<'a, Repo, WriteAs<'b, Signer>, C> +where + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, +{ /// Create a new [`Issue`] using the [`super::Issues`] as the /// main storage, and writing the update to the `cache`. - pub fn create<'g, G>( + pub fn create<'g>( &'g mut self, title: cob::Title, description: impl ToString, labels: &[Label], assignees: &[Did], embeds: impl IntoIterator<Item = Embed<Uri>>, - signer: &Device<G>, - ) -> Result<IssueMut<'a, 'g, R, C>, super::Error> + ) -> Result<IssueMut<'a, 'b, 'g, Repo, Signer, C>, super::Error> where - R: ReadRepository + WriteRepository + cob::Store<Namespace = NodeId>, - G: crypto::signature::Signer<crypto::Signature>, + Repo: ReadRepository + WriteRepository + cob::Store<Namespace = NodeId>, C: Update<Issue>, { self.store.create( @@ -119,19 +125,17 @@ impl<'a, R, C> Cache<super::Issues<'a, R>, C> { assignees, embeds, &mut self.cache, - signer, ) } /// Remove the given `id` from the [`super::Issues`] storage, and /// removing the entry from the `cache`. - pub fn remove<G>(&mut self, id: &IssueId, signer: &Device<G>) -> Result<(), super::Error> + pub fn remove(&mut self, id: &IssueId) -> Result<(), super::Error> where - G: crypto::signature::Signer<crypto::Signature>, - R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, C: Remove<Issue>, { - self.store.remove(id, signer)?; + self.store.raw.remove(id)?; self.cache .remove(id) .map_err(|e| super::Error::CacheRemove { @@ -140,12 +144,17 @@ impl<'a, R, C> Cache<super::Issues<'a, R>, C> { })?; Ok(()) } +} +impl<'a, Repo, Access, C> Cache<'a, Repo, Access, C> +where + Access: cob::store::access::Access, +{ /// Read the given `id` from the [`super::Issues`] store and /// writing it to the `cache`. pub fn write(&mut self, id: &IssueId) -> Result<(), super::Error> where - R: ReadRepository + cob::Store, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, C: Update<Issue>, { let issue = self @@ -171,7 +180,7 @@ impl<'a, R, C> Cache<super::Issues<'a, R>, C> { on_issue: impl Fn(&Result<(IssueId, Issue), store::Error>, &cache::Progress) -> ControlFlow<()>, ) -> Result<(), super::Error> where - R: ReadRepository + cob::Store, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, C: Update<Issue> + Remove<Issue>, { // Start by clearing the cache. This will get rid of issues that are cached but @@ -198,14 +207,15 @@ impl<'a, R, C> Cache<super::Issues<'a, R>, C> { } } -impl<'a, R> Cache<super::Issues<'a, R>, cache::NoCache> +impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, cache::NoCache> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: WriteRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Signer<crypto::Signature>, { /// Get a `Cache` that does no write-through modifications and /// uses the [`super::Issues`] store for all reads and writes. - pub fn no_cache(repository: &'a R) -> Result<Self, RepositoryError> { - let store = super::Issues::open(repository)?; + pub fn no_cache(repository: &'a Repo, signer: &'b Signer) -> Result<Self, RepositoryError> { + let store = super::Issues::open(repository, WriteAs::new(signer))?; Ok(Self { store, cache: cache::NoCache, @@ -216,7 +226,7 @@ where pub fn get_mut<'g>( &'g mut self, id: &ObjectId, - ) -> Result<IssueMut<'a, 'g, R, cache::NoCache>, super::Error> { + ) -> Result<IssueMut<'a, 'b, 'g, Repo, Signer, cache::NoCache>, super::Error> { let issue = self .store .get(id)? @@ -231,28 +241,28 @@ where } } -impl<R> Cache<R, StoreReader> { - pub fn reader(store: R, cache: StoreReader) -> Self { +impl<'a, Repo> Cache<'a, Repo, ReadOnly, StoreReader> { + pub fn reader(store: super::Issues<'a, Repo, ReadOnly>, cache: StoreReader) -> Self { Self { store, cache } } } -impl<R> Cache<R, StoreWriter> { - pub fn open(store: R, cache: StoreWriter) -> Self { +impl<'a, Repo, Access> Cache<'a, Repo, Access, StoreWriter> { + pub fn open(store: super::Issues<'a, Repo, Access>, cache: StoreWriter) -> Self { Self { store, cache } } } -impl<'a, R> Cache<super::Issues<'a, R>, StoreWriter> +impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, StoreWriter> where - R: ReadRepository + cob::Store, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, { /// Get the [`IssueMut`], identified by `id`, using the /// `StoreWriter` for retrieving the `Issue`. pub fn get_mut<'g>( &'g mut self, id: &ObjectId, - ) -> Result<IssueMut<'a, 'g, R, StoreWriter>, Error> { + ) -> Result<IssueMut<'a, 'b, 'g, Repo, Signer, StoreWriter>, Error> { let issue = Issues::get(self, id)? .ok_or_else(move || Error::NotFound(super::TYPENAME.clone(), *id))?; @@ -265,7 +275,7 @@ where } } -impl<R, C> cache::Update<Issue> for Cache<R, C> +impl<'a, Repo, Access, C> cache::Update<Issue> for Cache<'a, Repo, Access, C> where C: cache::Update<Issue>, { @@ -282,7 +292,7 @@ where } } -impl<R, C> cache::Remove<Issue> for Cache<R, C> +impl<'a, Repo, Access, C> cache::Remove<Issue> for Cache<'a, Repo, Access, C> where C: cache::Remove<Issue>, { @@ -377,9 +387,10 @@ impl Iterator for NoCacheIter<'_> { } } -impl<R> Issues for Cache<super::Issues<'_, R>, cache::NoCache> +impl<'a, Repo, Access> Issues for Cache<'a, Repo, Access, cache::NoCache> where - R: ReadRepository + cob::Store, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, + Access: store::access::Access, { type Error = super::Error; type Iter<'b> @@ -459,9 +470,9 @@ impl Iterator for IssuesIter<'_> { } } -impl<R> Issues for Cache<R, StoreWriter> +impl<'a, Repo, Access> Issues for Cache<'a, Repo, Access, StoreWriter> where - R: HasRepoId, + Repo: HasRepoId, { type Error = Error; type Iter<'b> @@ -486,9 +497,9 @@ where } } -impl<R> Issues for Cache<R, StoreReader> +impl<'a, Repo, Access> Issues for Cache<'a, Repo, Access, StoreReader> where - R: HasRepoId, + Repo: HasRepoId, { type Error = Error; type Iter<'b> @@ -618,14 +629,17 @@ mod tests { use crate::cob::cache::{Store, Update, Write}; use crate::cob::migrate; + use crate::cob::store::access::ReadOnly; use crate::cob::thread::Thread; use crate::issue::{CloseReason, Issue, IssueCounts, IssueId, State}; + use crate::storage::HasRepoId as _; use crate::test::arbitrary; use crate::test::storage::MockRepository; use super::{Cache, Issues}; - fn memory(store: MockRepository) -> Cache<MockRepository, Store<Write>> { + fn memory<'a>(store: &'a MockRepository) -> Cache<'a, MockRepository, ReadOnly, Store<Write>> { + let store = super::super::Issues::open(store, ReadOnly).unwrap(); let cache = Store::<Write>::memory() .unwrap() .with_migrations(migrate::ignore) @@ -636,7 +650,7 @@ mod tests { #[test] fn test_is_empty() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); assert!(cache.is_empty().unwrap()); let issue = Issue::new(Thread::default()); @@ -658,7 +672,7 @@ mod tests { #[test] fn test_counts() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); let n_open = arbitrary::r#gen::<u8>(0); let n_closed = arbitrary::r#gen::<u8>(1); let open_ids = (0..n_open) @@ -699,7 +713,7 @@ mod tests { #[test] fn test_get() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); let ids = (0..arbitrary::r#gen::<u8>(1)) .map(|_| IssueId::from(arbitrary::oid())) .collect::<BTreeSet<IssueId>>(); @@ -734,7 +748,7 @@ mod tests { #[test] fn test_list() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); let ids = (0..arbitrary::r#gen::<u8>(1)) .map(|_| IssueId::from(arbitrary::oid())) .collect::<BTreeSet<IssueId>>(); @@ -764,7 +778,7 @@ mod tests { #[test] fn test_list_by_status() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); let ids = (0..arbitrary::r#gen::<u8>(1)) .map(|_| IssueId::from(arbitrary::oid())) .collect::<BTreeSet<IssueId>>(); @@ -794,7 +808,7 @@ mod tests { #[test] fn test_remove() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); let ids = (0..arbitrary::r#gen::<u8>(1)) .map(|_| IssueId::from(arbitrary::oid())) .collect::<BTreeSet<IssueId>>(); diff --git a/crates/radicle/src/cob/patch.rs b/crates/radicle/src/cob/patch.rs index bb5ae28c..21e80081 100644 --- a/crates/radicle/src/cob/patch.rs +++ b/crates/radicle/src/cob/patch.rs @@ -21,6 +21,7 @@ use thiserror::Error; use crate::cob; use crate::cob::common::{Author, Authorization, CodeLocation, Label, Reaction, Timestamp}; use crate::cob::store::Transaction; +use crate::cob::store::access::WriteAs; use crate::cob::store::{Cob, CobAction}; use crate::cob::thread; use crate::cob::thread::Thread; @@ -30,7 +31,6 @@ use crate::crypto::PublicKey; use crate::git; use crate::identity::PayloadError; use crate::identity::doc::{DocAt, DocError}; -use crate::node::device::Device; use crate::prelude::*; use crate::storage; @@ -368,15 +368,17 @@ impl<R: WriteRepository> Merged<'_, R> { /// /// This removes Git refs relating to the patch, both in the working copy, /// and the stored copy; and updates `rad/sigrefs`. - pub fn cleanup<G>( + pub fn cleanup<Signer>( self, working: &git::raw::Repository, - signer: &Device<G>, + signer: &Signer, ) -> Result<(), storage::RepositoryError> where - G: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { - let nid = signer.public_key(); + let nid = &signer.verifying_key(); let stored_ref = git::refs::patch(&self.patch).with_namespace(nid.into()); let working_ref = git::refs::workdir::patch_upstream(&self.patch); @@ -2038,20 +2040,28 @@ impl<R: ReadRepository> store::Transaction<Patch, R> { } } -pub struct PatchMut<'a, 'g, R, C> { +pub struct PatchMut<'a, 'b, 'g, Repo, Signer, Cache> { pub id: ObjectId, patch: Patch, - store: &'g mut Patches<'a, R>, - cache: &'g mut C, + store: &'g mut Patches<'a, Repo, WriteAs<'b, Signer>>, + cache: &'g mut Cache, } -impl<'a, 'g, R, C> PatchMut<'a, 'g, R, C> +impl<'a, 'b, 'g, Repo, Signer, Update> PatchMut<'a, 'b, 'g, Repo, Signer, Update> where - C: cob::cache::Update<Patch>, - R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, + Update: cob::cache::Update<Patch>, { - pub fn new(id: ObjectId, patch: Patch, cache: &'g mut Cache<Patches<'a, R>, C>) -> Self { + pub fn new( + id: ObjectId, + patch: Patch, + cache: &'g mut Cache<'a, Repo, WriteAs<'b, Signer>, Update>, + ) -> Self { Self { id, patch, @@ -2074,20 +2084,14 @@ where Ok(()) } - pub fn transaction<G, F>( - &mut self, - message: &str, - signer: &Device<G>, - operations: F, - ) -> Result<EntryId, Error> + pub fn transaction<F>(&mut self, message: &str, operations: F) -> Result<EntryId, Error> where - G: crypto::signature::Signer<crypto::Signature>, - F: FnOnce(&mut Transaction<Patch, R>) -> Result<(), store::Error>, + F: FnOnce(&mut Transaction<Patch, Repo>) -> Result<(), store::Error>, { let mut tx = Transaction::default(); operations(&mut tx)?; - let (patch, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?; + let (patch, commit) = tx.commit(message, self.id, &mut self.store.raw)?; self.cache .update(&self.store.as_ref().id(), &self.id, &patch) .map_err(|e| Error::CacheUpdate { @@ -2100,73 +2104,46 @@ where } /// Edit patch metadata. - pub fn edit<G, S>( - &mut self, - title: cob::Title, - target: MergeTarget, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - S: ToString, - { - self.transaction("Edit", signer, |tx| tx.edit(title, target)) + pub fn edit(&mut self, title: cob::Title, target: MergeTarget) -> Result<EntryId, Error> { + self.transaction("Edit", |tx| tx.edit(title, target)) } /// Edit revision metadata. - pub fn edit_revision<G, S>( + pub fn edit_revision( &mut self, revision: RevisionId, - description: S, + description: impl ToString, embeds: impl IntoIterator<Item = Embed<Uri>>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - S: ToString, - { - self.transaction("Edit revision", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("Edit revision", |tx| { tx.edit_revision(revision, description, embeds.into_iter().collect()) }) } /// Redact a revision. - pub fn redact<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Redact revision", signer, |tx| tx.redact(revision)) + pub fn redact(&mut self, revision: RevisionId) -> Result<EntryId, Error> { + self.transaction("Redact revision", |tx| tx.redact(revision)) } /// Create a thread on a patch revision. - pub fn thread<G, S>( + pub fn thread( &mut self, revision: RevisionId, - body: S, - signer: &Device<G>, - ) -> Result<CommentId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - S: ToString, - { - self.transaction("Create thread", signer, |tx| tx.thread(revision, body)) + body: impl ToString, + ) -> Result<CommentId, Error> { + self.transaction("Create thread", |tx| tx.thread(revision, body)) } /// Comment on a patch revision. - pub fn comment<G, S>( + pub fn comment( &mut self, revision: RevisionId, - body: S, + body: impl ToString, reply_to: Option<CommentId>, location: Option<CodeLocation>, embeds: impl IntoIterator<Item = Embed<Uri>>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - S: ToString, - { - self.transaction("Comment", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("Comment", |tx| { tx.comment( revision, body, @@ -2178,87 +2155,61 @@ where } /// React on a patch revision. - pub fn react<G>( + pub fn react( &mut self, revision: RevisionId, reaction: Reaction, location: Option<CodeLocation>, active: bool, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("React", signer, |tx| { - tx.react(revision, reaction, location, active) - }) + ) -> Result<EntryId, Error> { + self.transaction("React", |tx| tx.react(revision, reaction, location, active)) } /// Edit a comment on a patch revision. - pub fn comment_edit<G, S>( + pub fn comment_edit( &mut self, revision: RevisionId, comment: CommentId, - body: S, + body: impl ToString, embeds: impl IntoIterator<Item = Embed<Uri>>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - S: ToString, - { - self.transaction("Edit comment", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("Edit comment", |tx| { tx.comment_edit(revision, comment, body, embeds.into_iter().collect()) }) } /// React to a comment on a patch revision. - pub fn comment_react<G>( + pub fn comment_react( &mut self, revision: RevisionId, comment: CommentId, reaction: Reaction, active: bool, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("React comment", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("React comment", |tx| { tx.comment_react(revision, comment, reaction, active) }) } /// Redact a comment on a patch revision. - pub fn comment_redact<G>( + pub fn comment_redact( &mut self, revision: RevisionId, comment: CommentId, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Redact comment", signer, |tx| { - tx.comment_redact(revision, comment) - }) + ) -> Result<EntryId, Error> { + self.transaction("Redact comment", |tx| tx.comment_redact(revision, comment)) } /// Comment on a line of code as part of a review. - pub fn review_comment<G, S>( + pub fn review_comment( &mut self, review: ReviewId, - body: S, + body: impl ToString, location: Option<CodeLocation>, reply_to: Option<CommentId>, embeds: impl IntoIterator<Item = Embed<Uri>>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - S: ToString, - { - self.transaction("Review comment", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("Review comment", |tx| { tx.review_comment( review, body, @@ -2270,164 +2221,118 @@ where } /// Edit review comment. - pub fn edit_review_comment<G, S>( + pub fn edit_review_comment( &mut self, review: ReviewId, comment: EntryId, - body: S, + body: impl ToString, embeds: impl IntoIterator<Item = Embed<Uri>>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - S: ToString, - { - self.transaction("Edit review comment", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("Edit review comment", |tx| { tx.edit_review_comment(review, comment, body, embeds.into_iter().collect()) }) } /// React to a review comment. - pub fn react_review_comment<G>( + pub fn react_review_comment( &mut self, review: ReviewId, comment: EntryId, reaction: Reaction, active: bool, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("React to review comment", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("React to review comment", |tx| { tx.react_review_comment(review, comment, reaction, active) }) } /// React to a review comment. - pub fn redact_review_comment<G>( + pub fn redact_review_comment( &mut self, review: ReviewId, comment: EntryId, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Redact review comment", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("Redact review comment", |tx| { tx.redact_review_comment(review, comment) }) } /// Review a patch revision. - pub fn review<G>( + pub fn review( &mut self, revision: RevisionId, verdict: Option<Verdict>, summary: Option<String>, labels: Vec<Label>, - signer: &Device<G>, - ) -> Result<ReviewId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { + ) -> Result<ReviewId, Error> { if verdict.is_none() && summary.is_none() { return Err(Error::EmptyReview); } - self.transaction("Review", signer, |tx| { - tx.review(revision, verdict, summary, labels) - }) - .map(ReviewId) + self.transaction("Review", |tx| tx.review(revision, verdict, summary, labels)) + .map(ReviewId) } /// Edit a review. - pub fn review_edit<G>( + pub fn review_edit( &mut self, review: ReviewId, verdict: Option<Verdict>, summary: String, labels: Vec<Label>, embeds: impl IntoIterator<Item = Embed<Uri>>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Edit review", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("Edit review", |tx| { tx.review_edit(review, verdict, summary, labels, embeds) }) } /// React to a review. - pub fn review_react<G>( + pub fn review_react( &mut self, review: ReviewId, reaction: Reaction, active: bool, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("React to review", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("React to review", |tx| { tx.review_react(review, reaction, active) }) } /// Redact a patch review. - pub fn redact_review<G>( - &mut self, - review: ReviewId, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Redact review", signer, |tx| tx.redact_review(review)) + pub fn redact_review(&mut self, review: ReviewId) -> Result<EntryId, Error> { + self.transaction("Redact review", |tx| tx.redact_review(review)) } /// Resolve a patch review comment. - pub fn resolve_review_comment<G>( + pub fn resolve_review_comment( &mut self, review: ReviewId, comment: CommentId, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Resolve review comment", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("Resolve review comment", |tx| { tx.review_comment_resolve(review, comment) }) } /// Unresolve a patch review comment. - pub fn unresolve_review_comment<G>( + pub fn unresolve_review_comment( &mut self, review: ReviewId, comment: CommentId, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Unresolve review comment", signer, |tx| { + ) -> Result<EntryId, Error> { + self.transaction("Unresolve review comment", |tx| { tx.review_comment_unresolve(review, comment) }) } /// Merge a patch revision. - pub fn merge<G>( + pub fn merge( &mut self, revision: RevisionId, commit: git::Oid, - signer: &Device<G>, - ) -> Result<Merged<'_, R>, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { + ) -> Result<Merged<'_, Repo>, Error> { // TODO: Don't allow merging the same revision twice? - let entry = self.transaction("Merge revision", signer, |tx| tx.merge(revision, commit))?; + let entry = self.transaction("Merge revision", |tx| tx.merge(revision, commit))?; Ok(Merged { entry, @@ -2437,108 +2342,73 @@ where } /// Update a patch with a new revision. - pub fn update<G>( + pub fn update( &mut self, description: impl ToString, base: impl Into<git::Oid>, oid: impl Into<git::Oid>, - signer: &Device<G>, - ) -> Result<RevisionId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Add revision", signer, |tx| { - tx.revision(description, base, oid) - }) - .map(RevisionId) + ) -> Result<RevisionId, Error> { + self.transaction("Add revision", |tx| tx.revision(description, base, oid)) + .map(RevisionId) } /// Lifecycle a patch. - pub fn lifecycle<G>(&mut self, state: Lifecycle, signer: &Device<G>) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Lifecycle", signer, |tx| tx.lifecycle(state)) + pub fn lifecycle(&mut self, state: Lifecycle) -> Result<EntryId, Error> { + self.transaction("Lifecycle", |tx| tx.lifecycle(state)) } /// Assign a patch. - pub fn assign<G>( - &mut self, - assignees: BTreeSet<Did>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Assign", signer, |tx| tx.assign(assignees)) + pub fn assign(&mut self, assignees: BTreeSet<Did>) -> Result<EntryId, Error> { + self.transaction("Assign", |tx| tx.assign(assignees)) } /// Archive a patch. - pub fn archive<G>(&mut self, signer: &Device<G>) -> Result<bool, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.lifecycle(Lifecycle::Archived, signer)?; + pub fn archive(&mut self) -> Result<bool, Error> { + self.lifecycle(Lifecycle::Archived)?; Ok(true) } /// Mark an archived patch as ready to be reviewed again. /// Returns `false` if the patch was not archived. - pub fn unarchive<G>(&mut self, signer: &Device<G>) -> Result<bool, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { + pub fn unarchive(&mut self) -> Result<bool, Error> { if !self.is_archived() { return Ok(false); } - self.lifecycle(Lifecycle::Open, signer)?; + self.lifecycle(Lifecycle::Open)?; Ok(true) } /// Mark a patch as ready to be reviewed. /// Returns `false` if the patch was not a draft. - pub fn ready<G>(&mut self, signer: &Device<G>) -> Result<bool, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { + pub fn ready(&mut self) -> Result<bool, Error> { if !self.is_draft() { return Ok(false); } - self.lifecycle(Lifecycle::Open, signer)?; + self.lifecycle(Lifecycle::Open)?; Ok(true) } /// Mark an open patch as a draft. /// Returns `false` if the patch was not open and free of merges. - pub fn unready<G>(&mut self, signer: &Device<G>) -> Result<bool, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { + pub fn unready(&mut self) -> Result<bool, Error> { if !matches!(self.state(), State::Open { conflicts } if conflicts.is_empty()) { return Ok(false); } - self.lifecycle(Lifecycle::Draft, signer)?; + self.lifecycle(Lifecycle::Draft)?; Ok(true) } /// Label a patch. - pub fn label<G>( - &mut self, - labels: impl IntoIterator<Item = Label>, - signer: &Device<G>, - ) -> Result<EntryId, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - self.transaction("Label", signer, |tx| tx.label(labels)) + pub fn label(&mut self, labels: impl IntoIterator<Item = Label>) -> Result<EntryId, Error> { + self.transaction("Label", |tx| tx.label(labels)) } } -impl<R, C> Deref for PatchMut<'_, '_, R, C> { +impl<Repo, Signer, Cache> Deref for PatchMut<'_, '_, '_, Repo, Signer, Cache> { type Target = Patch; fn deref(&self) -> &Self::Target { @@ -2574,39 +2444,46 @@ pub struct ByRevision { pub revision: Revision, } -pub struct Patches<'a, R> { - raw: store::Store<'a, Patch, R>, +pub struct Patches<'a, Repo, Access> { + raw: store::Store<'a, Patch, Repo, Access>, } -impl<'a, R> Deref for Patches<'a, R> { - type Target = store::Store<'a, Patch, R>; +impl<'a, Repo, Access> Deref for Patches<'a, Repo, Access> { + type Target = store::Store<'a, Patch, Repo, Access>; fn deref(&self) -> &Self::Target { &self.raw } } -impl<R> HasRepoId for Patches<'_, R> +impl<Repo, Access> HasRepoId for Patches<'_, Repo, Access> where - R: ReadRepository, + Repo: HasRepoId, { fn rid(&self) -> RepoId { - self.as_ref().id() + self.raw.rid() } } -impl<'a, R> Patches<'a, R> +impl<'a, Repo, Access> Patches<'a, Repo, Access> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, + Access: store::access::Access, { /// Open a patches store. - pub fn open(repository: &'a R) -> Result<Self, RepositoryError> { + pub fn open(repository: &'a Repo, access: Access) -> Result<Self, RepositoryError> { let identity = repository.identity_head()?; - let raw = store::Store::open(repository)?.identity(identity); + let raw = store::Store::open(repository, access)?.identity(identity); Ok(Self { raw }) } +} +impl<'a, Repo, Access> Patches<'a, Repo, Access> +where + Repo: ReadRepository + cob::Store<Namespace = NodeId>, + Access: store::access::Access, +{ /// Patches count by state. pub fn counts(&self) -> Result<PatchCounts, store::Error> { let all = self.all()?; @@ -2678,12 +2555,14 @@ where } } -impl<'a, R> Patches<'a, R> +impl<'a, 'b, Repo, Signer> Patches<'a, Repo, WriteAs<'b, Signer>> where - R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, { /// Open a new patch. - pub fn create<'g, C, G>( + pub fn create<'g, Cache>( &'g mut self, title: cob::Title, description: impl ToString, @@ -2691,12 +2570,13 @@ where base: impl Into<git::Oid>, oid: impl Into<git::Oid>, labels: &[Label], - cache: &'g mut C, - signer: &Device<G>, - ) -> Result<PatchMut<'a, 'g, R, C>, Error> + cache: &'g mut Cache, + ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, Error> where - C: cob::cache::Update<Patch>, - G: crypto::signature::Signer<crypto::Signature>, + Cache: cob::cache::Update<Patch>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { self._create( title, @@ -2707,12 +2587,11 @@ where labels, Lifecycle::default(), cache, - signer, ) } /// Draft a patch. This patch will be created in a [`State::Draft`] state. - pub fn draft<'g, C, G>( + pub fn draft<'g, Cache>( &'g mut self, title: cob::Title, description: impl ToString, @@ -2720,12 +2599,12 @@ where base: impl Into<git::Oid>, oid: impl Into<git::Oid>, labels: &[Label], - cache: &'g mut C, - signer: &Device<G>, - ) -> Result<PatchMut<'a, 'g, R, C>, Error> + cache: &'g mut Cache, + ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, Error> where - C: cob::cache::Update<Patch>, - G: crypto::signature::Signer<crypto::Signature>, + Cache: cob::cache::Update<Patch>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { self._create( title, @@ -2736,16 +2615,15 @@ where labels, Lifecycle::Draft, cache, - signer, ) } /// Get a patch mutably. - pub fn get_mut<'g, C>( + pub fn get_mut<'g, Cache>( &'g mut self, id: &ObjectId, - cache: &'g mut C, - ) -> Result<PatchMut<'a, 'g, R, C>, store::Error> { + cache: &'g mut Cache, + ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, store::Error> { let patch = self .raw .get(id)? @@ -2760,7 +2638,7 @@ where } /// Create a patch. This is an internal function used by `create` and `draft`. - fn _create<'g, C, G>( + fn _create<'g, Cache>( &'g mut self, title: cob::Title, description: impl ToString, @@ -2769,14 +2647,16 @@ where oid: impl Into<git::Oid>, labels: &[Label], state: Lifecycle, - cache: &'g mut C, - signer: &Device<G>, - ) -> Result<PatchMut<'a, 'g, R, C>, Error> + cache: &'g mut Cache, + ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, Cache>, Error> where - C: cob::cache::Update<Patch>, - G: crypto::signature::Signer<crypto::Signature>, + Cache: cob::cache::Update<Patch>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { - let (id, patch) = Transaction::initial("Create patch", &mut self.raw, signer, |tx, _| { + let (id, patch) = Transaction::initial("Create patch", &mut self.raw, |tx, _| { tx.revision(description, base, oid)?; tx.edit(title, target)?; @@ -3062,7 +2942,7 @@ mod test { let alice = test::setup::NodeWithRepo::default(); let checkout = alice.repo.checkout(); let branch = checkout.branch_with([("README", b"Hello World!")]); - let mut patches = Cache::no_cache(&*alice.repo).unwrap(); + let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap(); let author: Did = alice.signer.public_key().into(); let target = MergeTarget::Delegates; let patch = patches @@ -3073,7 +2953,6 @@ mod test { branch.base, branch.oid, &[], - &alice.signer, ) .unwrap(); @@ -3104,7 +2983,7 @@ mod test { let alice = test::setup::NodeWithRepo::default(); let checkout = alice.repo.checkout(); let branch = checkout.branch_with([("README", b"Hello World!")]); - let mut patches = Cache::no_cache(&*alice.repo).unwrap(); + let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap(); let patch = patches .create( cob::Title::new("My first patch").unwrap(), @@ -3113,7 +2992,6 @@ mod test { branch.base, branch.oid, &[], - &alice.signer, ) .unwrap(); @@ -3122,7 +3000,7 @@ mod test { let (revision_id, _) = patch.revisions().last().unwrap(); assert!( patch - .comment(revision_id, "patch comment", None, None, [], &alice.signer) + .comment(revision_id, "patch comment", None, None, [],) .is_ok(), "can comment on patch" ); @@ -3137,7 +3015,7 @@ mod test { let alice = test::setup::NodeWithRepo::default(); let checkout = alice.repo.checkout(); let branch = checkout.branch_with([("README", b"Hello World!")]); - let mut patches = Cache::no_cache(&*alice.repo).unwrap(); + let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap(); let mut patch = patches .create( cob::Title::new("My first patch").unwrap(), @@ -3146,13 +3024,12 @@ mod test { branch.base, branch.oid, &[], - &alice.signer, ) .unwrap(); let id = patch.id; let (rid, _) = patch.revisions().next().unwrap(); - let _merge = patch.merge(rid, branch.base, &alice.signer).unwrap(); + let _merge = patch.merge(rid, branch.base).unwrap(); let patch = patches.get(&id).unwrap().unwrap(); let merges = patch.merges.iter().collect::<Vec<_>>(); @@ -3168,7 +3045,7 @@ mod test { let alice = test::setup::NodeWithRepo::default(); let checkout = alice.repo.checkout(); let branch = checkout.branch_with([("README", b"Hello World!")]); - let mut patches = Cache::no_cache(&*alice.repo).unwrap(); + let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap(); let mut patch = patches .create( cob::Title::new("My first patch").unwrap(), @@ -3177,7 +3054,6 @@ mod test { branch.base, branch.oid, &[], - &alice.signer, ) .unwrap(); @@ -3188,7 +3064,6 @@ mod test { Some(Verdict::Accept), Some("LGTM".to_owned()), vec![], - &alice.signer, ) .unwrap(); @@ -3201,17 +3076,17 @@ mod test { assert_eq!(review.verdict(), Some(Verdict::Accept)); assert_eq!(review.summary(), "LGTM"); - patch.redact_review(review_id, &alice.signer).unwrap(); + patch.redact_review(review_id).unwrap(); patch.reload().unwrap(); let (_, revision) = patch.latest(); assert_eq!(revision.reviews().count(), 0); // This is fine, redacting an already-redacted review is a no-op. - patch.redact_review(review_id, &alice.signer).unwrap(); + patch.redact_review(review_id).unwrap(); // If the review never existed, it's an error. patch - .redact_review(ReviewId(arbitrary::entry_id()), &alice.signer) + .redact_review(ReviewId(arbitrary::entry_id())) .unwrap_err(); } @@ -3220,7 +3095,7 @@ mod test { let alice = test::setup::NodeWithRepo::default(); let checkout = alice.repo.checkout(); let branch = checkout.branch_with([("README", b"Hello World!")]); - let mut patches = Cache::no_cache(&*alice.repo).unwrap(); + let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap(); let mut patch = patches .create( cob::Title::new("My first patch").unwrap(), @@ -3229,21 +3104,20 @@ mod test { branch.base, branch.oid, &[], - &alice.signer, ) .unwrap(); let update = checkout.branch_with([("README", b"Hello Radicle!")]); let updated = patch - .update("I've made changes.", branch.base, update.oid, &alice.signer) + .update("I've made changes.", branch.base, update.oid) .unwrap(); // It's fine to redact a review from a redacted revision. let review = patch - .review(updated, Some(Verdict::Accept), None, vec![], &alice.signer) + .review(updated, Some(Verdict::Accept), None, vec![]) .unwrap(); - patch.redact(updated, &alice.signer).unwrap(); - patch.redact_review(review, &alice.signer).unwrap(); + patch.redact(updated).unwrap(); + patch.redact_review(review).unwrap(); } #[test] @@ -3408,7 +3282,7 @@ mod test { let alice = test::setup::NodeWithRepo::default(); let checkout = alice.repo.checkout(); let branch = checkout.branch_with([("README", b"Hello World!")]); - let mut patches = Cache::no_cache(&*alice.repo).unwrap(); + let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap(); let mut patch = patches .create( cob::Title::new("My first patch").unwrap(), @@ -3417,19 +3291,12 @@ mod test { branch.base, branch.oid, &[], - &alice.signer, ) .unwrap(); let (rid, _) = patch.latest(); let review = patch - .review( - rid, - Some(Verdict::Accept), - Some("LGTM".to_owned()), - vec![], - &alice.signer, - ) + .review(rid, Some(Verdict::Accept), Some("LGTM".to_owned()), vec![]) .unwrap(); patch .review_edit( @@ -3438,7 +3305,6 @@ mod test { "Whoops!".to_owned(), vec![], vec![], - &alice.signer, ) .unwrap(); // Overwrite the comment. @@ -3453,7 +3319,7 @@ mod test { let alice = test::setup::NodeWithRepo::default(); let checkout = alice.repo.checkout(); let branch = checkout.branch_with([("README", b"Hello World!")]); - let mut patches = Cache::no_cache(&*alice.repo).unwrap(); + let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap(); let mut patch = patches .create( cob::Title::new("My first patch").unwrap(), @@ -3462,16 +3328,15 @@ mod test { branch.base, branch.oid, &[], - &alice.signer, ) .unwrap(); let (rid, _) = patch.latest(); patch - .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer) + .review(rid, Some(Verdict::Accept), None, vec![]) .unwrap(); patch - .review(rid, Some(Verdict::Reject), None, vec![], &alice.signer) + .review(rid, Some(Verdict::Reject), None, vec![]) .unwrap(); // This review is ignored, since there is already a review by this author. let (_, revision) = patch.latest(); @@ -3484,7 +3349,7 @@ mod test { let alice = test::setup::NodeWithRepo::default(); let checkout = alice.repo.checkout(); let branch = checkout.branch_with([("README", b"Hello World!")]); - let mut patches = Cache::no_cache(&*alice.repo).unwrap(); + let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap(); let mut patch = patches .create( cob::Title::new("My first patch").unwrap(), @@ -3493,16 +3358,15 @@ mod test { branch.base, branch.oid, &[], - &alice.signer, ) .unwrap(); let (rid, _) = patch.latest(); let review = patch - .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer) + .review(rid, Some(Verdict::Accept), None, vec![]) .unwrap(); patch - .review_comment(review, "First comment!", None, None, [], &alice.signer) + .review_comment(review, "First comment!", None, None, []) .unwrap(); let _review = patch @@ -3512,11 +3376,10 @@ mod test { "".to_string(), vec![], vec![], - &alice.signer, ) .unwrap(); patch - .review_comment(review, "Second comment!", None, None, [], &alice.signer) + .review_comment(review, "Second comment!", None, None, []) .unwrap(); let (_, revision) = patch.latest(); @@ -3535,7 +3398,7 @@ mod test { let alice = test::setup::NodeWithRepo::default(); let checkout = alice.repo.checkout(); let branch = checkout.branch_with([("README", b"Hello World!")]); - let mut patches = Cache::no_cache(&*alice.repo).unwrap(); + let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap(); let mut patch = patches .create( cob::Title::new("My first patch").unwrap(), @@ -3544,7 +3407,6 @@ mod test { branch.base, branch.oid, &[], - &alice.signer, ) .unwrap(); @@ -3556,7 +3418,7 @@ mod test { new: Some(CodeRange::Lines { range: 5..8 }), }; let review = patch - .review(rid, Some(Verdict::Accept), None, vec![], &alice.signer) + .review(rid, Some(Verdict::Accept), None, vec![]) .unwrap(); patch .review_comment( @@ -3565,7 +3427,6 @@ mod test { Some(location.clone()), None, [], - &alice.signer, ) .unwrap(); @@ -3582,7 +3443,7 @@ mod test { let alice = test::setup::NodeWithRepo::default(); let checkout = alice.repo.checkout(); let branch = checkout.branch_with([("README", b"Hello World!")]); - let mut patches = Cache::no_cache(&*alice.repo).unwrap(); + let mut patches = Cache::no_cache(&*alice.repo, &alice.signer).unwrap(); let mut patch = patches .create( cob::Title::new("My first patch").unwrap(), @@ -3591,19 +3452,12 @@ mod test { branch.base, branch.oid, &[], - &alice.signer, ) .unwrap(); let (rid, _) = patch.latest(); let review = patch - .review( - rid, - Some(Verdict::Accept), - Some("Nah".to_owned()), - vec![], - &alice.signer, - ) + .review(rid, Some(Verdict::Accept), Some("Nah".to_owned()), vec![]) .unwrap(); patch .review_edit( @@ -3612,7 +3466,6 @@ mod test { "".to_string(), vec![], vec![], - &alice.signer, ) .unwrap(); @@ -3633,7 +3486,8 @@ mod test { let mut patches = { let path = alice.tmp.path().join("cobs.db"); let mut db = cob::cache::Store::open(path).unwrap(); - let store = cob::patch::Patches::open(&*alice.repo).unwrap(); + let store = + cob::patch::Patches::open(&*alice.repo, WriteAs::new(&alice.signer)).unwrap(); db.migrate(migrate::ignore).unwrap(); cob::patch::Cache::open(store, db) @@ -3646,7 +3500,6 @@ mod test { branch.base, branch.oid, &[], - &alice.signer, ) .unwrap(); @@ -3655,7 +3508,7 @@ mod test { let update = checkout.branch_with([("README", b"Hello Radicle!")]); let _ = patch - .update("I've made changes.", branch.base, update.oid, &alice.signer) + .update("I've made changes.", branch.base, update.oid) .unwrap(); let id = patch.id; @@ -3686,7 +3539,7 @@ mod test { let branch = repo .checkout() .branch_with([("README.md", b"Hello, World!")]); - let mut patches = Cache::no_cache(&*repo).unwrap(); + let mut patches = Cache::no_cache(&*repo, &alice.signer).unwrap(); let mut patch = patches .create( cob::Title::new("My first patch").unwrap(), @@ -3695,7 +3548,6 @@ mod test { branch.base, branch.oid, &[], - &alice.signer, ) .unwrap(); let patch_id = patch.id; @@ -3704,17 +3556,17 @@ mod test { .checkout() .branch_with([("README.md", b"Hello, Radicle!")]); let revision_id = patch - .update("I've made changes.", branch.base, update.oid, &alice.signer) + .update("I've made changes.", branch.base, update.oid) .unwrap(); assert_eq!(patch.revisions().count(), 2); - patch.redact(revision_id, &alice.signer).unwrap(); + patch.redact(revision_id).unwrap(); assert_eq!(patch.latest().0, RevisionId(*patch_id)); assert_eq!(patch.revisions().count(), 1); // The patch's root must always exist. assert_eq!(patch.latest(), patch.root()); - assert!(patch.redact(patch.latest().0, &alice.signer).is_err()); + assert!(patch.redact(patch.latest().0).is_err()); } #[test] diff --git a/crates/radicle/src/cob/patch/cache.rs b/crates/radicle/src/cob/patch/cache.rs index 0f17c12d..58532e32 100644 --- a/crates/radicle/src/cob/patch/cache.rs +++ b/crates/radicle/src/cob/patch/cache.rs @@ -8,9 +8,9 @@ use crate::cob; use crate::cob::cache::{self, StoreReader}; use crate::cob::cache::{Remove, StoreWriter, Update}; use crate::cob::store; +use crate::cob::store::access::{ReadOnly, WriteAs}; use crate::cob::{Label, ObjectId, TypeName}; use crate::git; -use crate::node::device::Device; use crate::prelude::RepoId; use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository}; @@ -87,28 +87,36 @@ impl<T> PatchesMut for T where T: Patches + Update<Patch> + Remove<Patch> {} /// The `store` is used for the main storage when performing a /// write-through. It is also used for identifying which `RepoId` is /// being used for the `cache`. -pub struct Cache<R, C> { - pub(super) store: R, +pub struct Cache<'a, Repo, Access, C> { + pub(super) store: super::Patches<'a, Repo, Access>, pub(super) cache: C, } -impl<R, C> Cache<R, C> { - pub fn new(store: R, cache: C) -> Self { +impl<'a, Repo, Access, C> Cache<'a, Repo, Access, C> { + pub fn new(store: super::Patches<'a, Repo, Access>, cache: C) -> Self { Self { store, cache } } +} - pub fn rid(&self) -> RepoId - where - R: HasRepoId, - { +impl<'a, Repo, Access, C> HasRepoId for Cache<'a, Repo, Access, C> +where + Repo: HasRepoId, +{ + fn rid(&self) -> RepoId { self.store.rid() } } -impl<'a, R, C> Cache<super::Patches<'a, R>, C> { +impl<'a, 'b, Repo, Signer, C> Cache<'a, Repo, WriteAs<'b, Signer>, C> +where + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, +{ /// Create a new [`Patch`] using the [`super::Patches`] as the /// main storage, and writing the update to the `cache`. - pub fn create<'g, G>( + pub fn create<'g>( &'g mut self, title: cob::Title, description: impl ToString, @@ -116,11 +124,9 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> { base: impl Into<git::Oid>, oid: impl Into<git::Oid>, labels: &[Label], - signer: &Device<G>, - ) -> Result<PatchMut<'a, 'g, R, C>, super::Error> + ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, C>, super::Error> where - R: WriteRepository + cob::Store<Namespace = NodeId>, - G: crypto::signature::Signer<crypto::Signature>, + Repo: WriteRepository + cob::Store<Namespace = NodeId>, C: Update<Patch>, { self.store.create( @@ -131,14 +137,13 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> { oid, labels, &mut self.cache, - signer, ) } /// Create a new [`Patch`], in a draft state, using the /// [`super::Patches`] as the main storage, and writing the update /// to the `cache`. - pub fn draft<'g, G>( + pub fn draft<'g>( &'g mut self, title: cob::Title, description: impl ToString, @@ -146,11 +151,9 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> { base: impl Into<git::Oid>, oid: impl Into<git::Oid>, labels: &[Label], - signer: &Device<G>, - ) -> Result<PatchMut<'a, 'g, R, C>, super::Error> + ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, C>, super::Error> where - R: WriteRepository + cob::Store<Namespace = NodeId>, - G: crypto::signature::Signer<crypto::Signature>, + Repo: WriteRepository + cob::Store<Namespace = NodeId>, C: Update<Patch>, { self.store.draft( @@ -161,19 +164,17 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> { oid, labels, &mut self.cache, - signer, ) } /// Remove the given `id` from the [`super::Patches`] storage, and /// removing the entry from the `cache`. - pub fn remove<G>(&mut self, id: &PatchId, signer: &Device<G>) -> Result<(), super::Error> + pub fn remove(&mut self, id: &PatchId) -> Result<(), super::Error> where - G: crypto::signature::Signer<crypto::Signature>, - R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, C: Remove<Patch>, { - self.store.remove(id, signer)?; + self.store.raw.remove(id)?; self.cache .remove(id) .map_err(|e| super::Error::CacheRemove { @@ -182,12 +183,17 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> { })?; Ok(()) } +} +impl<'a, Repo, Access, C> Cache<'a, Repo, Access, C> +where + Access: cob::store::access::Access, +{ /// Read the given `id` from the [`super::Patches`] store and /// writing it to the `cache`. pub fn write(&mut self, id: &PatchId) -> Result<(), super::Error> where - R: ReadRepository + cob::Store, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, C: Update<Patch>, { let issue = self @@ -213,7 +219,7 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> { callback: impl Fn(&Result<(PatchId, Patch), store::Error>, &cache::Progress) -> ControlFlow<()>, ) -> Result<(), super::Error> where - R: ReadRepository + cob::Store, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, C: Update<Patch> + Remove<Patch>, { // Start by clearing the cache. This will get rid of patches that are cached but @@ -240,28 +246,28 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> { } } -impl<R> Cache<R, StoreReader> { - pub fn reader(store: R, cache: StoreReader) -> Self { +impl<'a, Repo> Cache<'a, Repo, ReadOnly, StoreReader> { + pub fn reader(store: super::Patches<'a, Repo, ReadOnly>, cache: StoreReader) -> Self { Self { store, cache } } } -impl<R> Cache<R, StoreWriter> { - pub fn open(store: R, cache: StoreWriter) -> Self { +impl<'a, Repo, Access> Cache<'a, Repo, Access, StoreWriter> { + pub fn open(store: super::Patches<'a, Repo, Access>, cache: StoreWriter) -> Self { Self { store, cache } } } -impl<'a, R> Cache<super::Patches<'a, R>, StoreWriter> +impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, StoreWriter> where - R: ReadRepository + cob::Store, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, { /// Get the [`PatchMut`], identified by `id`, using the /// `StoreWriter` for retrieving the `Patch`. pub fn get_mut<'g>( &'g mut self, id: &ObjectId, - ) -> Result<PatchMut<'a, 'g, R, StoreWriter>, Error> { + ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, StoreWriter>, Error> { let patch = Patches::get(self, id)? .ok_or_else(move || Error::NotFound(super::TYPENAME.clone(), *id))?; @@ -274,14 +280,14 @@ where } } -impl<'a, R> Cache<super::Patches<'a, R>, cache::NoCache> +impl<'a, 'b, Repo, Signer> Cache<'a, Repo, WriteAs<'b, Signer>, cache::NoCache> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, { /// Get a `Cache` that does no write-through modifications and /// uses the [`super::Patches`] store for all reads and writes. - pub fn no_cache(repository: &'a R) -> Result<Self, RepositoryError> { - let store = super::Patches::open(repository)?; + pub fn no_cache(repository: &'a Repo, signer: &'b Signer) -> Result<Self, RepositoryError> { + let store = super::Patches::open(repository, WriteAs::new(signer))?; Ok(Self { store, cache: cache::NoCache, @@ -292,7 +298,7 @@ where pub fn get_mut<'g>( &'g mut self, id: &ObjectId, - ) -> Result<PatchMut<'a, 'g, R, cache::NoCache>, super::Error> { + ) -> Result<PatchMut<'a, 'b, 'g, Repo, Signer, cache::NoCache>, super::Error> { let patch = self .store .get(id)? @@ -307,7 +313,7 @@ where } } -impl<R, C> cache::Update<Patch> for Cache<R, C> +impl<'a, Repo, Access, C> cache::Update<Patch> for Cache<'a, Repo, Access, C> where C: cache::Update<Patch>, { @@ -324,7 +330,7 @@ where } } -impl<R, C> cache::Remove<Patch> for Cache<R, C> +impl<'a, Repo, Access, C> cache::Remove<Patch> for Cache<'a, Repo, Access, C> where C: cache::Remove<Patch>, { @@ -443,9 +449,9 @@ impl Iterator for PatchesIter<'_> { } } -impl<R> Patches for Cache<R, StoreReader> +impl<'a, Repo, Access> Patches for Cache<'a, Repo, Access, StoreReader> where - R: HasRepoId, + Repo: HasRepoId, { type Error = Error; type Iter<'b> @@ -486,9 +492,10 @@ impl Iterator for NoCacheIter<'_> { } } -impl<R> Patches for Cache<super::Patches<'_, R>, cache::NoCache> +impl<'a, Repo, Access> Patches for Cache<'a, Repo, Access, cache::NoCache> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, + Access: store::access::Access, { type Error = super::Error; type Iter<'b> @@ -535,9 +542,10 @@ where } } -impl<R> Patches for Cache<R, StoreWriter> +impl<'a, Repo, Access> Patches for Cache<'a, Repo, Access, StoreWriter> where - R: HasRepoId, + Repo: HasRepoId + cob::Store<Namespace = NodeId>, + Access: store::access::Access, { type Error = Error; type Iter<'b> @@ -711,6 +719,7 @@ mod tests { use radicle_cob::ObjectId; use crate::cob::cache::{Store, Update, Write}; + use crate::cob::store::access::ReadOnly; use crate::cob::thread::{Comment, Thread}; use crate::cob::{Author, Title, migrate}; use crate::patch::{ @@ -718,12 +727,14 @@ mod tests { }; use crate::prelude::Did; use crate::profile::env; + use crate::storage::HasRepoId as _; use crate::test::arbitrary; use crate::test::storage::MockRepository; use super::{Cache, Patches}; - fn memory(store: MockRepository) -> Cache<MockRepository, Store<Write>> { + fn memory<'a>(store: &'a MockRepository) -> Cache<'a, MockRepository, ReadOnly, Store<Write>> { + let store = super::super::Patches::open(store, ReadOnly).unwrap(); let cache = Store::<Write>::memory() .unwrap() .with_migrations(migrate::ignore) @@ -764,7 +775,7 @@ mod tests { #[test] fn test_is_empty() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); assert!(cache.is_empty().unwrap()); let patch = Patch::new( @@ -792,7 +803,7 @@ mod tests { #[test] fn test_counts() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); let n_open = arbitrary::r#gen::<u8>(0); let n_draft = arbitrary::r#gen::<u8>(1); let n_archived = arbitrary::r#gen::<u8>(1); @@ -880,7 +891,7 @@ mod tests { #[test] fn test_get() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); let ids = (0..arbitrary::r#gen::<u8>(1)) .map(|_| PatchId::from(arbitrary::oid())) .collect::<BTreeSet<PatchId>>(); @@ -916,7 +927,7 @@ mod tests { #[test] fn test_find_by_revision() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); let patch_id = PatchId::from(arbitrary::oid()); let revisions = (0..arbitrary::r#gen::<NonZeroU8>(1).into()) .map(|_| revision()) @@ -958,7 +969,7 @@ mod tests { #[test] fn test_list() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); let ids = (0..arbitrary::r#gen::<u8>(1)) .map(|_| PatchId::from(arbitrary::oid())) .collect::<BTreeSet<PatchId>>(); @@ -989,7 +1000,7 @@ mod tests { #[test] fn test_list_by_status() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); let ids = (0..arbitrary::r#gen::<u8>(1)) .map(|_| PatchId::from(arbitrary::oid())) .collect::<BTreeSet<PatchId>>(); @@ -1020,7 +1031,7 @@ mod tests { #[test] fn test_remove() { let repo = arbitrary::r#gen::<MockRepository>(1); - let mut cache = memory(repo); + let mut cache = memory(&repo); let ids = (0..arbitrary::r#gen::<u8>(1)) .map(|_| PatchId::from(arbitrary::oid())) .collect::<BTreeSet<PatchId>>(); diff --git a/crates/radicle/src/cob/store.rs b/crates/radicle/src/cob/store.rs index 88653069..06f7d677 100644 --- a/crates/radicle/src/cob/store.rs +++ b/crates/radicle/src/cob/store.rs @@ -11,12 +11,14 @@ use serde::{Deserialize, Serialize}; use crate::cob::op::Op; use crate::cob::{Create, Embed, EntryId, ObjectId, TypeName, Update, Updated, Uri, Version}; use crate::git; -use crate::node::device::Device; use crate::prelude::*; -use crate::storage::SignRepository; use crate::storage::git as storage; +use crate::storage::{HasRepoId, SignRepository}; use crate::{cob, identity}; +pub mod access; +use access::WriteAs; + pub trait CobAction { /// Parent objects this action depends on. For example, patch revisions /// have the commit objects as their parent. @@ -135,94 +137,147 @@ pub enum Error { ClashingIdentifiers(String, String), } -/// Storage for collaborative objects of a specific type `T` in a single repository. -pub struct Store<'a, T, R> { - identity: Option<git::Oid>, - repo: &'a R, +/// The [`Scope`] of a [`Store`] keeps track of what collaborative object is +/// being accessed. +/// +/// For example, a `Scope<Patch>` keeps track of `Patch` access. +/// +/// The type parameter `T` is the collaborative object type. +pub struct Scope<'a, T> { witness: PhantomData<T>, type_name: &'a TypeName, } -impl<T, R> AsRef<R> for Store<'_, T, R> { - fn as_ref(&self) -> &R { - self.repo - } -} - -impl<'a, T, R> Store<'a, T, R> -where - R: ReadRepository + cob::Store, -{ - /// Open a new generic store. - pub fn open_for(type_name: &'a TypeName, repo: &'a R) -> Result<Self, Error> { - Ok(Self { - repo, - identity: None, - witness: PhantomData, - type_name, - }) - } - - /// Return a new store with the attached identity. - pub fn identity(self, identity: git::Oid) -> Self { +impl<'a, T: CobWithType> Scope<'a, T> { + fn new() -> Self { Self { - repo: self.repo, - witness: self.witness, - identity: Some(identity), - type_name: self.type_name, + witness: PhantomData, + type_name: T::type_name(), } } } -impl<'a, T, R> Store<'a, T, R> +/// Storage for collaborative objects of a specific type `T` in a single repository. +pub struct Store<'a, T, Repo, Access> { + identity: Option<git::Oid>, + repo: &'a Repo, + scope: Scope<'a, T>, + access: Access, +} + +impl<T, Repo, Access> HasRepoId for Store<'_, T, Repo, Access> where - R: ReadRepository + cob::Store<Namespace = NodeId>, - T: CobWithType, + Repo: HasRepoId, { - /// Open a new generic store. - pub fn open(repo: &'a R) -> Result<Self, Error> { + fn rid(&self) -> RepoId { + self.repo.rid() + } +} + +impl<T, Repo, Access> AsRef<Repo> for Store<'_, T, Repo, Access> { + fn as_ref(&self) -> &Repo { + self.repo + } +} + +impl<'a, T, Repo, Access> Store<'a, T, Repo, Access> +where + Repo: ReadRepository + cob::Store<Namespace = NodeId>, + Access: access::Access, +{ + pub fn open_for( + type_name: &'a TypeName, + repo: &'a Repo, + access: Access, + ) -> Result<Self, Error> { Ok(Self { repo, identity: None, - witness: PhantomData, - type_name: T::type_name(), + scope: Scope { + witness: PhantomData, + type_name, + }, + access, }) } } -impl<T, R> Store<'_, T, R> +impl<'a, T, Repo, Access> Store<'a, T, Repo, Access> where - R: ReadRepository + cob::Store<Namespace = NodeId>, - T: Cob + cob::Evaluate<R>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, + Access: access::Access, { + /// Return a new store with the attached identity. + pub fn identity(self, identity: git::Oid) -> Self { + Self { + repo: self.repo, + identity: Some(identity), + scope: self.scope, + access: self.access, + } + } +} + +impl<'a, T, Repo, Access> Store<'a, T, Repo, Access> +where + T: CobWithType, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, + Access: access::Access, +{ + /// Open a new generic store. + pub fn open(repo: &'a Repo, access: Access) -> Result<Self, Error> { + Ok(Self { + repo, + identity: None, + scope: Scope::new(), + access, + }) + } +} + +impl<'a, 'b, T, Repo, Signer> Store<'a, T, Repo, WriteAs<'b, Signer>> +where + T: Cob + cob::Evaluate<Repo>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, +{ + #[deprecated(note = "only exists to accommodate signatures in crate::cob::identity")] + pub(super) fn signer(&self) -> &Signer { + self.access.signer + } + + #[deprecated(note = "only exists to accommodate signatures in crate::cob::identity")] + pub(super) fn repo(&self) -> &Repo { + self.repo + } + pub fn transaction( &self, actions: Vec<T::Action>, embeds: Vec<Embed<Uri>>, - ) -> Transaction<T, R> { - Transaction::new(self.type_name.clone(), actions, embeds) + ) -> Transaction<T, Repo> { + Transaction::new(self.scope.type_name.clone(), actions, embeds) } } -impl<T, R> Store<'_, T, R> +impl<'a, 'b, T, Repo, Signer> Store<'a, T, Repo, WriteAs<'b, Signer>> where - R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, - T: Cob + cob::Evaluate<R>, + T: Cob + cob::Evaluate<Repo>, T::Action: Serialize, + Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { /// Update an object. - pub fn update<G>( - &self, + pub fn update( + &mut self, type_name: &TypeName, object_id: ObjectId, message: &str, actions: impl Into<NonEmpty<T::Action>>, embeds: Vec<Embed<Uri>>, - signer: &Device<G>, - ) -> Result<Updated<T>, Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { + ) -> 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)?; @@ -235,12 +290,13 @@ where }) }) .collect::<Result<_, _>>()?; + let namespace = self.access.signer.verifying_key(); let updated = cob::update( self.repo, - signer, + self.access.signer, self.identity, related, - signer.public_key(), + &namespace, Update { object_id, type_name: type_name.clone(), @@ -250,23 +306,19 @@ where }, )?; self.repo - .sign_refs(signer) + .sign_refs(self.access.signer) .map_err(|e| Error::SignRefs(Box::new(e)))?; Ok(updated) } /// Create an object. - pub fn create<G>( - &self, + pub fn create( + &mut self, message: &str, actions: impl Into<NonEmpty<T::Action>>, embeds: Vec<Embed<Uri>>, - signer: &Device<G>, - ) -> Result<(ObjectId, T), Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { + ) -> 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)?; @@ -279,14 +331,15 @@ where }) }) .collect::<Result<_, _>>()?; + let namespace = self.access.signer.verifying_key(); let cob = cob::create::<T, _, _>( self.repo, - signer, + self.access.signer, self.identity, parents, - signer.public_key(), + &namespace, Create { - type_name: self.type_name.clone(), + type_name: self.scope.type_name.clone(), version: Version::default(), message: message.to_owned(), embeds, @@ -296,28 +349,23 @@ where // Nb. We can't sign our refs before the identity refs exist, which are created after // the identity COB is created. Therefore we manually sign refs when creating identity // COBs. - if self.type_name != &*crate::cob::identity::TYPENAME { + if self.scope.type_name != &*crate::cob::identity::TYPENAME { self.repo - .sign_refs(signer) + .sign_refs(self.access.signer) .map_err(|e| Error::SignRefs(Box::new(e)))?; } Ok((*cob.id(), cob.object)) } /// Remove an object. - pub fn remove<G>(&self, id: &ObjectId, signer: &Device<G>) -> Result<(), Error> - where - G: crypto::signature::Signer<crypto::Signature>, - { - let name = git::refs::storage::cob(signer.public_key(), self.type_name, id); - match self - .repo - .reference_oid(signer.public_key(), &name.strip_namespace()) - { + pub fn remove(&mut self, id: &ObjectId) -> Result<(), Error> { + let namespace = self.access.signer.verifying_key(); + let name = git::refs::storage::cob(&namespace, self.scope.type_name, id); + match self.repo.reference_oid(&namespace, &name.strip_namespace()) { Ok(_) => { - cob::remove(self.repo, signer.public_key(), self.type_name, id)?; + cob::remove(self.repo, &namespace, self.scope.type_name, id)?; self.repo - .sign_refs(signer) + .sign_refs(self.access.signer) .map_err(|e| Error::SignRefs(Box::new(e)))?; Ok(()) } @@ -330,15 +378,16 @@ where } } -impl<'a, T, R> Store<'a, T, R> +impl<'a, T, Repo, Access> Store<'a, T, Repo, Access> where - R: ReadRepository + cob::Store, - T: Cob + cob::Evaluate<R> + 'a, + T: Cob + cob::Evaluate<Repo> + 'a, T::Action: Serialize, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, + Access: access::Access, { /// Get an object. pub fn get(&self, id: &ObjectId) -> Result<Option<T>, Error> { - cob::get::<T, _>(self.repo, self.type_name, id) + cob::get::<T, _>(self.repo, self.scope.type_name, id) .map(|r| r.map(|cob| cob.object)) .map_err(Error::from) } @@ -346,9 +395,11 @@ where /// Return all objects. pub fn all( &self, - ) -> Result<impl ExactSizeIterator<Item = Result<(ObjectId, T), Error>> + use<'a, T, R>, Error> - { - let raw = cob::list::<T, _>(self.repo, self.type_name)?; + ) -> Result< + impl ExactSizeIterator<Item = Result<(ObjectId, T), Error>> + use<'a, T, Repo, Access>, + Error, + > { + let raw = cob::list::<T, _>(self.repo, self.scope.type_name)?; Ok(raw.into_iter().map(|o| Ok((*o.id(), o.object)))) } @@ -360,7 +411,7 @@ where /// Return objects count. pub fn count(&self) -> Result<usize, Error> { - let raw = cob::list::<T, _>(self.repo, self.type_name)?; + let raw = cob::list::<T, _>(self.repo, self.scope.type_name)?; Ok(raw.len()) } @@ -408,26 +459,33 @@ where type_name, } } + + #[deprecated(note = "only exists to accommodate signatures in crate::cob::identity")] + pub(super) fn into_inner(self) -> (Vec<T::Action>, Vec<Embed<Uri>>) { + (self.actions, self.embeds) + } } -impl<T, R> Transaction<T, R> +impl<T, Repo> Transaction<T, Repo> where - T: Cob + CobWithType + cob::Evaluate<R>, + T: Cob + CobWithType + cob::Evaluate<Repo>, { /// Create a new transaction to be used as the initial set of operations for a COB. - pub fn initial<G, F, Tx>( + pub fn initial<'a, 'b, Signer, Tx, F>( message: &str, - store: &mut Store<T, R>, - signer: &Device<G>, + store: &mut Store<'a, T, Repo, WriteAs<'b, Signer>>, operations: F, ) -> Result<(ObjectId, T), Error> where + T::Action: Serialize + Clone, + Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, Tx: From<Self>, Self: From<Tx>, - G: crypto::signature::Signer<crypto::Signature>, - F: FnOnce(&mut Tx, &R) -> Result<(), Error>, - R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, - T::Action: Serialize + Clone, + F: FnOnce(&mut Tx, &Repo) -> Result<(), Error>, { let mut tx = Tx::from(Transaction::default()); operations(&mut tx, store.as_ref())?; @@ -436,13 +494,13 @@ where let actions = NonEmpty::from_vec(tx.actions) .expect("Transaction::initial: transaction must contain at least one action"); - store.create(message, actions, tx.embeds, signer) + store.create(message, actions, tx.embeds) } } -impl<T, R> Transaction<T, R> +impl<T, Repo> Transaction<T, Repo> where - T: Cob + cob::Evaluate<R>, + T: Cob + cob::Evaluate<Repo>, { /// Add an action to this transaction. pub fn push(&mut self, action: T::Action) -> Result<(), Error> { @@ -482,25 +540,28 @@ where /// Commit transaction. /// /// Returns an operation that can be applied onto an in-memory state. - pub fn commit<G>( + pub fn commit<'a, Signer>( self, msg: &str, id: ObjectId, - store: &mut Store<T, R>, - signer: &Device<G>, + store: &mut Store<T, Repo, WriteAs<'a, Signer>>, ) -> Result<(T, EntryId), Error> where - R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, T::Action: Serialize + Clone, - G: crypto::signature::Signer<crypto::Signature>, + Repo: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>, + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Signer<crypto::ssh::ExtendedSignature>, + Signer: crypto::signature::Verifier<crypto::Signature>, { let actions = NonEmpty::from_vec(self.actions) .expect("Transaction::commit: transaction must not be empty"); + let Updated { head, object: CollaborativeObject { object, .. }, .. - } = store.update(&self.type_name, id, msg, actions, self.embeds, signer)?; + } = store.update(&self.type_name, id, msg, actions, self.embeds)?; Ok((object, head)) } diff --git a/crates/radicle/src/cob/store/access.rs b/crates/radicle/src/cob/store/access.rs new file mode 100644 index 00000000..afccd4ee --- /dev/null +++ b/crates/radicle/src/cob/store/access.rs @@ -0,0 +1,37 @@ +//! Stores carry [`Access`] to indicate allowed access modes. In particular +//! whether writes to the store are allowed. + +pub use seal::Access; + +/// [`ReadOnly`] is used for read-only [`Access`]. +pub struct ReadOnly; + +/// [`WriteAs`] is used for write [`Access`]. +pub struct WriteAs<'a, Signer> { + pub(super) signer: &'a Signer, +} + +impl<'a, Signer> WriteAs<'a, Signer> { + pub fn new(signer: &'a Signer) -> Self { + Self { signer } + } +} + +// See <https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/#sealing-traits-via-method-signatures>. +#[allow(private_interfaces)] +mod seal { + enum Seal {} + + /// Marker trait for COB store access modes. + pub trait Access { + fn seal(&self, _: Seal); + } + + impl Access for super::ReadOnly { + fn seal(&self, _: Seal) {} + } + + impl<Signer> Access for super::WriteAs<'_, Signer> { + fn seal(&self, _: Seal) {} + } +} diff --git a/crates/radicle/src/node/device.rs b/crates/radicle/src/node/device.rs index 3e2556ae..8daab7c4 100644 --- a/crates/radicle/src/node/device.rs +++ b/crates/radicle/src/node/device.rs @@ -204,3 +204,12 @@ impl AsRef<crypto::PublicKey> for BoxedDevice { impl KeypairRef for BoxedDevice { type VerifyingKey = crypto::PublicKey; } + +impl Verifier<Signature> for BoxedDevice { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), crypto::signature::Error> { + self.0 + .node + .verify(msg, signature) + .map_err(crypto::signature::Error::from_source) + } +} diff --git a/crates/radicle/src/profile.rs b/crates/radicle/src/profile.rs index 09cbd045..bf029b1d 100644 --- a/crates/radicle/src/profile.rs +++ b/crates/radicle/src/profile.rs @@ -22,6 +22,7 @@ use localtime::LocalTime; use thiserror::Error; use crate::cob::migrate; +use crate::cob::store::access::{ReadOnly, WriteAs}; use crate::crypto::PublicKey; use crate::crypto::ssh::agent::Agent; use crate::crypto::ssh::{Keystore, Passphrase, keystore}; @@ -757,15 +758,16 @@ impl Home { } /// Return a read-only handle for the issues cache. - pub fn issues<'a, R>( + pub fn issues<'a, Repo>( &self, - repository: &'a R, - ) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreReader>, Error> + repository: &'a Repo, + ) -> Result<cob::issue::Cache<'a, Repo, ReadOnly, cob::cache::StoreReader>, Error> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, { let db = self.cobs_db()?; - let store = cob::issue::Issues::open(repository)?; + + let store = cob::issue::Issues::open(repository, ReadOnly)?; db.check_version()?; @@ -773,15 +775,16 @@ impl Home { } /// Return a read-write handle for the issues cache. - pub fn issues_mut<'a, R>( + pub fn issues_mut<'a, 'b, Repo, Signer>( &self, - repository: &'a R, - ) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreWriter>, Error> + repository: &'a Repo, + signer: &'b Signer, + ) -> Result<cob::issue::Cache<'a, Repo, WriteAs<'b, Signer>, cob::cache::StoreWriter>, Error> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, { let db = self.cobs_db_mut()?; - let store = cob::issue::Issues::open(repository)?; + let store = cob::issue::Issues::open(repository, WriteAs::new(signer))?; db.check_version()?; @@ -789,15 +792,15 @@ impl Home { } /// Return a read-only handle for the patches cache. - pub fn patches<'a, R>( + pub fn patches<'a, Repo>( &self, - repository: &'a R, - ) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreReader>, Error> + repository: &'a Repo, + ) -> Result<cob::patch::Cache<'a, Repo, ReadOnly, cob::cache::StoreReader>, Error> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, { let db = self.cobs_db()?; - let store = cob::patch::Patches::open(repository)?; + let store = cob::patch::Patches::open(repository, ReadOnly)?; db.check_version()?; @@ -805,15 +808,16 @@ impl Home { } /// Return a read-write handle for the patches cache. - pub fn patches_mut<'a, R>( + pub fn patches_mut<'a, 'b, Repo, Signer>( &self, - repository: &'a R, - ) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreWriter>, Error> + repository: &'a Repo, + signer: &'b Signer, + ) -> Result<cob::patch::Cache<'a, Repo, WriteAs<'b, Signer>, cob::cache::StoreWriter>, Error> where - R: ReadRepository + cob::Store<Namespace = NodeId>, + Repo: ReadRepository + cob::Store<Namespace = NodeId>, { let db = self.cobs_db_mut()?; - let store = cob::patch::Patches::open(repository)?; + let store = cob::patch::Patches::open(repository, WriteAs::new(signer))?; db.check_version()?; diff --git a/crates/radicle/src/rad.rs b/crates/radicle/src/rad.rs index eb5f65ba..7791b66e 100644 --- a/crates/radicle/src/rad.rs +++ b/crates/radicle/src/rad.rs @@ -76,6 +76,7 @@ where ) })?; let doc = identity::Doc::initial(proj, delegate, visibility); + let (project, identity) = Repository::init(&doc, &storage, signer)?; let url = git::Url::from(project.id); diff --git a/crates/radicle/src/storage.rs b/crates/radicle/src/storage.rs index 5156d594..61cefda9 100644 --- a/crates/radicle/src/storage.rs +++ b/crates/radicle/src/storage.rs @@ -24,7 +24,6 @@ use crate::identity::{Did, PayloadError, doc}; use crate::identity::{Doc, DocAt, DocError}; use crate::identity::{Identity, RepoId}; use crate::node::SyncedAt; -use crate::node::device::Device; use crate::storage::git::NAMESPACES_GLOB; use crate::storage::refs::{FeatureLevel, Refs, SignedRefs}; @@ -697,16 +696,20 @@ pub trait WriteRepository: ReadRepository + SignRepository { /// Allows signing refs. pub trait SignRepository { /// Sign the repository's refs under the `refs/rad/sigrefs` branch. - fn sign_refs<G>(&self, signer: &Device<G>) -> Result<SignedRefs, RepositoryError> + fn sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError> where - G: crypto::signature::Signer<crypto::Signature>; + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Verifier<crypto::Signature>; /// Sign the repository's refs under the `refs/rad/sigrefs` branch, even if unchanged. /// /// Most users will prefer [`Self::sign_refs`]. - fn force_sign_refs<G>(&self, signer: &Device<G>) -> Result<SignedRefs, RepositoryError> + fn force_sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError> where - G: crypto::signature::Signer<crypto::Signature>; + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Verifier<crypto::Signature>; } impl<T, S> ReadStorage for T diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs index 0286cceb..7d00c044 100644 --- a/crates/radicle/src/storage/git.rs +++ b/crates/radicle/src/storage/git.rs @@ -1004,43 +1004,52 @@ impl WriteRepository for Repository { } impl SignRepository for Repository { - fn sign_refs<G: crypto::signature::Signer<crypto::Signature>>( - &self, - signer: &Device<G>, - ) -> Result<SignedRefs, RepositoryError> { + fn sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError> + where + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Verifier<crypto::Signature>, + { self.sign_refs_with(signer, false) } - fn force_sign_refs<G: crypto::signature::Signer<crypto::Signature>>( - &self, - signer: &Device<G>, - ) -> Result<SignedRefs, RepositoryError> { + fn force_sign_refs<Signer>(&self, signer: &Signer) -> Result<SignedRefs, RepositoryError> + where + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Verifier<crypto::Signature>, + { self.sign_refs_with(signer, true) } } impl Repository { - fn sign_refs_with<G: crypto::signature::Signer<crypto::Signature>>( + fn sign_refs_with<Signer>( &self, - signer: &Device<G>, + signer: &Signer, force: bool, - ) -> Result<SignedRefs, RepositoryError> { - let remote = signer.public_key(); + ) -> Result<SignedRefs, RepositoryError> + where + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Verifier<crypto::Signature>, + { + let remote = signer.verifying_key(); // Ensure the root reference is set, which is checked during sigref verification. if self - .reference_oid(remote, &git::refs::storage::IDENTITY_ROOT) + .reference_oid(&remote, &git::refs::storage::IDENTITY_ROOT) .is_err() { - self.set_remote_identity_root(remote)?; + self.set_remote_identity_root(&remote)?; } - let committer = refs::sigrefs::git::Committer::from_env_or_now(remote); + let committer = refs::sigrefs::git::Committer::from_env_or_now(&remote); - let refs = self.references_of(remote)?; + let refs = self.references_of(&remote)?; let signed = if force { - refs.force_save(*remote, committer, self, signer)? + refs.force_save(remote, committer, self, signer)? } else { - refs.save(*remote, committer, self, signer)? + refs.save(remote, committer, self, signer)? }; Ok(signed) diff --git a/crates/radicle/src/storage/git/cob.rs b/crates/radicle/src/storage/git/cob.rs index 5c6a8b0f..17545f50 100644 --- a/crates/radicle/src/storage/git/cob.rs +++ b/crates/radicle/src/storage/git/cob.rs @@ -17,7 +17,6 @@ use crate::git::*; use crate::identity; use crate::identity::doc::DocError; use crate::node::NodeId; -use crate::node::device::Device; use crate::storage; use crate::storage::Error; use crate::storage::{ @@ -232,25 +231,35 @@ impl<R: storage::WriteRepository> change::Storage for DraftStore<'_, R> { } } -impl<R> SignRepository for DraftStore<'_, R> +impl<Repo> SignRepository for DraftStore<'_, Repo> where - R: storage::ReadRepository, + Repo: storage::ReadRepository, { - fn sign_refs<G: crypto::signature::Signer<crypto::Signature>>( + fn sign_refs<Signer>( &self, - signer: &Device<G>, - ) -> Result<storage::refs::SignedRefs, RepositoryError> { + signer: &Signer, + ) -> Result<storage::refs::SignedRefs, RepositoryError> + where + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Verifier<crypto::Signature>, + { // Since this is a draft store, we do not actually want to sign the refs. // Instead, we just return the existing signed refs. - let remote = self.repo.remote(signer.public_key())?; + let remote = self.repo.remote(&signer.verifying_key())?; Ok(remote.refs) } - fn force_sign_refs<G: crypto::signature::Signer<crypto::Signature>>( + fn force_sign_refs<Signer>( &self, - signer: &Device<G>, - ) -> Result<storage::refs::SignedRefs, RepositoryError> { + signer: &Signer, + ) -> Result<storage::refs::SignedRefs, RepositoryError> + where + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Verifier<crypto::Signature>, + { self.sign_refs(signer) } } diff --git a/crates/radicle/src/storage/refs.rs b/crates/radicle/src/storage/refs.rs index ee98c977..c7b6b3e9 100644 --- a/crates/radicle/src/storage/refs.rs +++ b/crates/radicle/src/storage/refs.rs @@ -499,6 +499,7 @@ mod tests { use super::*; use crate::assert_matches; + use crate::node::device::Device; use crate::storage::WriteRepository as _; use crate::{Storage, cob::Title, cob::identity::Identity, rad, test::fixtures}; @@ -574,14 +575,14 @@ mod tests { }) .unwrap(); - let mut paris_ident = Identity::load_mut(&paris).unwrap(); - let mut london_ident = Identity::load_mut(&london).unwrap(); + let mut paris_ident = Identity::load_mut(&paris, &alice).unwrap(); + let mut london_ident = Identity::load_mut(&london, &alice).unwrap(); paris_ident - .update(Title::new("Add Bob").unwrap(), "", &paris_doc, &alice) + .update(Title::new("Add Bob").unwrap(), "", &paris_doc) .unwrap(); london_ident - .update(Title::new("Add Bob").unwrap(), "", &london_doc, &alice) + .update(Title::new("Add Bob").unwrap(), "", &london_doc) .unwrap(); } diff --git a/crates/radicle/src/test/storage.rs b/crates/radicle/src/test/storage.rs index 4175a67d..f1b1ae21 100644 --- a/crates/radicle/src/test/storage.rs +++ b/crates/radicle/src/test/storage.rs @@ -11,7 +11,6 @@ use crate::git::fmt; use crate::identity::doc::{Doc, DocAt, DocError, RawDoc, RepoId}; use crate::node::NodeId; -use crate::node::device::Device; pub use crate::storage::*; @@ -367,17 +366,23 @@ impl WriteRepository for MockRepository { } impl SignRepository for MockRepository { - fn sign_refs<G: crypto::signature::Signer<crypto::Signature>>( + fn sign_refs<Signer>( &self, - _signer: &Device<G>, - ) -> Result<crate::storage::refs::SignedRefs, RepositoryError> { + _signer: &Signer, + ) -> Result<crate::storage::refs::SignedRefs, RepositoryError> + where + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + { todo!() } - fn force_sign_refs<G: crypto::signature::Signer<crypto::Signature>>( - &self, - _signer: &Device<G>, - ) -> Result<crate::storage::refs::SignedRefs, RepositoryError> { + fn force_sign_refs<Signer>(&self, _signer: &Signer) -> Result<refs::SignedRefs, RepositoryError> + where + Signer: crypto::signature::Keypair<VerifyingKey = crypto::PublicKey>, + Signer: crypto::signature::Signer<crypto::Signature>, + Signer: crypto::signature::Verifier<crypto::Signature>, + { todo!() } }