radicle: implement caching for issues and patches
Implement caching for the `Issue` and `Patch` COB. This is achieved by having the `Transaction::initial` and `Transaction::commit` methods take a cache which can update the newly created/updated object. The `Store::remove` method also takes the cache for removing the object from the cache, as well as the repository. This meant that the `*Mut` types for the respective COBs are required to carry a cache alongside the repository store. Identities will not be cached, and so this is always set to `NoCache` -- which performs no caching, and always succeeds. To perform cache reading for issues and patches, the `cache::Issues` and `cache::Patches` traits are introduced. They capture the minimal amount of methods that are needed for the `rad issue` and `rad patch` CLI commands. The implementor for each of these traits is there respective `Cache` types, which combines their backing repository store and the SQLite database. They both provide convenience methods: - `create` (and `draft` for patches) - `get_mut` - `remove` which can be used instead of their repository store counterparts. All uses of the repository stores (where needed) are replaced with `Cache` types. Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com> X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
parent
b26842d4d8
commit
985b0af3f6
|
|
@ -5,9 +5,10 @@ use std::str::FromStr;
|
|||
use std::time;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use radicle::issue::cache::Issues as _;
|
||||
use radicle::patch::cache::Patches as _;
|
||||
use thiserror::Error;
|
||||
|
||||
use radicle::cob;
|
||||
use radicle::git::raw;
|
||||
use radicle::identity::doc;
|
||||
use radicle::identity::doc::{DocError, RepoId};
|
||||
|
|
@ -186,8 +187,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
info.push([term::format::bold(proj.name()).into()]);
|
||||
info.push([term::format::italic(proj.description()).into()]);
|
||||
|
||||
let issues = cob::issue::Issues::open(&repo)?.counts()?;
|
||||
let patches = cob::patch::Patches::open(&repo)?.counts()?;
|
||||
let issues = profile.issues(&repo)?.counts()?;
|
||||
let patches = profile.patches(&repo)?.counts()?;
|
||||
|
||||
info.push([term::Line::spaced([
|
||||
term::format::tertiary(issues.open).into(),
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ use anyhow::anyhow;
|
|||
|
||||
use localtime::LocalTime;
|
||||
use radicle::identity::Identity;
|
||||
use radicle::issue::Issues;
|
||||
use radicle::issue::cache::Issues as _;
|
||||
use radicle::node::notifications;
|
||||
use radicle::node::notifications::*;
|
||||
use radicle::patch::Patches;
|
||||
use radicle::patch::cache::Patches as _;
|
||||
use radicle::prelude::{Profile, RepoId};
|
||||
use radicle::storage::{ReadRepository, ReadStorage};
|
||||
use radicle::{cob, Storage};
|
||||
|
|
@ -233,8 +233,8 @@ where
|
|||
let (_, head) = repo.head()?;
|
||||
let doc = repo.identity_doc()?;
|
||||
let proj = doc.project()?;
|
||||
let issues = Issues::open(&repo)?;
|
||||
let patches = Patches::open(&repo)?;
|
||||
let issues = profile.issues(&repo)?;
|
||||
let patches = profile.patches(&repo)?;
|
||||
|
||||
let mut notifs = notifs.by_repo(&rid, sort_by.field)?.collect::<Vec<_>>();
|
||||
if !sort_by.reverse {
|
||||
|
|
@ -406,13 +406,13 @@ fn show(
|
|||
|
||||
match n.kind {
|
||||
NotificationKind::Cob { type_name, id } if type_name == *cob::issue::TYPENAME => {
|
||||
let issues = Issues::open(&repo)?;
|
||||
let issues = profile.issues(&repo)?;
|
||||
let issue = issues.get(&id)?.unwrap();
|
||||
|
||||
term::issue::show(&issue, &id, term::issue::Format::default(), profile)?;
|
||||
}
|
||||
NotificationKind::Cob { type_name, id } if type_name == *cob::patch::TYPENAME => {
|
||||
let patches = Patches::open(&repo)?;
|
||||
let patches = profile.patches(&repo)?;
|
||||
let patch = patches.get(&id)?.unwrap();
|
||||
|
||||
term::patch::show(&patch, &id, false, &repo, None, profile)?;
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ use anyhow::{anyhow, Context as _};
|
|||
|
||||
use radicle::cob::common::{Label, Reaction};
|
||||
use radicle::cob::issue;
|
||||
use radicle::cob::issue::{CloseReason, Issues, State};
|
||||
use radicle::cob::issue::{CloseReason, State};
|
||||
use radicle::cob::thread;
|
||||
use radicle::crypto::Signer;
|
||||
use radicle::issue::cache::Issues as _;
|
||||
use radicle::prelude::Did;
|
||||
use radicle::profile;
|
||||
use radicle::storage;
|
||||
use radicle::storage::{WriteRepository, WriteStorage};
|
||||
use radicle::storage::{ReadRepository, WriteRepository, WriteStorage};
|
||||
use radicle::Profile;
|
||||
use radicle::{cob, Node};
|
||||
|
||||
|
|
@ -425,7 +426,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
| Operation::Label { .. }
|
||||
);
|
||||
|
||||
let mut issues = Issues::open(&repo)?;
|
||||
let mut issues = profile.issues_mut(&repo)?;
|
||||
|
||||
match options.op {
|
||||
Operation::Edit {
|
||||
|
|
@ -546,7 +547,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
issue.label(labels, &signer)?;
|
||||
}
|
||||
Operation::List { assigned, state } => {
|
||||
list(&issues, &assigned, &state, &profile)?;
|
||||
list(issues, &assigned, &state, &profile)?;
|
||||
}
|
||||
Operation::Delete { id } => {
|
||||
let id = id.resolve(&repo.backend)?;
|
||||
|
|
@ -562,13 +563,16 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn list<R: WriteRepository + cob::Store>(
|
||||
issues: &Issues<R>,
|
||||
fn list<C>(
|
||||
cache: C,
|
||||
assigned: &Option<Assigned>,
|
||||
state: &Option<State>,
|
||||
profile: &profile::Profile,
|
||||
) -> anyhow::Result<()> {
|
||||
if issues.is_empty()? {
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
C: issue::cache::Issues,
|
||||
{
|
||||
if cache.is_empty()? {
|
||||
term::print(term::format::italic("Nothing to show."));
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -580,7 +584,8 @@ fn list<R: WriteRepository + cob::Store>(
|
|||
};
|
||||
|
||||
let mut all = Vec::new();
|
||||
for result in issues.all()? {
|
||||
let issues = cache.list()?;
|
||||
for result in issues {
|
||||
let Ok((id, issue)) = result else {
|
||||
// Skip issues that failed to load.
|
||||
continue;
|
||||
|
|
@ -664,16 +669,20 @@ fn list<R: WriteRepository + cob::Store>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn open<R: WriteRepository + cob::Store, G: Signer>(
|
||||
fn open<R, G>(
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
labels: Vec<Label>,
|
||||
assignees: Vec<Did>,
|
||||
options: &Options,
|
||||
issues: &mut Issues<R>,
|
||||
cache: &mut issue::Cache<issue::Issues<'_, R>, cob::cache::StoreWriter>,
|
||||
signer: &G,
|
||||
profile: &Profile,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
R: ReadRepository + WriteRepository + cob::Store,
|
||||
G: Signer,
|
||||
{
|
||||
let (title, description) = if let (Some(t), Some(d)) = (title.as_ref(), description.as_ref()) {
|
||||
(t.to_owned(), d.to_owned())
|
||||
} else if let Some((t, d)) = term::issue::get_title_description(title, description)? {
|
||||
|
|
@ -681,7 +690,7 @@ fn open<R: WriteRepository + cob::Store, G: Signer>(
|
|||
} else {
|
||||
anyhow::bail!("aborting issue creation due to empty title or description");
|
||||
};
|
||||
let issue = issues.create(
|
||||
let issue = cache.create(
|
||||
&title,
|
||||
description,
|
||||
labels.as_slice(),
|
||||
|
|
@ -696,14 +705,18 @@ fn open<R: WriteRepository + cob::Store, G: Signer>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn edit<'a, 'g, R: WriteRepository + cob::Store, G: radicle::crypto::Signer>(
|
||||
issues: &'a mut issue::Issues<'a, R>,
|
||||
fn edit<'a, 'g, R, G>(
|
||||
issues: &'g mut issue::Cache<issue::Issues<'a, R>, cob::cache::StoreWriter>,
|
||||
repo: &storage::git::Repository,
|
||||
id: Rev,
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
signer: &G,
|
||||
) -> anyhow::Result<issue::IssueMut<'a, 'g, R>> {
|
||||
) -> anyhow::Result<issue::IssueMut<'a, 'g, R, cob::cache::StoreWriter>>
|
||||
where
|
||||
R: WriteRepository + ReadRepository + cob::Store,
|
||||
G: radicle::crypto::Signer,
|
||||
{
|
||||
let id = id.resolve(&repo.backend)?;
|
||||
let mut issue = issues.get_mut(&id)?;
|
||||
let (root, _) = issue.root();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ use anyhow::anyhow;
|
|||
|
||||
use radicle::cob::patch::PatchId;
|
||||
use radicle::cob::{patch, Label};
|
||||
use radicle::patch::cache::Patches as _;
|
||||
use radicle::storage::git::transport;
|
||||
use radicle::{prelude::*, Node};
|
||||
|
||||
|
|
@ -179,27 +180,6 @@ pub struct LabelOptions {
|
|||
pub delete: BTreeSet<Label>,
|
||||
}
|
||||
|
||||
pub struct Filter(fn(&patch::State) -> bool);
|
||||
|
||||
impl Filter {
|
||||
/// Match everything.
|
||||
fn all() -> Self {
|
||||
Self(|_| true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Filter {
|
||||
fn default() -> Self {
|
||||
Self(|state| matches!(state, patch::State::Open { .. }))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Filter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Filter(..)")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Operation {
|
||||
Show {
|
||||
|
|
@ -250,7 +230,7 @@ pub enum Operation {
|
|||
opts: LabelOptions,
|
||||
},
|
||||
List {
|
||||
filter: Filter,
|
||||
filter: Option<patch::Status>,
|
||||
},
|
||||
Edit {
|
||||
patch_id: Rev,
|
||||
|
|
@ -311,7 +291,7 @@ impl Args for Options {
|
|||
let mut patch_id = None;
|
||||
let mut revision_id = None;
|
||||
let mut message = Message::default();
|
||||
let mut filter = Filter::default();
|
||||
let mut filter = Some(patch::Status::Open);
|
||||
let mut diff = false;
|
||||
let mut debug = false;
|
||||
let mut undo = false;
|
||||
|
|
@ -487,19 +467,19 @@ impl Args for Options {
|
|||
|
||||
// List options.
|
||||
Long("all") => {
|
||||
filter = Filter::all();
|
||||
filter = None;
|
||||
}
|
||||
Long("draft") => {
|
||||
filter = Filter(|s| s == &patch::State::Draft);
|
||||
filter = Some(patch::Status::Draft);
|
||||
}
|
||||
Long("archived") => {
|
||||
filter = Filter(|s| s == &patch::State::Archived);
|
||||
filter = Some(patch::Status::Archived);
|
||||
}
|
||||
Long("merged") => {
|
||||
filter = Filter(|s| matches!(s, patch::State::Merged { .. }));
|
||||
filter = Some(patch::Status::Merged);
|
||||
}
|
||||
Long("open") => {
|
||||
filter = Filter(|s| matches!(s, patch::State::Open { .. }));
|
||||
filter = Some(patch::Status::Open);
|
||||
}
|
||||
Long("authored") => {
|
||||
authored = true;
|
||||
|
|
@ -662,12 +642,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
transport::local::register(profile.storage.clone());
|
||||
|
||||
match options.op {
|
||||
Operation::List { filter: Filter(f) } => {
|
||||
Operation::List { filter } => {
|
||||
let mut authors: BTreeSet<Did> = options.authors.iter().cloned().collect();
|
||||
if options.authored {
|
||||
authors.insert(profile.did());
|
||||
}
|
||||
list::run(f, authors, &repository, &profile)?;
|
||||
list::run(filter.as_ref(), authors, &repository, &profile)?;
|
||||
}
|
||||
Operation::Show {
|
||||
patch_id,
|
||||
|
|
@ -694,7 +674,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
.map(|rev| rev.resolve::<radicle::git::Oid>(&repository.backend))
|
||||
.transpose()?
|
||||
.map(patch::RevisionId::from);
|
||||
diff::run(&patch_id, revision_id, &repository)?;
|
||||
diff::run(&patch_id, revision_id, &repository, &profile)?;
|
||||
}
|
||||
Operation::Update {
|
||||
ref patch_id,
|
||||
|
|
@ -742,6 +722,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
revision_id,
|
||||
&repository,
|
||||
&workdir,
|
||||
&profile,
|
||||
opts,
|
||||
)?;
|
||||
}
|
||||
|
|
@ -801,7 +782,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
label::run(&patch_id, add, delete, &profile, &repository)?;
|
||||
}
|
||||
Operation::Set { patch_id } => {
|
||||
let patches = radicle::cob::patch::Patches::open(&repository)?;
|
||||
let patches = profile.patches(&repository)?;
|
||||
let patch_id = patch_id.resolve(&repository.backend)?;
|
||||
let patch = patches
|
||||
.get(&patch_id)?
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
use super::*;
|
||||
|
||||
use radicle::cob::patch;
|
||||
use radicle::prelude::*;
|
||||
use radicle::storage::git::Repository;
|
||||
|
||||
pub fn run(patch_id: &PatchId, profile: &Profile, repository: &Repository) -> anyhow::Result<()> {
|
||||
let signer = term::signer(profile)?;
|
||||
let mut patches = patch::Patches::open(repository)?;
|
||||
let mut patches = profile.patches_mut(repository)?;
|
||||
let Ok(mut patch) = patches.get_mut(patch_id) else {
|
||||
anyhow::bail!("Patch `{patch_id}` not found");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ pub fn run(
|
|||
repository: &Repository,
|
||||
) -> anyhow::Result<()> {
|
||||
let signer = term::signer(profile)?;
|
||||
let mut patches = radicle::cob::patch::Patches::open(repository)?;
|
||||
let mut patches = profile.patches_mut(repository)?;
|
||||
let Ok(mut patch) = patches.get_mut(patch_id) else {
|
||||
anyhow::bail!("Patch `{patch_id}` not found");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ use git_ref_format::Qualified;
|
|||
use radicle::cob::patch;
|
||||
use radicle::cob::patch::RevisionId;
|
||||
use radicle::git::RefString;
|
||||
use radicle::patch::cache::Patches as _;
|
||||
use radicle::patch::PatchId;
|
||||
use radicle::storage::git::Repository;
|
||||
use radicle::{git, rad};
|
||||
use radicle::{git, rad, Profile};
|
||||
|
||||
use crate::terminal as term;
|
||||
|
||||
|
|
@ -33,10 +34,10 @@ pub fn run(
|
|||
revision_id: Option<RevisionId>,
|
||||
stored: &Repository,
|
||||
working: &git::raw::Repository,
|
||||
profile: &Profile,
|
||||
opts: Options,
|
||||
) -> anyhow::Result<()> {
|
||||
let patches = patch::Patches::open(stored)?;
|
||||
|
||||
let patches = profile.patches(stored)?;
|
||||
let patch = patches
|
||||
.get(patch_id)?
|
||||
.ok_or_else(|| anyhow!("Patch `{patch_id}` not found"))?;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use super::*;
|
|||
use radicle::cob;
|
||||
use radicle::cob::patch;
|
||||
use radicle::cob::thread::CommentId;
|
||||
use radicle::patch::ByRevision;
|
||||
use radicle::prelude::*;
|
||||
use radicle::storage::git::Repository;
|
||||
|
||||
|
|
@ -19,10 +20,15 @@ pub fn run(
|
|||
profile: &Profile,
|
||||
) -> anyhow::Result<()> {
|
||||
let signer = term::signer(profile)?;
|
||||
let mut patches = patch::Patches::open(repo)?;
|
||||
let mut patches = profile.patches_mut(repo)?;
|
||||
|
||||
let revision_id = revision_id.resolve::<cob::EntryId>(&repo.backend)?;
|
||||
let (patch_id, patch, revision_id, revision) = patches
|
||||
let ByRevision {
|
||||
id: patch_id,
|
||||
patch,
|
||||
revision_id,
|
||||
revision,
|
||||
} = patches
|
||||
.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);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use radicle::cob::patch;
|
||||
use radicle::prelude::*;
|
||||
use radicle::storage::git::Repository;
|
||||
|
||||
|
|
@ -6,7 +5,7 @@ use super::*;
|
|||
|
||||
pub fn run(patch_id: &PatchId, profile: &Profile, repository: &Repository) -> anyhow::Result<()> {
|
||||
let signer = &term::signer(profile)?;
|
||||
let patches = patch::Patches::open(repository)?;
|
||||
let mut patches = profile.patches_mut(repository)?;
|
||||
patches.remove(patch_id, signer)?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ pub fn run(
|
|||
patch_id: &PatchId,
|
||||
revision_id: Option<patch::RevisionId>,
|
||||
stored: &Repository,
|
||||
profile: &Profile,
|
||||
) -> anyhow::Result<()> {
|
||||
let patches = patch::Patches::open(stored)?;
|
||||
let patches = profile.patches(stored)?;
|
||||
let Some(patch) = patches.get(patch_id)? else {
|
||||
anyhow::bail!("Patch `{patch_id}` not found");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use super::*;
|
||||
|
||||
use radicle::cob::{patch, resolve_embed};
|
||||
use radicle::cob::{self, patch, resolve_embed};
|
||||
use radicle::crypto;
|
||||
use radicle::prelude::*;
|
||||
use radicle::storage::git::Repository;
|
||||
|
|
@ -15,7 +15,7 @@ pub fn run(
|
|||
repository: &Repository,
|
||||
) -> anyhow::Result<()> {
|
||||
let signer = term::signer(profile)?;
|
||||
let mut patches = patch::Patches::open(repository)?;
|
||||
let mut patches = profile.patches_mut(repository)?;
|
||||
let Ok(patch) = patches.get_mut(patch_id) else {
|
||||
anyhow::bail!("Patch `{patch_id}` not found");
|
||||
};
|
||||
|
|
@ -29,7 +29,7 @@ pub fn run(
|
|||
}
|
||||
|
||||
fn edit_root<G>(
|
||||
mut patch: patch::PatchMut<'_, '_, Repository>,
|
||||
mut patch: patch::PatchMut<'_, '_, Repository, cob::cache::StoreWriter>,
|
||||
title: String,
|
||||
description: String,
|
||||
repository: &Repository,
|
||||
|
|
@ -76,7 +76,7 @@ where
|
|||
}
|
||||
|
||||
fn edit_revision<G>(
|
||||
mut patch: patch::PatchMut<'_, '_, Repository>,
|
||||
mut patch: patch::PatchMut<'_, '_, Repository, cob::cache::StoreWriter>,
|
||||
revision: patch::RevisionId,
|
||||
mut title: String,
|
||||
description: String,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ pub fn run(
|
|||
repository: &Repository,
|
||||
) -> anyhow::Result<()> {
|
||||
let signer = term::signer(profile)?;
|
||||
let mut patches = radicle::cob::patch::Patches::open(repository)?;
|
||||
let mut patches = profile.patches_mut(repository)?;
|
||||
let Ok(mut patch) = patches.get_mut(patch_id) else {
|
||||
anyhow::bail!("Patch `{patch_id}` not found");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use std::collections::BTreeSet;
|
||||
|
||||
use radicle::cob::patch;
|
||||
use radicle::cob::patch::{Patch, PatchId, Patches};
|
||||
use radicle::cob::patch::{Patch, PatchId};
|
||||
use radicle::patch::cache::Patches as _;
|
||||
use radicle::prelude::*;
|
||||
use radicle::profile::Profile;
|
||||
use radicle::storage::git::Repository;
|
||||
|
|
@ -15,22 +16,23 @@ use crate::terminal::patch as common;
|
|||
|
||||
/// List patches.
|
||||
pub fn run(
|
||||
filter: fn(&patch::State) -> bool,
|
||||
filter: Option<&patch::Status>,
|
||||
authors: BTreeSet<Did>,
|
||||
repository: &Repository,
|
||||
profile: &Profile,
|
||||
) -> anyhow::Result<()> {
|
||||
let patches = Patches::open(repository)?;
|
||||
let patches = profile.patches(repository)?;
|
||||
|
||||
let mut all = Vec::new();
|
||||
for patch in patches.all()? {
|
||||
let iter = match filter {
|
||||
Some(status) => patches.list_by_status(status)?,
|
||||
None => patches.list()?,
|
||||
};
|
||||
for patch in iter {
|
||||
let Ok((id, patch)) = patch else {
|
||||
// Skip patches that failed to load.
|
||||
continue;
|
||||
};
|
||||
if !filter(patch.state()) {
|
||||
continue;
|
||||
}
|
||||
if !authors.is_empty() {
|
||||
if !authors.contains(patch.author().id()) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use super::*;
|
||||
|
||||
use radicle::cob::patch;
|
||||
use radicle::prelude::*;
|
||||
use radicle::storage::git::Repository;
|
||||
|
||||
|
|
@ -11,7 +10,7 @@ pub fn run(
|
|||
repository: &Repository,
|
||||
) -> anyhow::Result<()> {
|
||||
let signer = term::signer(profile)?;
|
||||
let mut patches = patch::Patches::open(repository)?;
|
||||
let mut patches = profile.patches_mut(repository)?;
|
||||
let Ok(mut patch) = patches.get_mut(patch_id) else {
|
||||
anyhow::bail!("Patch `{patch_id}` not found");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,10 +13,14 @@ pub fn run(
|
|||
repository: &Repository,
|
||||
) -> anyhow::Result<()> {
|
||||
let signer = &term::signer(profile)?;
|
||||
let mut patches = patch::Patches::open(repository)?;
|
||||
let mut patches = profile.patches_mut(repository)?;
|
||||
|
||||
let revision_id = revision_id.resolve::<Oid>(&repository.backend)?;
|
||||
let (patch_id, _, revision_id, _) = patches
|
||||
let patch::ByRevision {
|
||||
id: patch_id,
|
||||
revision_id,
|
||||
..
|
||||
} = patches
|
||||
.find_by_revision(&patch::RevisionId::from(revision_id))?
|
||||
.ok_or_else(|| anyhow!("Patch revision `{revision_id}` not found"))?;
|
||||
let Ok(mut patch) = patches.get_mut(&patch_id) else {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ mod builder;
|
|||
|
||||
use anyhow::{anyhow, Context};
|
||||
|
||||
use radicle::cob::patch::{PatchId, Patches, RevisionId, Verdict};
|
||||
use radicle::cob::patch::{PatchId, RevisionId, Verdict};
|
||||
use radicle::git;
|
||||
use radicle::prelude::*;
|
||||
use radicle::storage::git::Repository;
|
||||
|
|
@ -61,8 +61,7 @@ pub fn run(
|
|||
"couldn't load repository {} from local state",
|
||||
repository.id
|
||||
))?;
|
||||
let mut patches = Patches::open(repository)?;
|
||||
|
||||
let mut patches = profile.patches_mut(repository)?;
|
||||
let mut patch = patches
|
||||
.get_mut(&patch_id)
|
||||
.context(format!("couldn't find patch {patch_id} locally"))?;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ pub fn run(
|
|||
// TODO: Should be optional.
|
||||
workdir: &git::raw::Repository,
|
||||
) -> anyhow::Result<()> {
|
||||
let patches = patch::Patches::open(stored)?;
|
||||
let patches = profile.patches(stored)?;
|
||||
let Some(patch) = patches.get(patch_id)? else {
|
||||
anyhow::bail!("Patch `{patch_id}` not found");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ pub fn run(
|
|||
base_id: Option<git::raw::Oid>,
|
||||
message: term::patch::Message,
|
||||
profile: &Profile,
|
||||
storage: &Repository,
|
||||
repository: &Repository,
|
||||
workdir: &git::raw::Repository,
|
||||
) -> anyhow::Result<()> {
|
||||
// `HEAD`; This is what we are proposing as a patch.
|
||||
let head_branch = try_branch(workdir.head()?)?;
|
||||
|
||||
let (_, target_oid) = get_merge_target(storage, &head_branch)?;
|
||||
let mut patches = patch::Patches::open(storage)?;
|
||||
let (_, target_oid) = get_merge_target(repository, &head_branch)?;
|
||||
let mut patches = profile.patches_mut(repository)?;
|
||||
let Ok(mut patch) = patches.get_mut(&patch_id) else {
|
||||
anyhow::bail!("Patch `{patch_id}` not found");
|
||||
};
|
||||
|
|
@ -27,7 +27,7 @@ pub fn run(
|
|||
let head_oid = branch_oid(&head_branch)?;
|
||||
let base_oid = match base_id {
|
||||
Some(oid) => oid,
|
||||
None => storage.backend.merge_base(*target_oid, *head_oid)?,
|
||||
None => repository.backend.merge_base(*target_oid, *head_oid)?,
|
||||
};
|
||||
|
||||
// N.b. we don't update if both the head and base are the same as
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ use std::path::Path;
|
|||
|
||||
use localtime::LocalDuration;
|
||||
use localtime::LocalTime;
|
||||
use radicle::cob::issue;
|
||||
use radicle::cob::patch;
|
||||
use radicle::git;
|
||||
use radicle::issue::cache::Issues as _;
|
||||
use radicle::node::address;
|
||||
use radicle::node::routing;
|
||||
use radicle::patch::cache::Patches as _;
|
||||
use radicle::storage::{ReadRepository, ReadStorage, WriteRepository};
|
||||
use radicle_term::Element;
|
||||
use serde::Serialize;
|
||||
|
|
@ -90,8 +90,8 @@ pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|||
|
||||
for repo in storage.repositories()? {
|
||||
let repo = storage.repository(repo.rid)?;
|
||||
let issues = issue::Issues::open(&repo)?.counts()?;
|
||||
let patches = patch::Patches::open(&repo)?.counts()?;
|
||||
let issues = profile.issues(&repo)?.counts()?;
|
||||
let patches = profile.patches(&repo)?.counts()?;
|
||||
|
||||
stats.local.issues += issues.total();
|
||||
stats.local.patches += patches.total();
|
||||
|
|
|
|||
|
|
@ -1456,6 +1456,7 @@ fn test_cob_replication() {
|
|||
|
||||
let bob_repo = bob.storage.repository(rid).unwrap();
|
||||
let mut bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
|
||||
let mut bob_cache = radicle::cob::cache::InMemory::default();
|
||||
let issue = bob_issues
|
||||
.create(
|
||||
"Something's fishy",
|
||||
|
|
@ -1463,6 +1464,7 @@ fn test_cob_replication() {
|
|||
&[],
|
||||
&[],
|
||||
[],
|
||||
&mut bob_cache,
|
||||
&bob.signer,
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -1505,7 +1507,7 @@ fn test_cob_deletion() {
|
|||
bob.routes_to(&[(rid, alice.id)]);
|
||||
|
||||
let alice_repo = alice.storage.repository(rid).unwrap();
|
||||
let mut alice_issues = radicle::cob::issue::Issues::open(&alice_repo).unwrap();
|
||||
let mut alice_issues = radicle::cob::issue::Cache::no_cache(&alice_repo).unwrap();
|
||||
let issue = alice_issues
|
||||
.create(
|
||||
"Something's fishy",
|
||||
|
|
@ -1526,7 +1528,7 @@ fn test_cob_deletion() {
|
|||
let bob_issues = radicle::cob::issue::Issues::open(&bob_repo).unwrap();
|
||||
assert!(bob_issues.get(issue_id).unwrap().is_some());
|
||||
|
||||
let alice_issues = radicle::cob::issue::Issues::open(&alice_repo).unwrap();
|
||||
let mut alice_issues = radicle::cob::issue::Cache::no_cache(&alice_repo).unwrap();
|
||||
alice_issues.remove(issue_id, &alice.signer).unwrap();
|
||||
|
||||
log::debug!(target: "test", "Removing issue..");
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ use axum::http::Method;
|
|||
use axum::response::{IntoResponse, Json};
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use radicle::issue::cache::Issues as _;
|
||||
use radicle::patch::cache::Patches as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tokio::sync::RwLock;
|
||||
|
|
@ -60,8 +62,8 @@ impl Context {
|
|||
|
||||
let payload = doc.project()?;
|
||||
let delegates = doc.delegates;
|
||||
let issues = issue::Issues::open(&repo)?.counts()?;
|
||||
let patches = patch::Patches::open(&repo)?.counts()?;
|
||||
let issues = self.profile.issues(&repo)?.counts()?;
|
||||
let patches = self.profile.patches(&repo)?.counts()?;
|
||||
let db = &self.profile.database()?;
|
||||
let seeding = db.count(&id).unwrap_or_default();
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,14 @@ pub enum Error {
|
|||
#[error(transparent)]
|
||||
Storage(#[from] radicle::storage::Error),
|
||||
|
||||
/// Cob cache error.
|
||||
#[error(transparent)]
|
||||
CobCache(#[from] radicle::cob::cache::Error),
|
||||
|
||||
/// Cob issue cache error.
|
||||
#[error(transparent)]
|
||||
CacheIssue(#[from] radicle::cob::issue::cache::Error),
|
||||
|
||||
/// Cob issue error.
|
||||
#[error(transparent)]
|
||||
CobIssue(#[from] radicle::cob::issue::Error),
|
||||
|
|
@ -38,6 +46,10 @@ pub enum Error {
|
|||
#[error(transparent)]
|
||||
CobPatch(#[from] radicle::cob::patch::Error),
|
||||
|
||||
/// Cob patch cache error.
|
||||
#[error(transparent)]
|
||||
CachePatch(#[from] radicle::cob::patch::cache::Error),
|
||||
|
||||
/// Cob store error.
|
||||
#[error(transparent)]
|
||||
CobStore(#[from] radicle::cob::store::Error),
|
||||
|
|
@ -123,6 +135,7 @@ impl IntoResponse for Error {
|
|||
Error::BadRequest(msg) => (StatusCode::BAD_REQUEST, Some(msg)),
|
||||
other => {
|
||||
tracing::error!("Error: {message}");
|
||||
tracing::debug!("Error Debug: {:?}", other);
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Some(other.to_string()))
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ use axum::response::IntoResponse;
|
|||
use axum::routing::get;
|
||||
use axum::{Json, Router};
|
||||
|
||||
use radicle::cob::issue::Issues;
|
||||
use radicle::cob::patch::Patches;
|
||||
use radicle::identity::{Did, Visibility};
|
||||
use radicle::issue::cache::Issues as _;
|
||||
use radicle::node::routing::Store;
|
||||
use radicle::patch::cache::Patches as _;
|
||||
use radicle::storage::{ReadRepository, ReadStorage};
|
||||
|
||||
use crate::api::error::Error;
|
||||
|
|
@ -44,7 +44,6 @@ async fn delegates_projects_handler(
|
|||
let storage = &ctx.profile.storage;
|
||||
let db = &ctx.profile.database()?;
|
||||
let pinned = &ctx.profile.config.web.pinned;
|
||||
|
||||
let mut projects = match show {
|
||||
ProjectQuery::All => storage
|
||||
.repositories()?
|
||||
|
|
@ -73,13 +72,13 @@ async fn delegates_projects_handler(
|
|||
let Ok(payload) = id.doc.project() else {
|
||||
return None;
|
||||
};
|
||||
let Ok(issues) = Issues::open(&repo) else {
|
||||
let Ok(issues) = ctx.profile.issues(&repo) else {
|
||||
return None;
|
||||
};
|
||||
let Ok(issues) = issues.counts() else {
|
||||
return None;
|
||||
};
|
||||
let Ok(patches) = Patches::open(&repo) else {
|
||||
let Ok(patches) = ctx.profile.patches(&repo) else {
|
||||
return None;
|
||||
};
|
||||
let Ok(patches) = patches.counts() else {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ use serde::{Deserialize, Serialize};
|
|||
use serde_json::json;
|
||||
use tower_http::set_header::SetResponseHeaderLayer;
|
||||
|
||||
use radicle::cob::{issue, patch, resolve_embed, Embed, Label, Uri};
|
||||
use radicle::cob::{
|
||||
issue, issue::cache::Issues as _, patch, patch::cache::Patches as _, resolve_embed, Embed,
|
||||
Label, Uri,
|
||||
};
|
||||
use radicle::identity::{Did, RepoId, Visibility};
|
||||
use radicle::node::routing::Store;
|
||||
use radicle::node::{AliasStore, Node, NodeId};
|
||||
|
|
@ -126,13 +129,13 @@ async fn project_root_handler(
|
|||
let Ok(payload) = info.doc.project() else {
|
||||
return None;
|
||||
};
|
||||
let Ok(issues) = issue::Issues::open(&repo) else {
|
||||
let Ok(issues) = ctx.profile.issues(&repo) else {
|
||||
return None;
|
||||
};
|
||||
let Ok(issues) = issues.counts() else {
|
||||
return None;
|
||||
};
|
||||
let Ok(patches) = patch::Patches::open(&repo) else {
|
||||
let Ok(patches) = ctx.profile.patches(&repo) else {
|
||||
return None;
|
||||
};
|
||||
let Ok(patches) = patches.counts() else {
|
||||
|
|
@ -587,9 +590,9 @@ async fn issues_handler(
|
|||
let state = state.unwrap_or_default();
|
||||
let storage = &ctx.profile.storage;
|
||||
let repo = storage.repository(project)?;
|
||||
let issues = issue::Issues::open(&repo)?;
|
||||
let issues = ctx.profile.issues(&repo)?;
|
||||
let mut issues: Vec<_> = issues
|
||||
.all()?
|
||||
.list()?
|
||||
.filter_map(|r| {
|
||||
let (id, issue) = r.ok()?;
|
||||
(state.matches(issue.state())).then_some((id, issue))
|
||||
|
|
@ -639,7 +642,7 @@ async fn issue_create_handler(
|
|||
.filter_map(|embed| resolve_embed(&repo, embed))
|
||||
.collect();
|
||||
|
||||
let mut issues = issue::Issues::open(&repo)?;
|
||||
let mut issues = ctx.profile.issues_mut(&repo)?;
|
||||
let issue = issues
|
||||
.create(
|
||||
issue.title,
|
||||
|
|
@ -672,7 +675,7 @@ async fn issue_update_handler(
|
|||
let storage = &ctx.profile.storage;
|
||||
let signer = ctx.profile.signer()?;
|
||||
let repo = storage.repository(project)?;
|
||||
let mut issues = issue::Issues::open(&repo)?;
|
||||
let mut issues = ctx.profile.issues_mut(&repo)?;
|
||||
let mut issue = issues.get_mut(&issue_id.into())?;
|
||||
|
||||
let id = match action {
|
||||
|
|
@ -723,7 +726,9 @@ async fn issue_handler(
|
|||
) -> impl IntoResponse {
|
||||
let storage = &ctx.profile.storage;
|
||||
let repo = storage.repository(project)?;
|
||||
let issue = issue::Issues::open(&repo)?
|
||||
let issue = ctx
|
||||
.profile
|
||||
.issues(&repo)?
|
||||
.get(&issue_id.into())?
|
||||
.ok_or(Error::NotFound)?;
|
||||
let aliases = ctx.profile.aliases();
|
||||
|
|
@ -756,7 +761,7 @@ async fn patch_create_handler(
|
|||
.signer()
|
||||
.map_err(|_| Error::Auth("Unauthorized"))?;
|
||||
let repo = storage.repository(project)?;
|
||||
let mut patches = patch::Patches::open(&repo)?;
|
||||
let mut patches = ctx.profile.patches_mut(&repo)?;
|
||||
let base_oid = repo.raw().merge_base(*patch.target, *patch.oid)?;
|
||||
|
||||
let patch = patches
|
||||
|
|
@ -795,7 +800,7 @@ async fn patch_update_handler(
|
|||
.signer()
|
||||
.map_err(|_| Error::Auth("Unauthorized"))?;
|
||||
let repo = storage.repository(project)?;
|
||||
let mut patches = patch::Patches::open(&repo)?;
|
||||
let mut patches = ctx.profile.patches_mut(&repo)?;
|
||||
let mut patch = patches.get_mut(&patch_id.into())?;
|
||||
let id = match action {
|
||||
patch::Action::Edit { title, target } => patch.edit(title, target, &signer)?,
|
||||
|
|
@ -940,9 +945,9 @@ async fn patches_handler(
|
|||
let state = state.unwrap_or_default();
|
||||
let storage = &ctx.profile.storage;
|
||||
let repo = storage.repository(project)?;
|
||||
let patches = patch::Patches::open(&repo)?;
|
||||
let patches = ctx.profile.patches(&repo)?;
|
||||
let mut patches = patches
|
||||
.all()?
|
||||
.list()?
|
||||
.filter_map(|r| {
|
||||
let (id, patch) = r.ok()?;
|
||||
(state.matches(patch.state())).then_some((id, patch))
|
||||
|
|
@ -968,9 +973,8 @@ async fn patch_handler(
|
|||
) -> impl IntoResponse {
|
||||
let storage = &ctx.profile.storage;
|
||||
let repo = storage.repository(project)?;
|
||||
let patch = patch::Patches::open(&repo)?
|
||||
.get(&patch_id.into())?
|
||||
.ok_or(Error::NotFound)?;
|
||||
let patches = ctx.profile.patches(&repo)?;
|
||||
let patch = patches.get(&patch_id.into())?.ok_or(Error::NotFound)?;
|
||||
let aliases = ctx.profile.aliases();
|
||||
|
||||
Ok::<_, Error>(Json(api::json::patch(
|
||||
|
|
|
|||
|
|
@ -11,18 +11,16 @@ use serde_json::Value;
|
|||
use time::OffsetDateTime;
|
||||
use tower::ServiceExt;
|
||||
|
||||
use radicle::cob::issue::Issues;
|
||||
use radicle::cob::patch::{MergeTarget, Patches};
|
||||
use radicle::cob::patch::MergeTarget;
|
||||
use radicle::crypto::ssh::keystore::MemorySigner;
|
||||
use radicle::crypto::ssh::Keystore;
|
||||
use radicle::crypto::{KeyPair, Seed, Signer};
|
||||
use radicle::git::{raw as git2, RefString};
|
||||
use radicle::identity::Visibility;
|
||||
use radicle::node;
|
||||
use radicle::profile;
|
||||
use radicle::profile::Home;
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle::Storage;
|
||||
use radicle::{node, profile};
|
||||
use radicle_crypto::test::signer::MockSigner;
|
||||
|
||||
use crate::api::{auth, Context};
|
||||
|
|
@ -231,7 +229,7 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
|
|||
|
||||
let storage = &profile.storage;
|
||||
let repo = storage.repository(rid).unwrap();
|
||||
let mut issues = Issues::open(&repo).unwrap();
|
||||
let mut issues = profile.issues_mut(&repo).unwrap();
|
||||
let issue = issues
|
||||
.create(
|
||||
"Issue #1".to_string(),
|
||||
|
|
@ -245,7 +243,7 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
|
|||
tracing::debug!(target: "test", "Contributor issue: {}", issue.id());
|
||||
|
||||
// eq. rad patch open
|
||||
let mut patches = Patches::open(&repo).unwrap();
|
||||
let mut patches = profile.patches_mut(&repo).unwrap();
|
||||
let oid = radicle::git::Oid::from_str(HEAD).unwrap();
|
||||
let base = radicle::git::Oid::from_str(PARENT).unwrap();
|
||||
let patch = patches
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ use reactor::poller::popol;
|
|||
use reactor::Reactor;
|
||||
use thiserror::Error;
|
||||
|
||||
use radicle::git;
|
||||
use radicle::node;
|
||||
use radicle::node::address;
|
||||
use radicle::node::address::Store as _;
|
||||
|
|
@ -22,6 +21,7 @@ use radicle::node::notifications;
|
|||
use radicle::node::Handle as _;
|
||||
use radicle::profile::Home;
|
||||
use radicle::Storage;
|
||||
use radicle::{cob, git};
|
||||
|
||||
use crate::control;
|
||||
use crate::crypto::Signer;
|
||||
|
|
@ -42,6 +42,9 @@ pub enum Error {
|
|||
/// A routing database error.
|
||||
#[error("routing database error: {0}")]
|
||||
Routing(#[from] routing::Error),
|
||||
/// A cobs cache database error.
|
||||
#[error("cobs cache database error: {0}")]
|
||||
CobsCache(#[from] cob::cache::Error),
|
||||
/// A node database error.
|
||||
#[error("node database error: {0}")]
|
||||
Database(#[from] node::db::Error),
|
||||
|
|
@ -156,6 +159,7 @@ impl Runtime {
|
|||
let policies = home.policies_mut()?;
|
||||
let policies = policy::Config::new(policy, scope, policies);
|
||||
let notifications = home.notifications_mut()?;
|
||||
let cobs_cache = cob::cache::Store::open(home.cobs().join(cob::cache::COBS_DB_FILE))?;
|
||||
|
||||
log::info!(target: "node", "Default seeding policy set to '{}'", &policy);
|
||||
log::info!(target: "node", "Initializing service ({:?})..", network);
|
||||
|
|
@ -257,6 +261,7 @@ impl Runtime {
|
|||
nid,
|
||||
handle.clone(),
|
||||
notifications,
|
||||
cobs_cache,
|
||||
worker::Config {
|
||||
capacity: 8,
|
||||
timeout: time::Duration::from_secs(9),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use std::{
|
|||
|
||||
use crossbeam_channel as chan;
|
||||
|
||||
use radicle::cob::cache::COBS_DB_FILE;
|
||||
use radicle::cob::issue;
|
||||
use radicle::crypto::ssh::{keystore::MemorySigner, Keystore};
|
||||
use radicle::crypto::test::signer::MockSigner;
|
||||
|
|
@ -110,6 +111,7 @@ impl Environment {
|
|||
let keystore = Keystore::new(&home.keys());
|
||||
let keypair = KeyPair::from_seed(Seed::from([!(self.users as u8); 32]));
|
||||
let policies_db = home.node().join(POLICIES_DB_FILE);
|
||||
let cobs_db = home.cobs().join(COBS_DB_FILE);
|
||||
|
||||
config.write(&home.config()).unwrap();
|
||||
|
||||
|
|
@ -124,6 +126,7 @@ impl Environment {
|
|||
|
||||
policy::Store::open(policies_db).unwrap();
|
||||
home.database_mut().unwrap(); // Just create the database.
|
||||
cob::cache::Store::open(cobs_db).unwrap();
|
||||
|
||||
transport::local::register(storage.clone());
|
||||
keystore.store(keypair.clone(), "radicle", None).unwrap();
|
||||
|
|
@ -380,7 +383,7 @@ impl<G: Signer + cyphernet::Ecdh> NodeHandle<G> {
|
|||
/// Create an [`issue::Issue`] in the `NodeHandle`'s storage.
|
||||
pub fn issue(&self, rid: RepoId, title: &str, desc: &str) -> cob::ObjectId {
|
||||
let repo = self.storage.repository(rid).unwrap();
|
||||
let mut issues = issue::Issues::open(&repo).unwrap();
|
||||
let mut issues = issue::Cache::no_cache(&repo).unwrap();
|
||||
*issues
|
||||
.create(title, desc, &[], &[], [], &self.signer)
|
||||
.unwrap()
|
||||
|
|
|
|||
|
|
@ -880,7 +880,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::Issues::open(&repo).unwrap();
|
||||
let mut issues = radicle::issue::Cache::no_cache(&repo).unwrap();
|
||||
issues
|
||||
.create("Issue while offline!", "", &[], &[], [], alice.signer())
|
||||
.unwrap();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use radicle::node::notifications;
|
|||
use radicle::prelude::NodeId;
|
||||
use radicle::storage::refs::RefsAt;
|
||||
use radicle::storage::{ReadRepository, ReadStorage};
|
||||
use radicle::{crypto, Storage};
|
||||
use radicle::{cob, crypto, Storage};
|
||||
use radicle_fetch::FetchLimit;
|
||||
|
||||
use crate::runtime::{thread, Handle};
|
||||
|
|
@ -179,6 +179,7 @@ struct Worker {
|
|||
handle: Handle,
|
||||
policies: policy::Config<policy::store::Read>,
|
||||
notifications: notifications::StoreWriter,
|
||||
cache: cob::cache::StoreWriter,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
|
|
@ -199,7 +200,13 @@ impl Worker {
|
|||
} = task;
|
||||
let remote = fetch.remote();
|
||||
let channels = channels::ChannelsFlush::new(self.handle.clone(), channels, remote, stream);
|
||||
let result = self._process(fetch, stream, channels, self.notifications.clone());
|
||||
let result = self._process(
|
||||
fetch,
|
||||
stream,
|
||||
channels,
|
||||
self.notifications.clone(),
|
||||
self.cache.clone(),
|
||||
);
|
||||
|
||||
log::trace!(target: "worker", "Sending response back to service..");
|
||||
|
||||
|
|
@ -222,6 +229,7 @@ impl Worker {
|
|||
stream: StreamId,
|
||||
mut channels: channels::ChannelsFlush,
|
||||
notifs: notifications::StoreWriter,
|
||||
cache: cob::cache::StoreWriter,
|
||||
) -> FetchResult {
|
||||
match fetch {
|
||||
FetchRequest::Initiator {
|
||||
|
|
@ -232,7 +240,7 @@ impl Worker {
|
|||
timeout: _timeout,
|
||||
} => {
|
||||
log::debug!(target: "worker", "Worker processing outgoing fetch for {rid}");
|
||||
let result = self.fetch(rid, remote, refs_at, channels, notifs);
|
||||
let result = self.fetch(rid, remote, refs_at, channels, notifs, cache);
|
||||
FetchResult::Initiator { rid, result }
|
||||
}
|
||||
FetchRequest::Responder { remote } => {
|
||||
|
|
@ -282,6 +290,7 @@ impl Worker {
|
|||
refs_at: Option<Vec<RefsAt>>,
|
||||
channels: channels::ChannelsFlush,
|
||||
notifs: notifications::StoreWriter,
|
||||
mut cache: cob::cache::StoreWriter,
|
||||
) -> Result<fetch::FetchResult, FetchError> {
|
||||
let FetchConfig {
|
||||
limit,
|
||||
|
|
@ -302,7 +311,7 @@ impl Worker {
|
|||
channels,
|
||||
notifs,
|
||||
)?;
|
||||
let result = handle.fetch(rid, &self.storage, *limit, remote, refs_at)?;
|
||||
let result = handle.fetch(rid, &self.storage, &mut cache, *limit, remote, refs_at)?;
|
||||
|
||||
if let Err(e) = garbage::collect(&self.storage, rid, *expiry) {
|
||||
// N.b. ensure that `git gc` works in debug mode.
|
||||
|
|
@ -326,6 +335,7 @@ impl Pool {
|
|||
nid: NodeId,
|
||||
handle: Handle,
|
||||
notifications: notifications::StoreWriter,
|
||||
cache: cob::cache::StoreWriter,
|
||||
config: Config,
|
||||
) -> Result<Self, policy::Error> {
|
||||
let mut pool = Vec::with_capacity(config.capacity);
|
||||
|
|
@ -343,6 +353,7 @@ impl Pool {
|
|||
fetch_config: config.fetch.clone(),
|
||||
policies,
|
||||
notifications: notifications.clone(),
|
||||
cache: cache.clone(),
|
||||
};
|
||||
let thread = thread::spawn(&nid, format!("worker#{i}"), || worker.run());
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ use localtime::LocalTime;
|
|||
use radicle::crypto::PublicKey;
|
||||
use radicle::prelude::RepoId;
|
||||
use radicle::storage::refs::RefsAt;
|
||||
use radicle::storage::{ReadStorage as _, RefUpdate, RemoteRepository, WriteRepository as _};
|
||||
use radicle::{git, node, Storage};
|
||||
use radicle::storage::{
|
||||
ReadRepository, ReadStorage as _, RefUpdate, RemoteRepository, WriteRepository as _,
|
||||
};
|
||||
use radicle::{cob, git, node, Storage};
|
||||
use radicle_fetch::{Allowed, BlockList, FetchLimit};
|
||||
|
||||
use super::channels::ChannelsFlush;
|
||||
|
|
@ -62,6 +64,7 @@ impl Handle {
|
|||
self,
|
||||
rid: RepoId,
|
||||
storage: &Storage,
|
||||
cache: &mut cob::cache::StoreWriter,
|
||||
limit: FetchLimit,
|
||||
remote: PublicKey,
|
||||
refs_at: Option<Vec<RefsAt>>,
|
||||
|
|
@ -117,6 +120,8 @@ impl Handle {
|
|||
}
|
||||
}
|
||||
|
||||
cache_cobs(&rid, &applied.updated, &repo, cache)?;
|
||||
|
||||
Ok(FetchResult {
|
||||
updated: applied.updated,
|
||||
namespaces: remotes.into_iter().collect(),
|
||||
|
|
@ -194,3 +199,89 @@ fn notify(
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write new `RefUpdate`s that are related a `Patch` or an `Issue`
|
||||
/// COB to the COB cache.
|
||||
fn cache_cobs<S, C>(
|
||||
rid: &RepoId,
|
||||
refs: &[RefUpdate],
|
||||
storage: &S,
|
||||
cache: &mut C,
|
||||
) -> Result<(), error::Cache>
|
||||
where
|
||||
S: ReadRepository + cob::Store,
|
||||
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 issues = cob::issue::Issues::open(storage)?;
|
||||
let patches = cob::patch::Patches::open(storage)?;
|
||||
for update in refs {
|
||||
match update {
|
||||
RefUpdate::Updated { name, .. } | RefUpdate::Created { name, .. } => {
|
||||
match name.to_namespaced() {
|
||||
Some(name) => {
|
||||
let Some(identifier) = cob::TypedId::from_namespaced(&name)? else {
|
||||
continue;
|
||||
};
|
||||
if identifier.is_issue() {
|
||||
if let Some(issue) = issues.get(&identifier.id)? {
|
||||
cache
|
||||
.update(rid, &identifier.id, &issue)
|
||||
.map(|_| ())
|
||||
.map_err(|e| error::Cache::Update {
|
||||
id: identifier.id,
|
||||
type_name: identifier.type_name,
|
||||
err: e.into(),
|
||||
})?;
|
||||
}
|
||||
} else if identifier.is_patch() {
|
||||
if let Some(patch) = patches.get(&identifier.id)? {
|
||||
cache
|
||||
.update(rid, &identifier.id, &patch)
|
||||
.map(|_| ())
|
||||
.map_err(|e| error::Cache::Update {
|
||||
id: identifier.id,
|
||||
type_name: identifier.type_name,
|
||||
err: e.into(),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
RefUpdate::Deleted { name, .. } => match name.to_namespaced() {
|
||||
Some(name) => {
|
||||
let Some(identifier) = cob::TypedId::from_namespaced(&name)? else {
|
||||
continue;
|
||||
};
|
||||
if identifier.is_issue() {
|
||||
cob::cache::Remove::<cob::issue::Issue>::remove(cache, &identifier.id)
|
||||
.map(|_| ())
|
||||
.map_err(|e| error::Cache::Remove {
|
||||
id: identifier.id,
|
||||
type_name: identifier.type_name,
|
||||
err: e.into(),
|
||||
})?;
|
||||
} else if identifier.is_patch() {
|
||||
cob::cache::Remove::<cob::patch::Patch>::remove(cache, &identifier.id)
|
||||
.map(|_| ())
|
||||
.map_err(
|
||||
|e: <C as cob::cache::Remove<cob::patch::Patch>>::RemoveError| {
|
||||
error::Cache::Remove {
|
||||
id: identifier.id,
|
||||
type_name: identifier.type_name,
|
||||
err: e.into(),
|
||||
}
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
None => continue,
|
||||
},
|
||||
RefUpdate::Skipped { .. } => { /* Do nothing */ }
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::io;
|
|||
|
||||
use thiserror::Error;
|
||||
|
||||
use radicle::{git, identity, storage};
|
||||
use radicle::{cob, git, identity, storage};
|
||||
use radicle_fetch as fetch;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
@ -19,6 +19,32 @@ pub enum Fetch {
|
|||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
#[error("validation of storage repository failed")]
|
||||
Validation,
|
||||
#[error(transparent)]
|
||||
Cache(#[from] Cache),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Cache {
|
||||
#[error(transparent)]
|
||||
Parse(#[from] cob::ParseIdentifierError),
|
||||
#[error(transparent)]
|
||||
Repository(#[from] storage::RepositoryError),
|
||||
#[error("failed to remove {type_name} '{id}' from cache: {err}")]
|
||||
Remove {
|
||||
id: cob::ObjectId,
|
||||
type_name: cob::TypeName,
|
||||
#[source]
|
||||
err: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
},
|
||||
#[error(transparent)]
|
||||
Store(#[from] cob::store::Error),
|
||||
#[error("failed to update {type_name} '{id}' in cache: {err}")]
|
||||
Update {
|
||||
id: cob::ObjectId,
|
||||
type_name: cob::TypeName,
|
||||
#[source]
|
||||
err: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> {
|
|||
.map_err(Error::from);
|
||||
}
|
||||
["list"] => {
|
||||
list::for_fetch(&url, &stored)?;
|
||||
list::for_fetch(&url, &profile, &stored)?;
|
||||
}
|
||||
["list", "for-push"] => {
|
||||
list::for_push(&profile, &stored)?;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use radicle::patch::cache::Patches as _;
|
||||
use radicle::profile;
|
||||
use thiserror::Error;
|
||||
|
||||
use radicle::cob;
|
||||
|
|
@ -17,9 +19,15 @@ pub enum Error {
|
|||
/// Git error.
|
||||
#[error(transparent)]
|
||||
Git(#[from] radicle::git::ext::Error),
|
||||
/// Profile error.
|
||||
#[error(transparent)]
|
||||
Profile(#[from] profile::Error),
|
||||
/// COB store error.
|
||||
#[error(transparent)]
|
||||
CobStore(#[from] cob::store::Error),
|
||||
/// Patch COB cache error.
|
||||
#[error(transparent)]
|
||||
PatchCache(#[from] radicle::patch::cache::Error),
|
||||
/// General repository error.
|
||||
#[error(transparent)]
|
||||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
|
|
@ -28,6 +36,7 @@ pub enum Error {
|
|||
/// List refs for fetching (`git fetch` and `git ls-remote`).
|
||||
pub fn for_fetch<R: ReadRepository + cob::Store + 'static>(
|
||||
url: &Url,
|
||||
profile: &Profile,
|
||||
stored: &R,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(namespace) = url.namespace {
|
||||
|
|
@ -48,7 +57,7 @@ pub fn for_fetch<R: ReadRepository + cob::Store + 'static>(
|
|||
}
|
||||
// List the patch refs, but don't abort if there's an error, as this would break
|
||||
// all fetch behavior. Instead, just output an error to the user.
|
||||
if let Err(e) = patch_refs(stored) {
|
||||
if let Err(e) = patch_refs(profile, stored) {
|
||||
eprintln!("remote: error listing patch refs: {e}");
|
||||
}
|
||||
}
|
||||
|
|
@ -74,9 +83,12 @@ pub fn for_push<R: ReadRepository>(profile: &Profile, stored: &R) -> Result<(),
|
|||
}
|
||||
|
||||
/// List canonical patch references. These are magic refs that can be used to pull patch updates.
|
||||
fn patch_refs<R: ReadRepository + cob::Store + 'static>(stored: &R) -> Result<(), Error> {
|
||||
let patches = radicle::cob::patch::Patches::open(stored)?;
|
||||
for patch in patches.all()? {
|
||||
fn patch_refs<R: ReadRepository + cob::Store + 'static>(
|
||||
profile: &Profile,
|
||||
stored: &R,
|
||||
) -> Result<(), Error> {
|
||||
let patches = profile.patches(stored)?;
|
||||
for patch in patches.list()? {
|
||||
let Ok((id, patch)) = patch else {
|
||||
// Ignore patches that fail to decode.
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::IsTerminal;
|
||||
use std::ops::ControlFlow;
|
||||
|
|
@ -8,8 +9,10 @@ use std::{assert_eq, io};
|
|||
|
||||
use thiserror::Error;
|
||||
|
||||
use radicle::cob;
|
||||
use radicle::cob::object::ParseObjectId;
|
||||
use radicle::cob::patch;
|
||||
use radicle::cob::patch::cache::Patches as _;
|
||||
use radicle::crypto::Signer;
|
||||
use radicle::explorer::ExplorerResource;
|
||||
use radicle::identity::Did;
|
||||
|
|
@ -81,6 +84,9 @@ pub enum Error {
|
|||
/// Patch COB error.
|
||||
#[error(transparent)]
|
||||
Patch(#[from] radicle::cob::patch::Error),
|
||||
/// Error from COB patch cache.
|
||||
#[error(transparent)]
|
||||
PatchCache(#[from] patch::cache::Error),
|
||||
/// Patch edit message error.
|
||||
#[error(transparent)]
|
||||
PatchEdit(#[from] cli::patch::Error),
|
||||
|
|
@ -209,7 +215,16 @@ pub fn run(
|
|||
let working = git::raw::Repository::open(working)?;
|
||||
|
||||
if dst == &*rad::PATCHES_REFNAME {
|
||||
patch_open(src, &nid, &working, stored, &signer, profile, opts.clone())
|
||||
patch_open(
|
||||
src,
|
||||
&nid,
|
||||
&working,
|
||||
stored,
|
||||
profile.patches_mut(stored)?,
|
||||
&signer,
|
||||
profile,
|
||||
opts.clone(),
|
||||
)
|
||||
} else {
|
||||
let dst = git::Qualified::from_refstr(dst)
|
||||
.ok_or_else(|| Error::InvalidQualifiedRef(dst.clone()))?;
|
||||
|
|
@ -225,6 +240,7 @@ pub fn run(
|
|||
&nid,
|
||||
&working,
|
||||
stored,
|
||||
profile.patches_mut(stored)?,
|
||||
&signer,
|
||||
opts.clone(),
|
||||
)
|
||||
|
|
@ -266,7 +282,16 @@ pub fn run(
|
|||
return Err(Error::HeadsDiverge(head.into(), *canonical_oid));
|
||||
}
|
||||
}
|
||||
push(src, &dst, *force, &nid, &working, stored, &signer)
|
||||
push(
|
||||
src,
|
||||
&dst,
|
||||
*force,
|
||||
&nid,
|
||||
&working,
|
||||
stored,
|
||||
profile.patches_mut(stored)?,
|
||||
&signer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -323,6 +348,10 @@ fn patch_open<G: Signer>(
|
|||
nid: &NodeId,
|
||||
working: &git::raw::Repository,
|
||||
stored: &storage::git::Repository,
|
||||
mut patches: patch::Cache<
|
||||
patch::Patches<'_, storage::git::Repository>,
|
||||
cob::cache::StoreWriter,
|
||||
>,
|
||||
signer: &G,
|
||||
profile: &Profile,
|
||||
opts: Options,
|
||||
|
|
@ -353,7 +382,6 @@ fn patch_open<G: Signer>(
|
|||
let (title, description) =
|
||||
cli::patch::get_create_message(opts.message, &stored.backend, &base, &head)?;
|
||||
|
||||
let mut patches = patch::Patches::open(stored)?;
|
||||
let patch = if opts.draft {
|
||||
patches.draft(
|
||||
&title,
|
||||
|
|
@ -442,6 +470,10 @@ fn patch_update<G: Signer>(
|
|||
nid: &NodeId,
|
||||
working: &git::raw::Repository,
|
||||
stored: &storage::git::Repository,
|
||||
mut patches: patch::Cache<
|
||||
patch::Patches<'_, storage::git::Repository>,
|
||||
cob::cache::StoreWriter,
|
||||
>,
|
||||
signer: &G,
|
||||
opts: Options,
|
||||
) -> Result<Option<ExplorerResource>, Error> {
|
||||
|
|
@ -452,7 +484,6 @@ fn patch_update<G: Signer>(
|
|||
|
||||
push_ref(src, &dst, force, working, stored.raw())?;
|
||||
|
||||
let mut patches = patch::Patches::open(stored)?;
|
||||
let Ok(mut patch) = patches.get_mut(&patch_id) else {
|
||||
return Err(Error::NotFound(patch_id));
|
||||
};
|
||||
|
|
@ -504,6 +535,7 @@ fn push<G: Signer>(
|
|||
nid: &NodeId,
|
||||
working: &git::raw::Repository,
|
||||
stored: &storage::git::Repository,
|
||||
patches: patch::Cache<patch::Patches<'_, storage::git::Repository>, cob::cache::StoreWriter>,
|
||||
signer: &G,
|
||||
) -> Result<Option<ExplorerResource>, Error> {
|
||||
let head = working.find_reference(src.as_str())?;
|
||||
|
|
@ -524,7 +556,7 @@ fn push<G: Signer>(
|
|||
let old = old.peel_to_commit()?.id();
|
||||
// Only delegates should publish the merge result to the COB.
|
||||
if stored.delegates()?.contains(&nid.into()) {
|
||||
patch_merge_all(old.into(), head.into(), working, stored, signer)?;
|
||||
patch_merge_all(old.into(), head.into(), working, patches, signer)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -536,7 +568,10 @@ fn patch_merge_all<G: Signer>(
|
|||
old: git::Oid,
|
||||
new: git::Oid,
|
||||
working: &git::raw::Repository,
|
||||
stored: &storage::git::Repository,
|
||||
mut patches: patch::Cache<
|
||||
patch::Patches<'_, storage::git::Repository>,
|
||||
cob::cache::StoreWriter,
|
||||
>,
|
||||
signer: &G,
|
||||
) -> Result<(), Error> {
|
||||
let mut revwalk = working.revwalk()?;
|
||||
|
|
@ -547,15 +582,13 @@ fn patch_merge_all<G: Signer>(
|
|||
.map(|r| r.map(git::Oid::from))
|
||||
.collect::<Result<Vec<git::Oid>, _>>()?;
|
||||
|
||||
let mut patches = patch::Patches::open(stored)?;
|
||||
for patch in patches.all()? {
|
||||
let Ok((id, patch)) = patch else {
|
||||
// Skip patches that failed to load.
|
||||
continue;
|
||||
};
|
||||
if !patch.is_open() && !patch.is_draft() {
|
||||
continue;
|
||||
}
|
||||
let all = patches
|
||||
.opened()?
|
||||
.chain(patches.drafted()?)
|
||||
// Skip patches that failed to load.
|
||||
.filter_map(|patch| patch.ok())
|
||||
.collect::<Vec<_>>();
|
||||
for (id, patch) in all {
|
||||
// Later revisions are more likely to be merged, so we build the list backwards.
|
||||
let revisions = patch
|
||||
.revisions()
|
||||
|
|
@ -578,8 +611,8 @@ fn patch_merge_all<G: Signer>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn patch_merge<G: Signer>(
|
||||
mut patch: patch::PatchMut<storage::git::Repository>,
|
||||
fn patch_merge<C: cob::cache::Update<patch::Patch>, G: Signer>(
|
||||
mut patch: patch::PatchMut<storage::git::Repository, C>,
|
||||
revision: patch::RevisionId,
|
||||
commit: git::Oid,
|
||||
working: &git::raw::Repository,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::collections::HashSet;
|
|||
use std::env;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use radicle::cob::patch::{PatchId, Patches};
|
||||
use radicle::cob::patch::PatchId;
|
||||
use radicle::git::Oid;
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle_cli::terminal as term;
|
||||
|
|
@ -15,7 +15,7 @@ fn main() -> anyhow::Result<()> {
|
|||
let profile = radicle::Profile::load()?;
|
||||
let (working, rid) = radicle::rad::cwd()?;
|
||||
let stored = profile.storage.repository(rid)?;
|
||||
let mut patches = Patches::open(&stored)?;
|
||||
let mut patches = profile.patches_mut(&stored)?;
|
||||
let mut patch = patches.get_mut(&pid)?;
|
||||
|
||||
if patch.is_merged() {
|
||||
|
|
|
|||
|
|
@ -20,12 +20,16 @@ pub use radicle_cob::{
|
|||
};
|
||||
pub use radicle_cob::{create, get, git, list, remove, update};
|
||||
|
||||
/// The exact identifier for a particular COB.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct TypedId {
|
||||
/// The identifier of the COB in the store.
|
||||
pub id: ObjectId,
|
||||
/// The type identifier of the COB in the store.
|
||||
pub type_name: TypeName,
|
||||
}
|
||||
|
||||
/// Errors that occur when parsing a Git refname into a [`TypedId`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ParseIdentifierError {
|
||||
#[error(transparent)]
|
||||
|
|
@ -35,20 +39,39 @@ pub enum ParseIdentifierError {
|
|||
}
|
||||
|
||||
impl TypedId {
|
||||
/// Returns `true` is the [`TypedId::type_name`] is for an
|
||||
/// [`issue::Issue`].
|
||||
pub fn is_issue(&self) -> bool {
|
||||
self.type_name == *issue::TYPENAME
|
||||
}
|
||||
|
||||
/// Returns `true` is the [`TypedId::type_name`] is for an
|
||||
/// [`patch::Patch`].
|
||||
pub fn is_patch(&self) -> bool {
|
||||
self.type_name == *patch::TYPENAME
|
||||
}
|
||||
|
||||
/// Parse a [`crate::git::Namespaced`] refname into a [`TypedId`].
|
||||
///
|
||||
/// All namespaces are stripped before parsing the suffix for the
|
||||
/// [`TypedId`] (see [`TypedId::from_qualified`]).
|
||||
pub fn from_namespaced(
|
||||
n: &crate::git::Namespaced,
|
||||
) -> Result<Option<Self>, ParseIdentifierError> {
|
||||
Self::from_qualified(&n.strip_namespace())
|
||||
Self::from_qualified(&n.strip_namespace_recursive())
|
||||
}
|
||||
|
||||
/// Parse a [`crate::git::Qualified`] refname into a [`TypedId`].
|
||||
///
|
||||
/// The refname is expected to be of the form:
|
||||
/// `refs/cobs/<type name>/<object id>`
|
||||
///
|
||||
/// If the refname is not of that form then `None` will be returned.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This will fail if the refname is of the correct form, but the
|
||||
/// type name or object id fail to parse.
|
||||
pub fn from_qualified(q: &crate::git::Qualified) -> Result<Option<Self>, ParseIdentifierError> {
|
||||
match q.non_empty_iter() {
|
||||
("refs", "cobs", type_name, mut id) => {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ pub enum Error {
|
|||
NoRows,
|
||||
}
|
||||
|
||||
/// Read and write to the store.
|
||||
pub type StoreWriter = Store<Write>;
|
||||
/// Write to the store.
|
||||
pub type StoreReader = Store<Read>;
|
||||
|
||||
/// Read-only type witness.
|
||||
#[derive(Clone)]
|
||||
pub struct Read;
|
||||
|
|
@ -194,7 +199,10 @@ pub trait Remove<T> {
|
|||
type Out;
|
||||
type RemoveError: std::error::Error + Send + Sync + 'static;
|
||||
|
||||
fn remove(&mut self, rid: &RepoId, id: &ObjectId) -> Result<Self::Out, Self::RemoveError>;
|
||||
/// Delete an object in the COB cache.
|
||||
///
|
||||
/// This assumes that the `id` is unique across repositories.
|
||||
fn remove(&mut self, id: &ObjectId) -> Result<Self::Out, Self::RemoveError>;
|
||||
}
|
||||
|
||||
/// An in-memory cache for storing COB objects.
|
||||
|
|
@ -259,7 +267,48 @@ impl<T> Remove<T> for NoCache {
|
|||
type Out = ();
|
||||
type RemoveError = Infallible;
|
||||
|
||||
fn remove(&mut self, _rid: &RepoId, _id: &ObjectId) -> Result<Self::Out, Self::RemoveError> {
|
||||
fn remove(&mut self, _id: &ObjectId) -> Result<Self::Out, Self::RemoveError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Track the progress of cache writes when transferring the
|
||||
/// repository COBs to their respective caches.
|
||||
///
|
||||
/// See [`crate::cob::issue::Cache::write_all`] and
|
||||
/// [`crate::cob::patch::Cache::write_all`].
|
||||
pub struct WriteAllProgress {
|
||||
total: usize,
|
||||
seen: usize,
|
||||
}
|
||||
|
||||
impl WriteAllProgress {
|
||||
/// Create a new progress tracker with the given `total` amount.
|
||||
pub fn new(total: usize) -> Self {
|
||||
Self { total, seen: 0 }
|
||||
}
|
||||
|
||||
/// Increment the [`WriteAllProgress::seen`] progress.
|
||||
pub fn inc(&mut self) {
|
||||
self.seen += 1;
|
||||
}
|
||||
|
||||
/// Return the `total` amount.
|
||||
pub fn total(&self) -> usize {
|
||||
self.total
|
||||
}
|
||||
|
||||
/// Return the `seen` amount.
|
||||
pub fn seen(&self) -> usize {
|
||||
self.seen
|
||||
}
|
||||
|
||||
/// Return the percentage of the progress made.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the `total` provided is `0`.
|
||||
pub fn percentage(&self) -> f32 {
|
||||
(self.seen as f32 / self.total as f32) * 100.0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
pub mod cache;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
|
|
@ -15,8 +17,10 @@ use crate::cob::thread::{Comment, CommentId, Thread};
|
|||
use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName};
|
||||
use crate::crypto::Signer;
|
||||
use crate::identity::doc::{Doc, DocError};
|
||||
use crate::prelude::{Did, ReadRepository, Verified};
|
||||
use crate::storage::{RepositoryError, WriteRepository};
|
||||
use crate::prelude::{Did, ReadRepository, RepoId, Verified};
|
||||
use crate::storage::{HasRepoId, RepositoryError, WriteRepository};
|
||||
|
||||
pub use cache::Cache;
|
||||
|
||||
/// Issue operation.
|
||||
pub type Op = cob::Op<Action>;
|
||||
|
|
@ -56,6 +60,18 @@ pub enum Error {
|
|||
/// Error decoding an operation.
|
||||
#[error("op decoding failed: {0}")]
|
||||
Op(#[from] op::OpEncodingError),
|
||||
#[error("failed to update issue {id} in cache: {err}")]
|
||||
CacheUpdate {
|
||||
id: IssueId,
|
||||
#[source]
|
||||
err: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
},
|
||||
#[error("failed to remove issue {id} from cache : {err}")]
|
||||
CacheRemove {
|
||||
id: IssueId,
|
||||
#[source]
|
||||
err: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Reason why an issue was closed.
|
||||
|
|
@ -410,8 +426,8 @@ impl Issue {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, 'g, R> From<IssueMut<'a, 'g, R>> for (IssueId, Issue) {
|
||||
fn from(value: IssueMut<'a, 'g, R>) -> Self {
|
||||
impl<'a, 'g, R, C> From<IssueMut<'a, 'g, R, C>> for (IssueId, Issue) {
|
||||
fn from(value: IssueMut<'a, 'g, R, C>) -> Self {
|
||||
(value.id, value.issue)
|
||||
}
|
||||
}
|
||||
|
|
@ -524,13 +540,14 @@ impl<R: ReadRepository> store::Transaction<Issue, R> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct IssueMut<'a, 'g, R> {
|
||||
pub struct IssueMut<'a, 'g, R, C> {
|
||||
id: ObjectId,
|
||||
issue: Issue,
|
||||
store: &'g mut Issues<'a, R>,
|
||||
cache: &'g mut C,
|
||||
}
|
||||
|
||||
impl<'a, 'g, R> std::fmt::Debug for IssueMut<'a, 'g, R> {
|
||||
impl<'a, 'g, R, C> std::fmt::Debug for IssueMut<'a, 'g, R, C> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.debug_struct("IssueMut")
|
||||
.field("id", &self.id)
|
||||
|
|
@ -539,9 +556,10 @@ impl<'a, 'g, R> std::fmt::Debug for IssueMut<'a, 'g, R> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, 'g, R> IssueMut<'a, 'g, R>
|
||||
impl<'a, 'g, R, C> IssueMut<'a, 'g, R, C>
|
||||
where
|
||||
R: WriteRepository + cob::Store,
|
||||
C: cob::cache::Update<Issue>,
|
||||
{
|
||||
/// Reload the issue data from storage.
|
||||
pub fn reload(&mut self) -> Result<(), store::Error> {
|
||||
|
|
@ -660,13 +678,19 @@ where
|
|||
operations(&mut tx)?;
|
||||
|
||||
let (issue, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
|
||||
self.cache
|
||||
.update(&self.store.as_ref().id(), &self.id, &issue)
|
||||
.map_err(|e| Error::CacheUpdate {
|
||||
id: self.id,
|
||||
err: e.into(),
|
||||
})?;
|
||||
self.issue = issue;
|
||||
|
||||
Ok(commit)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'g, R> Deref for IssueMut<'a, 'g, R> {
|
||||
impl<'a, 'g, R, C> Deref for IssueMut<'a, 'g, R, C> {
|
||||
type Target = Issue;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
|
@ -686,6 +710,15 @@ impl<'a, R> Deref for Issues<'a, R> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, R> HasRepoId for Issues<'a, R>
|
||||
where
|
||||
R: ReadRepository,
|
||||
{
|
||||
fn rid(&self) -> RepoId {
|
||||
self.raw.as_ref().id()
|
||||
}
|
||||
}
|
||||
|
||||
/// Detailed information on issue states
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -718,35 +751,21 @@ impl<'a, R> Issues<'a, R>
|
|||
where
|
||||
R: WriteRepository + cob::Store,
|
||||
{
|
||||
/// 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>(&'g mut self, id: &ObjectId) -> Result<IssueMut<'a, 'g, R>, 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,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new issue.
|
||||
pub fn create<'g, G: Signer>(
|
||||
pub fn create<'g, G, C>(
|
||||
&'g mut self,
|
||||
title: impl ToString,
|
||||
description: impl ToString,
|
||||
labels: &[Label],
|
||||
assignees: &[Did],
|
||||
embeds: impl IntoIterator<Item = Embed>,
|
||||
cache: &'g mut C,
|
||||
signer: &G,
|
||||
) -> Result<IssueMut<'a, 'g, R>, Error> {
|
||||
) -> Result<IssueMut<'a, 'g, R, C>, Error>
|
||||
where
|
||||
G: Signer,
|
||||
C: cob::cache::Update<Issue>,
|
||||
{
|
||||
let (id, issue) = Transaction::initial("Create issue", &mut self.raw, signer, |tx| {
|
||||
tx.thread(description, embeds)?;
|
||||
tx.edit(title)?;
|
||||
|
|
@ -759,11 +778,52 @@ where
|
|||
}
|
||||
Ok(())
|
||||
})?;
|
||||
cache
|
||||
.update(&self.raw.as_ref().id(), &id, &issue)
|
||||
.map_err(|e| Error::CacheUpdate { id, err: e.into() })?;
|
||||
|
||||
Ok(IssueMut {
|
||||
id,
|
||||
issue,
|
||||
store: self,
|
||||
cache,
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove an issue.
|
||||
pub fn remove<C, G: Signer>(&self, id: &ObjectId, signer: &G) -> Result<(), store::Error>
|
||||
where
|
||||
C: cob::cache::Remove<Issue>,
|
||||
{
|
||||
self.raw.remove(id, signer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R> Issues<'a, R>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
/// 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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -782,11 +842,6 @@ where
|
|||
|
||||
Ok(state_groups)
|
||||
}
|
||||
|
||||
/// Remove an issue.
|
||||
pub fn remove<G: Signer>(&self, id: &ObjectId, signer: &G) -> Result<(), store::Error> {
|
||||
self.raw.remove(id, signer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Issue action.
|
||||
|
|
@ -859,16 +914,16 @@ mod test {
|
|||
use super::*;
|
||||
use crate::cob::{ActorId, Reaction};
|
||||
use crate::git::Oid;
|
||||
use crate::issue::cache::Issues as _;
|
||||
use crate::test;
|
||||
use crate::test::arbitrary;
|
||||
|
||||
#[test]
|
||||
fn test_concurrency() {
|
||||
let t = test::setup::Network::default();
|
||||
let mut issues_alice = Issues::open(&*t.alice.repo).unwrap();
|
||||
let mut bob_issues = Issues::open(&*t.bob.repo).unwrap();
|
||||
let mut eve_issues = Issues::open(&*t.eve.repo).unwrap();
|
||||
|
||||
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
|
||||
.create(
|
||||
"Alice Issue",
|
||||
|
|
@ -954,7 +1009,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_create_and_assign() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
|
||||
let assignee = Did::from(arbitrary::gen::<ActorId>(1));
|
||||
let assignee_two = Did::from(arbitrary::gen::<ActorId>(1));
|
||||
|
|
@ -993,7 +1048,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_create_and_reassign() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
|
||||
let assignee = Did::from(arbitrary::gen::<ActorId>(1));
|
||||
let assignee_two = Did::from(arbitrary::gen::<ActorId>(1));
|
||||
|
|
@ -1002,7 +1057,7 @@ mod test {
|
|||
"My first issue",
|
||||
"Blah blah blah.",
|
||||
&[],
|
||||
&[assignee],
|
||||
&[assignee, assignee_two],
|
||||
[],
|
||||
&node.signer,
|
||||
)
|
||||
|
|
@ -1026,7 +1081,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_create_and_get() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let created = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -1052,7 +1107,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_create_and_change_state() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -1092,7 +1147,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_create_and_unassign() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
|
||||
let assignee = Did::from(arbitrary::gen::<ActorId>(1));
|
||||
let assignee_two = Did::from(arbitrary::gen::<ActorId>(1));
|
||||
|
|
@ -1120,7 +1175,8 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_edit() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -1144,7 +1200,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_edit_description() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -1170,7 +1226,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_react() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -1200,7 +1256,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_reply() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -1255,7 +1311,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_label() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let bug_label = Label::new("bug").unwrap();
|
||||
let ux_label = Label::new("ux").unwrap();
|
||||
let wontfix_label = Label::new("wontfix").unwrap();
|
||||
|
|
@ -1293,7 +1349,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 = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -1333,7 +1389,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_comment_redact() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -1381,8 +1437,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_all() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
issues
|
||||
.create("First", "Blah", &[], &[], [], &node.signer)
|
||||
.unwrap();
|
||||
|
|
@ -1394,7 +1449,7 @@ mod test {
|
|||
.unwrap();
|
||||
|
||||
let issues = issues
|
||||
.all()
|
||||
.list()
|
||||
.unwrap()
|
||||
.map(|r| r.map(|(_, i)| i))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
|
|
@ -1410,7 +1465,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_issue_multilines() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let created = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -1436,7 +1491,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_embeds() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let embed1 = Embed {
|
||||
name: String::from("example.html"),
|
||||
content: b"<html>Hello World!</html>".to_vec(),
|
||||
|
|
@ -1494,7 +1549,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_embeds_edit() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let embed1 = Embed {
|
||||
name: String::from("example.html"),
|
||||
content: b"<html>Hello World!</html>".to_vec(),
|
||||
|
|
@ -1538,7 +1593,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_invalid_actions() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -1572,7 +1627,7 @@ mod test {
|
|||
#[test]
|
||||
fn test_invalid_tx() {
|
||||
let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
@ -1608,7 +1663,7 @@ mod test {
|
|||
let identity = repo.identity().unwrap().head();
|
||||
let missing = arbitrary::oid();
|
||||
let type_name = Issue::type_name().clone();
|
||||
let mut issues = Issues::open(&*repo).unwrap();
|
||||
let mut issues = Cache::no_cache(&*repo).unwrap();
|
||||
let mut issue = issues
|
||||
.create(
|
||||
"My first issue",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,685 @@
|
|||
use std::ops::ControlFlow;
|
||||
use std::str::FromStr;
|
||||
|
||||
use sqlite as sql;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::cob;
|
||||
use crate::cob::cache;
|
||||
use crate::cob::cache::{Remove, StoreReader, StoreWriter, Update};
|
||||
use crate::cob::store;
|
||||
use crate::cob::{Embed, Label, ObjectId, TypeName};
|
||||
use crate::crypto::Signer;
|
||||
use crate::prelude::{Did, RepoId};
|
||||
use crate::sql::transaction;
|
||||
use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository};
|
||||
|
||||
use super::{Issue, IssueCounts, IssueId, IssueMut, State};
|
||||
|
||||
/// A set of read-only methods for a [`Issue`] store.
|
||||
pub trait Issues {
|
||||
type Error: std::error::Error + Send + Sync + 'static;
|
||||
|
||||
/// An iterator for returning a set of issues from the store.
|
||||
type Iter<'a>: Iterator<Item = Result<(IssueId, Issue), Self::Error>> + 'a
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
/// Get the `Issue`, identified by `id`, returning `None` if it
|
||||
/// was not found.
|
||||
fn get(&self, id: &IssueId) -> Result<Option<Issue>, Self::Error>;
|
||||
|
||||
/// List all issues that are in the store.
|
||||
fn list(&self) -> Result<Self::Iter<'_>, Self::Error>;
|
||||
|
||||
/// Get the [`IssueCounts`] of all the issues in the store.
|
||||
fn counts(&self) -> Result<IssueCounts, Self::Error>;
|
||||
|
||||
/// Returns `true` if there are no issues in the store.
|
||||
fn is_empty(&self) -> Result<bool, Self::Error> {
|
||||
Ok(self.counts()?.total() == 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Issues`] store that can also [`Update`] and [`Remove`]
|
||||
/// [`Issue`] in/from the store.
|
||||
pub trait IssuesMut: Issues + Update<Issue> + Remove<Issue> {}
|
||||
|
||||
impl<T> IssuesMut for T where T: Issues + Update<Issue> + Remove<Issue> {}
|
||||
|
||||
/// An `Issue` store that relies on the `cache` for reads and as a
|
||||
/// write-through cache.
|
||||
///
|
||||
/// 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,
|
||||
cache: C,
|
||||
}
|
||||
|
||||
impl<R, C> Cache<R, C> {
|
||||
pub fn new(store: R, cache: C) -> Self {
|
||||
Self { store, cache }
|
||||
}
|
||||
|
||||
pub fn rid(&self) -> RepoId
|
||||
where
|
||||
R: HasRepoId,
|
||||
{
|
||||
self.store.rid()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R, C> Cache<super::Issues<'a, R>, C> {
|
||||
/// Create a new [`Issue`] using the [`super::Issues`] as the
|
||||
/// main storage, and writing the update to the `cache`.
|
||||
pub fn create<'g, G>(
|
||||
&'g mut self,
|
||||
title: impl ToString,
|
||||
description: impl ToString,
|
||||
labels: &[Label],
|
||||
assignees: &[Did],
|
||||
embeds: impl IntoIterator<Item = Embed>,
|
||||
signer: &G,
|
||||
) -> Result<IssueMut<'a, 'g, R, C>, super::Error>
|
||||
where
|
||||
R: ReadRepository + WriteRepository + cob::Store,
|
||||
G: Signer,
|
||||
C: Update<Issue>,
|
||||
{
|
||||
self.store.create(
|
||||
title,
|
||||
description,
|
||||
labels,
|
||||
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: &G) -> Result<(), super::Error>
|
||||
where
|
||||
G: Signer,
|
||||
R: ReadRepository + SignRepository + cob::Store,
|
||||
C: Remove<Issue>,
|
||||
{
|
||||
self.store.remove(id, signer)?;
|
||||
self.cache
|
||||
.remove(id)
|
||||
.map_err(|e| super::Error::CacheRemove {
|
||||
id: *id,
|
||||
err: e.into(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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,
|
||||
C: Update<Issue>,
|
||||
{
|
||||
let issue = self
|
||||
.store
|
||||
.get(id)?
|
||||
.ok_or_else(|| store::Error::NotFound((*super::TYPENAME).clone(), *id))?;
|
||||
self.update(&self.rid(), id, &issue)
|
||||
.map_err(|e| super::Error::CacheUpdate {
|
||||
id: *id,
|
||||
err: e.into(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read all the issues from the [`super::Issues`] store and
|
||||
/// writing them to `cache`.
|
||||
///
|
||||
/// The `callback` is used for reporting success, failures, and
|
||||
/// progress to the caller. The caller may also decide to continue
|
||||
/// or break from the process.
|
||||
pub fn write_all(
|
||||
&mut self,
|
||||
on_issue: impl Fn(
|
||||
&Result<(IssueId, Issue), store::Error>,
|
||||
&cache::WriteAllProgress,
|
||||
) -> ControlFlow<()>,
|
||||
) -> Result<(), super::Error>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
C: Update<Issue>,
|
||||
{
|
||||
let issues = self.store.all()?;
|
||||
let mut progress = cache::WriteAllProgress::new(issues.len());
|
||||
for issue in self.store.all()? {
|
||||
progress.inc();
|
||||
match on_issue(&issue, &progress) {
|
||||
ControlFlow::Continue(()) => match issue {
|
||||
Ok((id, issue)) => {
|
||||
self.update(&self.rid(), &id, &issue)
|
||||
.map_err(|e| super::Error::CacheUpdate { id, err: e.into() })?;
|
||||
}
|
||||
Err(_) => continue,
|
||||
},
|
||||
ControlFlow::Break(()) => break,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R> Cache<super::Issues<'a, R>, cache::NoCache>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
/// 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)?;
|
||||
Ok(Self {
|
||||
store,
|
||||
cache: cache::NoCache,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the [`IssueMut`], identified by `id`.
|
||||
pub fn get_mut<'g>(
|
||||
&'g mut self,
|
||||
id: &ObjectId,
|
||||
) -> Result<IssueMut<'a, 'g, R, cache::NoCache>, super::Error> {
|
||||
let issue = self
|
||||
.store
|
||||
.get(id)?
|
||||
.ok_or_else(move || store::Error::NotFound(super::TYPENAME.clone(), *id))?;
|
||||
|
||||
Ok(IssueMut {
|
||||
id: *id,
|
||||
issue,
|
||||
store: &mut self.store,
|
||||
cache: &mut self.cache,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Cache<R, StoreReader> {
|
||||
pub fn reader(store: R, cache: StoreReader) -> Self {
|
||||
Self { store, cache }
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Cache<R, StoreWriter> {
|
||||
pub fn open(store: R, cache: StoreWriter) -> Self {
|
||||
Self { store, cache }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R> Cache<super::Issues<'a, R>, StoreWriter>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
/// 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> {
|
||||
let issue = Issues::get(self, id)?
|
||||
.ok_or_else(move || Error::NotFound(super::TYPENAME.clone(), *id))?;
|
||||
|
||||
Ok(IssueMut {
|
||||
id: *id,
|
||||
issue,
|
||||
store: &mut self.store,
|
||||
cache: &mut self.cache,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, C> cache::Update<Issue> for Cache<R, C>
|
||||
where
|
||||
C: cache::Update<Issue>,
|
||||
{
|
||||
type Out = <C as cache::Update<Issue>>::Out;
|
||||
type UpdateError = <C as cache::Update<Issue>>::UpdateError;
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
rid: &RepoId,
|
||||
id: &ObjectId,
|
||||
object: &Issue,
|
||||
) -> Result<Self::Out, Self::UpdateError> {
|
||||
self.cache.update(rid, id, object)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, C> cache::Remove<Issue> for Cache<R, C>
|
||||
where
|
||||
C: cache::Remove<Issue>,
|
||||
{
|
||||
type Out = <C as cache::Remove<Issue>>::Out;
|
||||
type RemoveError = <C as cache::Remove<Issue>>::RemoveError;
|
||||
|
||||
fn remove(&mut self, id: &ObjectId) -> Result<Self::Out, Self::RemoveError> {
|
||||
self.cache.remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UpdateError {
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Sql(#[from] sql::Error),
|
||||
}
|
||||
|
||||
impl Update<Issue> for StoreWriter {
|
||||
type Out = bool;
|
||||
type UpdateError = UpdateError;
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
rid: &RepoId,
|
||||
id: &ObjectId,
|
||||
object: &Issue,
|
||||
) -> Result<Self::Out, Self::UpdateError> {
|
||||
transaction::<_, UpdateError>(&self.db, move |db| {
|
||||
let mut stmt = db.prepare(
|
||||
"INSERT INTO issues (id, repo, issue)
|
||||
VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT DO UPDATE
|
||||
SET issue = (?3)",
|
||||
)?;
|
||||
|
||||
stmt.bind((1, sql::Value::String(id.to_string())))?;
|
||||
stmt.bind((2, rid))?;
|
||||
stmt.bind((3, sql::Value::String(serde_json::to_string(&object)?)))?;
|
||||
stmt.next()?;
|
||||
|
||||
Ok(db.change_count() > 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Remove<Issue> for StoreWriter {
|
||||
type Out = bool;
|
||||
type RemoveError = sql::Error;
|
||||
|
||||
fn remove(&mut self, id: &ObjectId) -> Result<Self::Out, Self::RemoveError> {
|
||||
transaction::<_, sql::Error>(&self.db, move |db| {
|
||||
let mut stmt = db.prepare(
|
||||
"DELETE FROM issues
|
||||
WHERE id = ?1",
|
||||
)?;
|
||||
|
||||
stmt.bind((1, sql::Value::String(id.to_string())))?;
|
||||
stmt.next()?;
|
||||
|
||||
Ok(db.change_count() > 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NoCacheIter<'a> {
|
||||
inner: Box<dyn Iterator<Item = Result<(IssueId, Issue), super::Error>> + 'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for NoCacheIter<'a> {
|
||||
type Item = Result<(IssueId, Issue), super::Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R> Issues for Cache<super::Issues<'a, R>, cache::NoCache>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
type Error = super::Error;
|
||||
type Iter<'b> = NoCacheIter<'b> where Self: 'b;
|
||||
|
||||
fn get(&self, id: &IssueId) -> Result<Option<Issue>, Self::Error> {
|
||||
self.store.get(id).map_err(super::Error::from)
|
||||
}
|
||||
|
||||
fn list(&self) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
self.store
|
||||
.all()
|
||||
.map(|inner| NoCacheIter {
|
||||
inner: Box::new(inner.into_iter().map(|res| res.map_err(super::Error::from))),
|
||||
})
|
||||
.map_err(super::Error::from)
|
||||
}
|
||||
|
||||
fn counts(&self) -> Result<IssueCounts, Self::Error> {
|
||||
self.store.counts().map_err(super::Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("object `{1}` of type `{0}` was not found")]
|
||||
NotFound(TypeName, ObjectId),
|
||||
#[error(transparent)]
|
||||
Object(#[from] cob::object::ParseObjectId),
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Sql(#[from] sql::Error),
|
||||
}
|
||||
|
||||
/// Iterator that returns a set of issues based on an SQL query.
|
||||
///
|
||||
/// The query is expected to return rows with columns identified by
|
||||
/// the `id` and `issue` names.
|
||||
pub struct IssuesIter<'a> {
|
||||
inner: sql::CursorWithOwnership<'a>,
|
||||
}
|
||||
|
||||
impl<'a> IssuesIter<'a> {
|
||||
fn parse_row(row: sql::Row) -> Result<(IssueId, Issue), Error> {
|
||||
let id = IssueId::from_str(row.read::<&str, _>("id"))?;
|
||||
let issue = serde_json::from_str::<Issue>(row.read::<&str, _>("issue"))?;
|
||||
Ok((id, issue))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for IssuesIter<'a> {
|
||||
type Item = Result<(IssueId, Issue), Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let row = self.inner.next()?;
|
||||
Some(row.map_err(Error::from).and_then(IssuesIter::parse_row))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Issues for Cache<R, StoreWriter>
|
||||
where
|
||||
R: HasRepoId,
|
||||
{
|
||||
type Error = Error;
|
||||
type Iter<'b> = IssuesIter<'b> where Self: 'b;
|
||||
|
||||
fn get(&self, id: &IssueId) -> Result<Option<Issue>, Self::Error> {
|
||||
query::get(&self.cache.db, &self.rid(), id)
|
||||
}
|
||||
|
||||
fn list(&self) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
query::list(&self.cache.db, &self.rid())
|
||||
}
|
||||
|
||||
fn counts(&self) -> Result<IssueCounts, Self::Error> {
|
||||
query::counts(&self.cache.db, &self.rid())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Issues for Cache<R, StoreReader>
|
||||
where
|
||||
R: HasRepoId,
|
||||
{
|
||||
type Error = Error;
|
||||
type Iter<'b> = IssuesIter<'b> where Self: 'b;
|
||||
|
||||
fn get(&self, id: &IssueId) -> Result<Option<Issue>, Self::Error> {
|
||||
query::get(&self.cache.db, &self.rid(), id)
|
||||
}
|
||||
|
||||
fn list(&self) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
query::list(&self.cache.db, &self.rid())
|
||||
}
|
||||
|
||||
fn counts(&self) -> Result<IssueCounts, Self::Error> {
|
||||
query::counts(&self.cache.db, &self.rid())
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper SQL queries for [ `Issues`] trait implementations.
|
||||
mod query {
|
||||
use sqlite as sql;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(super) fn get(
|
||||
db: &sql::ConnectionThreadSafe,
|
||||
rid: &RepoId,
|
||||
id: &IssueId,
|
||||
) -> Result<Option<Issue>, Error> {
|
||||
let id = sql::Value::String(id.to_string());
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT issue
|
||||
FROM issues
|
||||
WHERE id = ?1 and repo = ?2",
|
||||
)?;
|
||||
|
||||
stmt.bind((1, id))?;
|
||||
stmt.bind((2, rid))?;
|
||||
|
||||
match stmt.into_iter().next().transpose()? {
|
||||
None => Ok(None),
|
||||
Some(row) => {
|
||||
let issue = row.read::<&str, _>("issue");
|
||||
let issue = serde_json::from_str(issue)?;
|
||||
Ok(Some(issue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn list<'a>(
|
||||
db: &'a sql::ConnectionThreadSafe,
|
||||
rid: &RepoId,
|
||||
) -> Result<IssuesIter<'a>, Error> {
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT id, issue
|
||||
FROM issues
|
||||
WHERE repo = ?1
|
||||
",
|
||||
)?;
|
||||
stmt.bind((1, rid))?;
|
||||
Ok(IssuesIter {
|
||||
inner: stmt.into_iter(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn counts(
|
||||
db: &sql::ConnectionThreadSafe,
|
||||
rid: &RepoId,
|
||||
) -> Result<IssueCounts, Error> {
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT
|
||||
issue->'$.state' AS state,
|
||||
COUNT(*) AS count
|
||||
FROM issues
|
||||
WHERE repo = ?1
|
||||
GROUP BY issue->'$.state.status'",
|
||||
)?;
|
||||
stmt.bind((1, rid))?;
|
||||
|
||||
stmt.into_iter()
|
||||
.try_fold(IssueCounts::default(), |mut counts, row| {
|
||||
let row = row?;
|
||||
let count = row.read::<i64, _>("count") as usize;
|
||||
let status = serde_json::from_str::<State>(row.read::<&str, _>("state"))?;
|
||||
match status {
|
||||
State::Closed { .. } => counts.closed += count,
|
||||
State::Open => counts.open += count,
|
||||
}
|
||||
Ok(counts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
use radicle_cob::ObjectId;
|
||||
|
||||
use crate::cob::cache::{Store, Update, Write};
|
||||
use crate::cob::thread::Thread;
|
||||
use crate::issue::{CloseReason, Issue, IssueCounts, IssueId, State};
|
||||
use crate::test::arbitrary;
|
||||
use crate::test::storage::MockRepository;
|
||||
|
||||
use super::{Cache, Issues};
|
||||
|
||||
fn memory(store: MockRepository) -> Cache<MockRepository, Store<Write>> {
|
||||
let cache = Store::<Write>::memory().unwrap();
|
||||
Cache { store, cache }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_empty() {
|
||||
let repo = arbitrary::gen::<MockRepository>(1);
|
||||
let mut cache = memory(repo);
|
||||
assert!(cache.is_empty().unwrap());
|
||||
|
||||
let issue = Issue::new(Thread::default());
|
||||
let id = ObjectId::from_str("47799cbab2eca047b6520b9fce805da42b49ecab").unwrap();
|
||||
cache.update(&cache.rid(), &id, &issue).unwrap();
|
||||
|
||||
let issue = Issue {
|
||||
state: State::Closed {
|
||||
reason: CloseReason::Solved,
|
||||
},
|
||||
..Issue::new(Thread::default())
|
||||
};
|
||||
let id = ObjectId::from_str("ae981ded6ed2ed2cdba34c8603714782667f18a3").unwrap();
|
||||
cache.update(&cache.rid(), &id, &issue).unwrap();
|
||||
|
||||
assert!(!cache.is_empty().unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_counts() {
|
||||
let repo = arbitrary::gen::<MockRepository>(1);
|
||||
let mut cache = memory(repo);
|
||||
let n_open = arbitrary::gen::<u8>(0);
|
||||
let n_closed = arbitrary::gen::<u8>(1);
|
||||
let open_ids = (0..n_open)
|
||||
.map(|_| IssueId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<IssueId>>();
|
||||
let closed_ids = (0..n_closed)
|
||||
.map(|_| IssueId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<IssueId>>();
|
||||
|
||||
for id in open_ids.iter() {
|
||||
let issue = Issue::new(Thread::default());
|
||||
cache
|
||||
.update(&cache.rid(), &IssueId::from(*id), &issue)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
for id in closed_ids.iter() {
|
||||
let issue = Issue {
|
||||
state: State::Closed {
|
||||
reason: CloseReason::Solved,
|
||||
},
|
||||
..Issue::new(Thread::default())
|
||||
};
|
||||
cache
|
||||
.update(&cache.rid(), &IssueId::from(*id), &issue)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
cache.counts().unwrap(),
|
||||
IssueCounts {
|
||||
open: open_ids.len(),
|
||||
closed: closed_ids.len()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get() {
|
||||
let repo = arbitrary::gen::<MockRepository>(1);
|
||||
let mut cache = memory(repo);
|
||||
let ids = (0..arbitrary::gen::<u8>(1))
|
||||
.map(|_| IssueId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<IssueId>>();
|
||||
let missing = (0..arbitrary::gen::<u8>(2))
|
||||
.filter_map(|_| {
|
||||
let id = IssueId::from(arbitrary::oid());
|
||||
(!ids.contains(&id)).then_some(id)
|
||||
})
|
||||
.collect::<BTreeSet<IssueId>>();
|
||||
let mut issues = Vec::with_capacity(ids.len());
|
||||
|
||||
for id in ids.iter() {
|
||||
let issue = Issue {
|
||||
title: id.to_string(),
|
||||
..Issue::new(Thread::default())
|
||||
};
|
||||
cache
|
||||
.update(&cache.rid(), &IssueId::from(*id), &issue)
|
||||
.unwrap();
|
||||
issues.push((*id, issue));
|
||||
}
|
||||
|
||||
for (id, issue) in issues.into_iter() {
|
||||
assert_eq!(Some(issue), cache.get(&id).unwrap());
|
||||
}
|
||||
|
||||
for id in &missing {
|
||||
assert_eq!(cache.get(id).unwrap(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
let repo = arbitrary::gen::<MockRepository>(1);
|
||||
let mut cache = memory(repo);
|
||||
let ids = (0..arbitrary::gen::<u8>(1))
|
||||
.map(|_| IssueId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<IssueId>>();
|
||||
let mut issues = Vec::with_capacity(ids.len());
|
||||
|
||||
for id in ids.iter() {
|
||||
let issue = Issue {
|
||||
title: id.to_string(),
|
||||
..Issue::new(Thread::default())
|
||||
};
|
||||
cache
|
||||
.update(&cache.rid(), &IssueId::from(*id), &issue)
|
||||
.unwrap();
|
||||
issues.push((*id, issue));
|
||||
}
|
||||
|
||||
let mut list = cache
|
||||
.list()
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
list.sort_by_key(|(id, _)| *id);
|
||||
issues.sort_by_key(|(id, _)| *id);
|
||||
assert_eq!(issues, list);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove() {
|
||||
let repo = arbitrary::gen::<MockRepository>(1);
|
||||
let mut cache = memory(repo);
|
||||
let ids = (0..arbitrary::gen::<u8>(1))
|
||||
.map(|_| IssueId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<IssueId>>();
|
||||
|
||||
for id in ids.iter() {
|
||||
let issue = Issue {
|
||||
title: id.to_string(),
|
||||
..Issue::new(Thread::default())
|
||||
};
|
||||
cache
|
||||
.update(&cache.rid(), &IssueId::from(*id), &issue)
|
||||
.unwrap();
|
||||
assert_eq!(Some(issue), cache.get(id).unwrap());
|
||||
super::Remove::remove(&mut cache, id).unwrap();
|
||||
assert_eq!(None, cache.get(id).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
pub mod cache;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
|
|
@ -7,9 +8,8 @@ use std::str::FromStr;
|
|||
use amplify::Wrapper;
|
||||
use nonempty::NonEmpty;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::ser::SerializeStruct;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use storage::RepositoryError;
|
||||
use storage::{HasRepoId, RepositoryError};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::cob;
|
||||
|
|
@ -27,6 +27,8 @@ use crate::identity::PayloadError;
|
|||
use crate::prelude::*;
|
||||
use crate::storage;
|
||||
|
||||
pub use cache::Cache;
|
||||
|
||||
/// Type name of a patch.
|
||||
pub static TYPENAME: Lazy<TypeName> =
|
||||
Lazy::new(|| FromStr::from_str("xyz.radicle.patch").expect("type name is valid"));
|
||||
|
|
@ -124,6 +126,18 @@ pub enum Error {
|
|||
/// Initialization failed.
|
||||
#[error("initialization failed: {0}")]
|
||||
Init(&'static str),
|
||||
#[error("failed to update patch {id} in cache: {err}")]
|
||||
CacheUpdate {
|
||||
id: PatchId,
|
||||
#[source]
|
||||
err: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
},
|
||||
#[error("failed to remove patch {id} from cache : {err}")]
|
||||
CacheRemove {
|
||||
id: PatchId,
|
||||
#[source]
|
||||
err: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Patch operation.
|
||||
|
|
@ -359,7 +373,8 @@ impl MergeTarget {
|
|||
}
|
||||
|
||||
/// Patch state.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Patch {
|
||||
/// Title of the patch.
|
||||
pub(super) title: String,
|
||||
|
|
@ -1301,7 +1316,8 @@ mod lookup {
|
|||
}
|
||||
|
||||
/// A patch revision.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Revision {
|
||||
/// Author of the revision.
|
||||
pub(super) author: Author,
|
||||
|
|
@ -1320,6 +1336,10 @@ pub struct Revision {
|
|||
/// Review comments resolved by this revision.
|
||||
pub(super) resolves: BTreeSet<(EntryId, CommentId)>,
|
||||
/// Reactions on code locations and revision itself
|
||||
#[serde(
|
||||
serialize_with = "ser::serialize_reactions",
|
||||
deserialize_with = "ser::deserialize_reactions"
|
||||
)]
|
||||
pub(super) reactions: BTreeMap<Option<CodeLocation>, Reactions>,
|
||||
}
|
||||
|
||||
|
|
@ -1443,6 +1463,39 @@ impl fmt::Display for State {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<&State> for Status {
|
||||
fn from(value: &State) -> Self {
|
||||
match value {
|
||||
State::Draft => Self::Draft,
|
||||
State::Open { .. } => Self::Open,
|
||||
State::Archived => Self::Archived,
|
||||
State::Merged { .. } => Self::Merged,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A simplified enumeration of a [`State`] that can be used for
|
||||
/// filtering purposes.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
Draft,
|
||||
#[default]
|
||||
Open,
|
||||
Archived,
|
||||
Merged,
|
||||
}
|
||||
|
||||
impl fmt::Display for Status {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Archived => write!(f, "archived"),
|
||||
Self::Draft => write!(f, "draft"),
|
||||
Self::Open => write!(f, "open"),
|
||||
Self::Merged => write!(f, "merged"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A lifecycle operation, resulting in a new state.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "status")]
|
||||
|
|
@ -1485,7 +1538,8 @@ impl fmt::Display for Verdict {
|
|||
}
|
||||
|
||||
/// A patch review on a revision.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Review {
|
||||
/// Review author.
|
||||
pub(super) author: Author,
|
||||
|
|
@ -1506,23 +1560,6 @@ pub struct Review {
|
|||
pub(super) timestamp: Timestamp,
|
||||
}
|
||||
|
||||
impl Serialize for Review {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
let mut state = serializer.serialize_struct("Review", 4)?;
|
||||
state.serialize_field("verdict", &self.verdict())?;
|
||||
state.serialize_field("summary", &self.summary())?;
|
||||
state.serialize_field(
|
||||
"comments",
|
||||
&self.comments().map(|(_, c)| c.body()).collect::<Vec<_>>(),
|
||||
)?;
|
||||
state.serialize_field("timestamp", &self.timestamp())?;
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl Review {
|
||||
pub fn new(
|
||||
author: Author,
|
||||
|
|
@ -1847,19 +1884,26 @@ impl<R: ReadRepository> store::Transaction<Patch, R> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct PatchMut<'a, 'g, R> {
|
||||
pub struct PatchMut<'a, 'g, R, C> {
|
||||
pub id: ObjectId,
|
||||
|
||||
patch: Patch,
|
||||
store: &'g mut Patches<'a, R>,
|
||||
cache: &'g mut C,
|
||||
}
|
||||
|
||||
impl<'a, 'g, R> PatchMut<'a, 'g, R>
|
||||
impl<'a, 'g, R, C> PatchMut<'a, 'g, R, C>
|
||||
where
|
||||
R: ReadRepository + SignRepository + cob::Store + 'static,
|
||||
C: cob::cache::Update<Patch>,
|
||||
R: ReadRepository + SignRepository + cob::Store,
|
||||
{
|
||||
pub fn new(id: ObjectId, patch: Patch, store: &'g mut Patches<'a, R>) -> Self {
|
||||
Self { id, patch, store }
|
||||
pub fn new(id: ObjectId, patch: Patch, cache: &'g mut Cache<Patches<'a, R>, C>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
patch,
|
||||
store: &mut cache.store,
|
||||
cache: &mut cache.cache,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &ObjectId {
|
||||
|
|
@ -1890,6 +1934,12 @@ where
|
|||
operations(&mut tx)?;
|
||||
|
||||
let (patch, commit) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
|
||||
self.cache
|
||||
.update(&self.store.as_ref().id(), &self.id, &patch)
|
||||
.map_err(|e| Error::CacheUpdate {
|
||||
id: self.id,
|
||||
err: e.into(),
|
||||
})?;
|
||||
self.patch = patch;
|
||||
|
||||
Ok(commit)
|
||||
|
|
@ -2214,7 +2264,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, 'g, R> Deref for PatchMut<'a, 'g, R> {
|
||||
impl<'a, 'g, R, C> Deref for PatchMut<'a, 'g, R, C> {
|
||||
type Target = Patch;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
|
@ -2223,7 +2273,7 @@ impl<'a, 'g, R> Deref for PatchMut<'a, 'g, R> {
|
|||
}
|
||||
|
||||
/// Detailed information on patch states
|
||||
#[derive(Default, Serialize)]
|
||||
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PatchCounts {
|
||||
pub open: usize,
|
||||
|
|
@ -2239,6 +2289,17 @@ impl PatchCounts {
|
|||
}
|
||||
}
|
||||
|
||||
/// Result of looking up a `Patch`'s `Revision`.
|
||||
///
|
||||
/// See [`Patches::find_by_revision`].
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ByRevision {
|
||||
pub id: PatchId,
|
||||
pub patch: Patch,
|
||||
pub revision_id: RevisionId,
|
||||
pub revision: Revision,
|
||||
}
|
||||
|
||||
pub struct Patches<'a, R> {
|
||||
raw: store::Store<'a, Patch, R>,
|
||||
}
|
||||
|
|
@ -2251,6 +2312,15 @@ impl<'a, R> Deref for Patches<'a, R> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, R> HasRepoId for Patches<'a, R>
|
||||
where
|
||||
R: ReadRepository,
|
||||
{
|
||||
fn rid(&self) -> RepoId {
|
||||
self.as_ref().id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R> Patches<'a, R>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
|
|
@ -2282,23 +2352,27 @@ where
|
|||
}
|
||||
|
||||
/// Find the `Patch` containing the given `Revision`.
|
||||
pub fn find_by_revision(
|
||||
&self,
|
||||
revision: &RevisionId,
|
||||
) -> Result<Option<(PatchId, Patch, RevisionId, Revision)>, Error> {
|
||||
pub fn find_by_revision(&self, revision: &RevisionId) -> Result<Option<ByRevision>, Error> {
|
||||
// Revision may be the patch's first, making it have the same ID.
|
||||
let p_id = ObjectId::from(revision.into_inner());
|
||||
if let Some(p) = self.get(&p_id)? {
|
||||
return Ok(p
|
||||
.revision(revision)
|
||||
.map(|r| (p_id, p.clone(), *revision, r.clone())));
|
||||
return Ok(p.revision(revision).map(|r| ByRevision {
|
||||
id: p_id,
|
||||
patch: p.clone(),
|
||||
revision_id: *revision,
|
||||
revision: r.clone(),
|
||||
}));
|
||||
}
|
||||
let result = self
|
||||
.all()?
|
||||
.filter_map(|result| result.ok())
|
||||
.find_map(|(p_id, p)| {
|
||||
p.revision(revision)
|
||||
.map(|r| (p_id, p.clone(), *revision, r.clone()))
|
||||
p.revision(revision).map(|r| ByRevision {
|
||||
id: p_id,
|
||||
patch: p.clone(),
|
||||
revision_id: *revision,
|
||||
revision: r.clone(),
|
||||
})
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
|
|
@ -2332,10 +2406,10 @@ where
|
|||
|
||||
impl<'a, R> Patches<'a, R>
|
||||
where
|
||||
R: ReadRepository + SignRepository + cob::Store + 'static,
|
||||
R: ReadRepository + SignRepository + cob::Store,
|
||||
{
|
||||
/// Open a new patch.
|
||||
pub fn create<'g, G: Signer>(
|
||||
pub fn create<'g, C, G>(
|
||||
&'g mut self,
|
||||
title: impl ToString,
|
||||
description: impl ToString,
|
||||
|
|
@ -2343,8 +2417,13 @@ where
|
|||
base: impl Into<git::Oid>,
|
||||
oid: impl Into<git::Oid>,
|
||||
labels: &[Label],
|
||||
cache: &'g mut C,
|
||||
signer: &G,
|
||||
) -> Result<PatchMut<'a, 'g, R>, Error> {
|
||||
) -> Result<PatchMut<'a, 'g, R, C>, Error>
|
||||
where
|
||||
C: cob::cache::Update<Patch>,
|
||||
G: Signer,
|
||||
{
|
||||
self._create(
|
||||
title,
|
||||
description,
|
||||
|
|
@ -2353,12 +2432,13 @@ where
|
|||
oid,
|
||||
labels,
|
||||
Lifecycle::default(),
|
||||
cache,
|
||||
signer,
|
||||
)
|
||||
}
|
||||
|
||||
/// Draft a patch. This patch will be created in a [`State::Draft`] state.
|
||||
pub fn draft<'g, G: Signer>(
|
||||
pub fn draft<'g, C, G: Signer>(
|
||||
&'g mut self,
|
||||
title: impl ToString,
|
||||
description: impl ToString,
|
||||
|
|
@ -2366,8 +2446,12 @@ where
|
|||
base: impl Into<git::Oid>,
|
||||
oid: impl Into<git::Oid>,
|
||||
labels: &[Label],
|
||||
cache: &'g mut C,
|
||||
signer: &G,
|
||||
) -> Result<PatchMut<'a, 'g, R>, Error> {
|
||||
) -> Result<PatchMut<'a, 'g, R, C>, Error>
|
||||
where
|
||||
C: cob::cache::Update<Patch>,
|
||||
{
|
||||
self._create(
|
||||
title,
|
||||
description,
|
||||
|
|
@ -2376,12 +2460,17 @@ where
|
|||
oid,
|
||||
labels,
|
||||
Lifecycle::Draft,
|
||||
cache,
|
||||
signer,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get a patch mutably.
|
||||
pub fn get_mut<'g>(&'g mut self, id: &ObjectId) -> Result<PatchMut<'a, 'g, R>, store::Error> {
|
||||
pub fn get_mut<'g, C>(
|
||||
&'g mut self,
|
||||
id: &ObjectId,
|
||||
cache: &'g mut C,
|
||||
) -> Result<PatchMut<'a, 'g, R, C>, store::Error> {
|
||||
let patch = self
|
||||
.raw
|
||||
.get(id)?
|
||||
|
|
@ -2391,11 +2480,12 @@ where
|
|||
id: *id,
|
||||
patch,
|
||||
store: self,
|
||||
cache,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a patch. This is an internal function used by `create` and `draft`.
|
||||
fn _create<'g, G: Signer>(
|
||||
fn _create<'g, C, G: Signer>(
|
||||
&'g mut self,
|
||||
title: impl ToString,
|
||||
description: impl ToString,
|
||||
|
|
@ -2404,8 +2494,12 @@ where
|
|||
oid: impl Into<git::Oid>,
|
||||
labels: &[Label],
|
||||
state: Lifecycle,
|
||||
cache: &'g mut C,
|
||||
signer: &G,
|
||||
) -> Result<PatchMut<'a, 'g, R>, Error> {
|
||||
) -> Result<PatchMut<'a, 'g, R, C>, Error>
|
||||
where
|
||||
C: cob::cache::Update<Patch>,
|
||||
{
|
||||
let (id, patch) = Transaction::initial("Create patch", &mut self.raw, signer, |tx| {
|
||||
tx.revision(description, base, oid)?;
|
||||
tx.edit(title, target)?;
|
||||
|
|
@ -2418,8 +2512,135 @@ where
|
|||
}
|
||||
Ok(())
|
||||
})?;
|
||||
cache
|
||||
.update(&self.raw.as_ref().id(), &id, &patch)
|
||||
.map_err(|e| Error::CacheUpdate { id, err: e.into() })?;
|
||||
|
||||
Ok(PatchMut::new(id, patch, self))
|
||||
Ok(PatchMut {
|
||||
id,
|
||||
patch,
|
||||
store: self,
|
||||
cache,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Helpers for de/serialization of patch data types.
|
||||
mod ser {
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use serde::ser::SerializeSeq;
|
||||
|
||||
use crate::cob::{thread::Reactions, ActorId, CodeLocation};
|
||||
|
||||
/// Serialize a `Revision`'s reaction as an object containing the
|
||||
/// `location`, `emoji`, and all `authors` that have performed the
|
||||
/// same reaction.
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Reaction {
|
||||
location: Option<CodeLocation>,
|
||||
emoji: super::Reaction,
|
||||
authors: Vec<ActorId>,
|
||||
}
|
||||
|
||||
impl Reaction {
|
||||
fn as_revision_reactions(
|
||||
reactions: Vec<Reaction>,
|
||||
) -> BTreeMap<Option<CodeLocation>, Reactions> {
|
||||
reactions.into_iter().fold(
|
||||
BTreeMap::<Option<CodeLocation>, Reactions>::new(),
|
||||
|mut reactions,
|
||||
Reaction {
|
||||
location,
|
||||
emoji,
|
||||
authors,
|
||||
}| {
|
||||
let mut inner = authors
|
||||
.into_iter()
|
||||
.map(|author| (author, emoji))
|
||||
.collect::<BTreeSet<_>>();
|
||||
let entry = reactions.entry(location).or_default();
|
||||
entry.append(&mut inner);
|
||||
reactions
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to serialize a `Revision`'s reactions, since
|
||||
/// `CodeLocation` cannot be a key for a JSON object.
|
||||
///
|
||||
/// The set `reactions` are first turned into a set of
|
||||
/// [`Reaction`]s and then serialized via a `Vec`.
|
||||
pub fn serialize_reactions<S>(
|
||||
reactions: &BTreeMap<Option<CodeLocation>, Reactions>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let reactions = reactions
|
||||
.iter()
|
||||
.flat_map(|(location, reaction)| {
|
||||
let reactions = reaction.iter().fold(
|
||||
BTreeMap::new(),
|
||||
|mut acc: BTreeMap<&super::Reaction, Vec<_>>, (author, emoji)| {
|
||||
acc.entry(emoji).or_default().push(*author);
|
||||
acc
|
||||
},
|
||||
);
|
||||
reactions
|
||||
.into_iter()
|
||||
.map(|(emoji, authors)| Reaction {
|
||||
location: location.clone(),
|
||||
emoji: *emoji,
|
||||
authors,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut s = serializer.serialize_seq(Some(reactions.len()))?;
|
||||
for r in &reactions {
|
||||
s.serialize_element(r)?;
|
||||
}
|
||||
s.end()
|
||||
}
|
||||
|
||||
/// Helper to deserialize a `Revision`'s reactions, the inverse of
|
||||
/// `serialize_reactions`.
|
||||
///
|
||||
/// The `Vec` of [`Reaction`]s are deserialized and converted to a
|
||||
/// `BTreeMap<Option<CodeLocation>, Reactions>`.
|
||||
pub fn deserialize_reactions<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<BTreeMap<Option<CodeLocation>, Reactions>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct ReactionsVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for ReactionsVisitor {
|
||||
type Value = Vec<Reaction>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a reaction of the form {'location', 'emoji', 'authors'}")
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let mut reactions = Vec::new();
|
||||
while let Some(reaction) = seq.next_element()? {
|
||||
reactions.push(reaction);
|
||||
}
|
||||
Ok(reactions)
|
||||
}
|
||||
}
|
||||
|
||||
let reactions = deserializer.deserialize_seq(ReactionsVisitor)?;
|
||||
Ok(Reaction::as_revision_reactions(reactions))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2437,6 +2658,7 @@ mod test {
|
|||
use crate::cob::test::Actor;
|
||||
use crate::crypto::test::signer::MockSigner;
|
||||
use crate::identity;
|
||||
use crate::patch::cache::Patches as _;
|
||||
use crate::test;
|
||||
use crate::test::arbitrary;
|
||||
use crate::test::arbitrary::gen;
|
||||
|
|
@ -2453,12 +2675,54 @@ mod test {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reactions_json_serialization() {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TestReactions {
|
||||
#[serde(
|
||||
serialize_with = "super::ser::serialize_reactions",
|
||||
deserialize_with = "super::ser::deserialize_reactions"
|
||||
)]
|
||||
inner: BTreeMap<Option<CodeLocation>, Reactions>,
|
||||
}
|
||||
|
||||
let reactions = TestReactions {
|
||||
inner: [(
|
||||
None,
|
||||
[
|
||||
(
|
||||
"z6Mkk7oqY4pPxhMmGEotDYsFo97vhCj85BLY1H256HrJmjN8"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
Reaction::new('🚀').unwrap(),
|
||||
),
|
||||
(
|
||||
"z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
Reaction::new('🙏').unwrap(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
reactions,
|
||||
serde_json::from_str(&serde_json::to_string(&reactions).unwrap()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_patch_create_and_get() {
|
||||
let alice = test::setup::NodeWithRepo::default();
|
||||
let checkout = alice.repo.checkout();
|
||||
let branch = checkout.branch_with([("README", b"Hello World!")]);
|
||||
let mut patches = Patches::open(&*alice.repo).unwrap();
|
||||
let mut patches = Cache::no_cache(&*alice.repo).unwrap();
|
||||
let author: Did = alice.signer.public_key().into();
|
||||
let target = MergeTarget::Delegates;
|
||||
let patch = patches
|
||||
|
|
@ -2491,7 +2755,7 @@ mod test {
|
|||
assert_eq!(revision.oid, branch.oid);
|
||||
assert_eq!(revision.base, branch.base);
|
||||
|
||||
let (id, _, _, _) = patches.find_by_revision(&rev_id).unwrap().unwrap();
|
||||
let ByRevision { id, .. } = patches.find_by_revision(&rev_id).unwrap().unwrap();
|
||||
assert_eq!(id, patch_id);
|
||||
}
|
||||
|
||||
|
|
@ -2500,7 +2764,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 = Patches::open(&*alice.repo).unwrap();
|
||||
let mut patches = Cache::no_cache(&*alice.repo).unwrap();
|
||||
let patch = patches
|
||||
.create(
|
||||
"My first patch",
|
||||
|
|
@ -2533,7 +2797,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 = Patches::open(&*alice.repo).unwrap();
|
||||
let mut patches = Cache::no_cache(&*alice.repo).unwrap();
|
||||
let mut patch = patches
|
||||
.create(
|
||||
"My first patch",
|
||||
|
|
@ -2564,7 +2828,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 = Patches::open(&*alice.repo).unwrap();
|
||||
let mut patches = Cache::no_cache(&*alice.repo).unwrap();
|
||||
let mut patch = patches
|
||||
.create(
|
||||
"My first patch",
|
||||
|
|
@ -2616,7 +2880,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 = Patches::open(&*alice.repo).unwrap();
|
||||
let mut patches = Cache::no_cache(&*alice.repo).unwrap();
|
||||
let mut patch = patches
|
||||
.create(
|
||||
"My first patch",
|
||||
|
|
@ -2804,7 +3068,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 = Patches::open(&*alice.repo).unwrap();
|
||||
let mut patches = Cache::no_cache(&*alice.repo).unwrap();
|
||||
let mut patch = patches
|
||||
.create(
|
||||
"My first patch",
|
||||
|
|
@ -2847,7 +3111,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 = Patches::open(&*alice.repo).unwrap();
|
||||
let mut patches = Cache::no_cache(&*alice.repo).unwrap();
|
||||
let mut patch = patches
|
||||
.create(
|
||||
"My first patch",
|
||||
|
|
@ -2894,7 +3158,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 = Patches::open(&*alice.repo).unwrap();
|
||||
let mut patches = Cache::no_cache(&*alice.repo).unwrap();
|
||||
let mut patch = patches
|
||||
.create(
|
||||
"My first patch",
|
||||
|
|
@ -2928,7 +3192,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 = Patches::open(&*alice.repo).unwrap();
|
||||
let mut patches = Cache::no_cache(&*alice.repo).unwrap();
|
||||
let mut patch = patches
|
||||
.create(
|
||||
"My first patch",
|
||||
|
|
@ -2977,7 +3241,7 @@ mod test {
|
|||
let branch = repo
|
||||
.checkout()
|
||||
.branch_with([("README.md", b"Hello, World!")]);
|
||||
let mut patches = Patches::open(&*repo).unwrap();
|
||||
let mut patches = Cache::no_cache(&*repo).unwrap();
|
||||
let mut patch = patches
|
||||
.create(
|
||||
"My first patch",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,974 @@
|
|||
use std::ops::ControlFlow;
|
||||
use std::str::FromStr;
|
||||
|
||||
use sqlite as sql;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::cob;
|
||||
use crate::cob::cache::{self, StoreReader};
|
||||
use crate::cob::cache::{Remove, StoreWriter, Update};
|
||||
use crate::cob::store;
|
||||
use crate::cob::{Label, ObjectId, TypeName};
|
||||
use crate::crypto::Signer;
|
||||
use crate::git;
|
||||
use crate::prelude::RepoId;
|
||||
use crate::sql::transaction;
|
||||
use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository};
|
||||
|
||||
use super::{
|
||||
ByRevision, MergeTarget, Patch, PatchCounts, PatchId, PatchMut, Revision, RevisionId, State,
|
||||
Status,
|
||||
};
|
||||
|
||||
/// A set of read-only methods for a [`Patch`] store.
|
||||
pub trait Patches {
|
||||
type Error: std::error::Error + Send + Sync + 'static;
|
||||
|
||||
/// An iterator for returning a set of patches from the store.
|
||||
type Iter<'a>: Iterator<Item = Result<(PatchId, Patch), Self::Error>> + 'a
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
/// Get the `Patch`, identified by `id`, returning `None` if it
|
||||
/// was not found.
|
||||
fn get(&self, id: &PatchId) -> Result<Option<Patch>, Self::Error>;
|
||||
|
||||
/// Get the `Patch` and its `Revision`, identified by the revision
|
||||
/// `id`, returning `None` if it was not found.
|
||||
fn find_by_revision(&self, id: &RevisionId) -> Result<Option<ByRevision>, Self::Error>;
|
||||
|
||||
/// List all patches that are in the store.
|
||||
fn list(&self) -> Result<Self::Iter<'_>, Self::Error>;
|
||||
|
||||
/// List all patches in the store that match the provided
|
||||
/// `status`.
|
||||
///
|
||||
/// Also see [`Patches::opened`], [`Patches::archived`],
|
||||
/// [`Patches::drafted`], [`Patches::merged`].
|
||||
fn list_by_status(&self, status: &Status) -> Result<Self::Iter<'_>, Self::Error>;
|
||||
|
||||
/// Get the [`PatchCounts`] of all the patches in the store.
|
||||
fn counts(&self) -> Result<PatchCounts, Self::Error>;
|
||||
|
||||
/// List all opened patches in the store.
|
||||
fn opened(&self) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
self.list_by_status(&Status::Open)
|
||||
}
|
||||
|
||||
/// List all archived patches in the store.
|
||||
fn archived(&self) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
self.list_by_status(&Status::Archived)
|
||||
}
|
||||
|
||||
/// List all drafted patches in the store.
|
||||
fn drafted(&self) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
self.list_by_status(&Status::Draft)
|
||||
}
|
||||
|
||||
/// List all merged patches in the store.
|
||||
fn merged(&self) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
self.list_by_status(&Status::Merged)
|
||||
}
|
||||
|
||||
/// Returns `true` if there are no patches in the store.
|
||||
fn is_empty(&self) -> Result<bool, Self::Error> {
|
||||
Ok(self.counts()?.total() == 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Patches`] store that can also [`Update`] and [`Remove`]
|
||||
/// [`Patch`] in/from the store.
|
||||
pub trait PatchesMut: Patches + Update<Patch> + Remove<Patch> {}
|
||||
|
||||
impl<T> PatchesMut for T where T: Patches + Update<Patch> + Remove<Patch> {}
|
||||
|
||||
/// A `Patch` store that relies on the `cache` for reads and as a
|
||||
/// write-through cache.
|
||||
///
|
||||
/// 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(super) cache: C,
|
||||
}
|
||||
|
||||
impl<R, C> Cache<R, C> {
|
||||
pub fn new(store: R, cache: C) -> Self {
|
||||
Self { store, cache }
|
||||
}
|
||||
|
||||
pub fn rid(&self) -> RepoId
|
||||
where
|
||||
R: HasRepoId,
|
||||
{
|
||||
self.store.rid()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
|
||||
/// Create a new [`Patch`] using the [`super::Patches`] as the
|
||||
/// main storage, and writing the update to the `cache`.
|
||||
pub fn create<'g, G>(
|
||||
&'g mut self,
|
||||
title: impl ToString,
|
||||
description: impl ToString,
|
||||
target: MergeTarget,
|
||||
base: impl Into<git::Oid>,
|
||||
oid: impl Into<git::Oid>,
|
||||
labels: &[Label],
|
||||
signer: &G,
|
||||
) -> Result<PatchMut<'a, 'g, R, C>, super::Error>
|
||||
where
|
||||
R: WriteRepository + cob::Store,
|
||||
G: Signer,
|
||||
C: Update<Patch>,
|
||||
{
|
||||
self.store.create(
|
||||
title,
|
||||
description,
|
||||
target,
|
||||
base,
|
||||
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>(
|
||||
&'g mut self,
|
||||
title: impl ToString,
|
||||
description: impl ToString,
|
||||
target: MergeTarget,
|
||||
base: impl Into<git::Oid>,
|
||||
oid: impl Into<git::Oid>,
|
||||
labels: &[Label],
|
||||
signer: &G,
|
||||
) -> Result<PatchMut<'a, 'g, R, C>, super::Error>
|
||||
where
|
||||
R: WriteRepository + cob::Store,
|
||||
G: Signer,
|
||||
C: Update<Patch>,
|
||||
{
|
||||
self.store.draft(
|
||||
title,
|
||||
description,
|
||||
target,
|
||||
base,
|
||||
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: &G) -> Result<(), super::Error>
|
||||
where
|
||||
G: Signer,
|
||||
R: ReadRepository + SignRepository + cob::Store,
|
||||
C: Remove<Patch>,
|
||||
{
|
||||
self.store.remove(id, signer)?;
|
||||
self.cache
|
||||
.remove(id)
|
||||
.map_err(|e| super::Error::CacheRemove {
|
||||
id: *id,
|
||||
err: e.into(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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,
|
||||
C: Update<Patch>,
|
||||
{
|
||||
let issue = self
|
||||
.store
|
||||
.get(id)?
|
||||
.ok_or_else(|| store::Error::NotFound((*super::TYPENAME).clone(), *id))?;
|
||||
self.update(&self.rid(), id, &issue)
|
||||
.map_err(|e| super::Error::CacheUpdate {
|
||||
id: *id,
|
||||
err: e.into(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read all the patches from the [`super::Patches`] store and
|
||||
/// writing them to `cache`.
|
||||
///
|
||||
/// The `callback` is used for reporting success, failures, and
|
||||
/// progress to the caller. The caller may also decide to continue
|
||||
/// or break from the process.
|
||||
pub fn write_all(
|
||||
&mut self,
|
||||
callback: impl Fn(
|
||||
&Result<(PatchId, Patch), store::Error>,
|
||||
&cache::WriteAllProgress,
|
||||
) -> ControlFlow<()>,
|
||||
) -> Result<(), super::Error>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
C: Update<Patch>,
|
||||
{
|
||||
let patches = self.store.all()?;
|
||||
let mut progress = cache::WriteAllProgress::new(patches.len());
|
||||
for patch in self.store.all()? {
|
||||
progress.inc();
|
||||
match callback(&patch, &progress) {
|
||||
ControlFlow::Continue(()) => match patch {
|
||||
Ok((id, patch)) => {
|
||||
self.update(&self.rid(), &id, &patch)
|
||||
.map_err(|e| super::Error::CacheUpdate { id, err: e.into() })?;
|
||||
}
|
||||
Err(_) => continue,
|
||||
},
|
||||
ControlFlow::Break(()) => break,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Cache<R, StoreReader> {
|
||||
pub fn reader(store: R, cache: StoreReader) -> Self {
|
||||
Self { store, cache }
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Cache<R, StoreWriter> {
|
||||
pub fn open(store: R, cache: StoreWriter) -> Self {
|
||||
Self { store, cache }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R> Cache<super::Patches<'a, R>, StoreWriter>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
/// 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> {
|
||||
let patch = Patches::get(self, id)?
|
||||
.ok_or_else(move || Error::NotFound(super::TYPENAME.clone(), *id))?;
|
||||
|
||||
Ok(PatchMut {
|
||||
id: *id,
|
||||
patch,
|
||||
store: &mut self.store,
|
||||
cache: &mut self.cache,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R> Cache<super::Patches<'a, R>, cache::NoCache>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
/// 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)?;
|
||||
Ok(Self {
|
||||
store,
|
||||
cache: cache::NoCache,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the [`PatchMut`], identified by `id`.
|
||||
pub fn get_mut<'g>(
|
||||
&'g mut self,
|
||||
id: &ObjectId,
|
||||
) -> Result<PatchMut<'a, 'g, R, cache::NoCache>, super::Error> {
|
||||
let patch = self
|
||||
.store
|
||||
.get(id)?
|
||||
.ok_or_else(move || store::Error::NotFound(super::TYPENAME.clone(), *id))?;
|
||||
|
||||
Ok(PatchMut {
|
||||
id: *id,
|
||||
patch,
|
||||
store: &mut self.store,
|
||||
cache: &mut self.cache,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, C> cache::Update<Patch> for Cache<R, C>
|
||||
where
|
||||
C: cache::Update<Patch>,
|
||||
{
|
||||
type Out = <C as cache::Update<Patch>>::Out;
|
||||
type UpdateError = <C as cache::Update<Patch>>::UpdateError;
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
rid: &RepoId,
|
||||
id: &radicle_cob::ObjectId,
|
||||
object: &Patch,
|
||||
) -> Result<Self::Out, Self::UpdateError> {
|
||||
self.cache.update(rid, id, object)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, C> cache::Remove<Patch> for Cache<R, C>
|
||||
where
|
||||
C: cache::Remove<Patch>,
|
||||
{
|
||||
type Out = <C as cache::Remove<Patch>>::Out;
|
||||
type RemoveError = <C as cache::Remove<Patch>>::RemoveError;
|
||||
|
||||
fn remove(&mut self, id: &ObjectId) -> Result<Self::Out, Self::RemoveError> {
|
||||
self.cache.remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UpdateError {
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Sql(#[from] sql::Error),
|
||||
}
|
||||
|
||||
impl Update<Patch> for StoreWriter {
|
||||
type Out = bool;
|
||||
type UpdateError = UpdateError;
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
rid: &RepoId,
|
||||
id: &ObjectId,
|
||||
object: &Patch,
|
||||
) -> Result<Self::Out, Self::UpdateError> {
|
||||
transaction::<_, UpdateError>(&self.db, move |db| {
|
||||
let mut stmt = db.prepare(
|
||||
"INSERT INTO patches (id, repo, patch)
|
||||
VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT DO UPDATE
|
||||
SET patch = (?3)",
|
||||
)?;
|
||||
|
||||
stmt.bind((1, sql::Value::String(id.to_string())))?;
|
||||
stmt.bind((2, rid))?;
|
||||
stmt.bind((3, sql::Value::String(serde_json::to_string(&object)?)))?;
|
||||
stmt.next()?;
|
||||
|
||||
Ok(db.change_count() > 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Remove<Patch> for StoreWriter {
|
||||
type Out = bool;
|
||||
type RemoveError = sql::Error;
|
||||
|
||||
fn remove(&mut self, id: &ObjectId) -> Result<Self::Out, Self::RemoveError> {
|
||||
transaction::<_, sql::Error>(&self.db, move |db| {
|
||||
let mut stmt = db.prepare(
|
||||
"DELETE FROM patches
|
||||
WHERE id = ?1",
|
||||
)?;
|
||||
|
||||
stmt.bind((1, sql::Value::String(id.to_string())))?;
|
||||
stmt.next()?;
|
||||
|
||||
Ok(db.change_count() > 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("object `{1}` of type `{0}` was not found")]
|
||||
NotFound(TypeName, ObjectId),
|
||||
#[error(transparent)]
|
||||
Object(#[from] cob::object::ParseObjectId),
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Sql(#[from] sql::Error),
|
||||
}
|
||||
|
||||
/// Iterator that returns a set of patches based on an SQL query.
|
||||
///
|
||||
/// The query is expected to return rows with columns identified by
|
||||
/// the `id` and `patch` names.
|
||||
pub struct PatchesIter<'a> {
|
||||
inner: sql::CursorWithOwnership<'a>,
|
||||
}
|
||||
|
||||
impl<'a> PatchesIter<'a> {
|
||||
fn parse_row(row: sql::Row) -> Result<(PatchId, Patch), Error> {
|
||||
let id = PatchId::from_str(row.read::<&str, _>("id"))?;
|
||||
let patch = serde_json::from_str::<Patch>(row.read::<&str, _>("patch"))?;
|
||||
Ok((id, patch))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PatchesIter<'a> {
|
||||
type Item = Result<(PatchId, Patch), Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let row = self.inner.next()?;
|
||||
Some(row.map_err(Error::from).and_then(PatchesIter::parse_row))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Patches for Cache<R, StoreReader>
|
||||
where
|
||||
R: HasRepoId,
|
||||
{
|
||||
type Error = Error;
|
||||
type Iter<'b> = PatchesIter<'b>
|
||||
where
|
||||
Self: 'b;
|
||||
|
||||
fn get(&self, id: &PatchId) -> Result<Option<Patch>, Self::Error> {
|
||||
query::get(&self.cache.db, &self.rid(), id)
|
||||
}
|
||||
|
||||
fn find_by_revision(&self, id: &RevisionId) -> Result<Option<ByRevision>, Error> {
|
||||
query::find_by_revision(&self.cache.db, &self.rid(), id)
|
||||
}
|
||||
|
||||
fn list(&self) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
query::list(&self.cache.db, &self.rid())
|
||||
}
|
||||
|
||||
fn list_by_status(&self, status: &Status) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
query::list_by_status(&self.cache.db, &self.rid(), status)
|
||||
}
|
||||
|
||||
fn counts(&self) -> Result<PatchCounts, Self::Error> {
|
||||
query::counts(&self.cache.db, &self.rid())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NoCacheIter<'a> {
|
||||
inner: Box<dyn Iterator<Item = Result<(PatchId, Patch), super::Error>> + 'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for NoCacheIter<'a> {
|
||||
type Item = Result<(PatchId, Patch), super::Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R> Patches for Cache<super::Patches<'a, R>, cache::NoCache>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
type Error = super::Error;
|
||||
type Iter<'b> = NoCacheIter<'b> where Self: 'b;
|
||||
|
||||
fn get(&self, id: &PatchId) -> Result<Option<Patch>, Self::Error> {
|
||||
self.store.get(id).map_err(super::Error::from)
|
||||
}
|
||||
|
||||
fn find_by_revision(&self, id: &RevisionId) -> Result<Option<ByRevision>, Self::Error> {
|
||||
self.store.find_by_revision(id)
|
||||
}
|
||||
|
||||
fn list(&self) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
self.store
|
||||
.all()
|
||||
.map(|inner| NoCacheIter {
|
||||
inner: Box::new(inner.into_iter().map(|res| res.map_err(super::Error::from))),
|
||||
})
|
||||
.map_err(super::Error::from)
|
||||
}
|
||||
|
||||
fn list_by_status(&self, status: &Status) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
let status = *status;
|
||||
self.store
|
||||
.all()
|
||||
.map(move |inner| NoCacheIter {
|
||||
inner: Box::new(inner.into_iter().filter_map(move |res| {
|
||||
match res {
|
||||
Ok((id, patch)) => (status == Status::from(&patch.state))
|
||||
.then_some((id, patch))
|
||||
.map(Ok),
|
||||
Err(e) => Some(Err(e.into())),
|
||||
}
|
||||
})),
|
||||
})
|
||||
.map_err(super::Error::from)
|
||||
}
|
||||
|
||||
fn counts(&self) -> Result<PatchCounts, Self::Error> {
|
||||
self.store.counts().map_err(super::Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Patches for Cache<R, StoreWriter>
|
||||
where
|
||||
R: HasRepoId,
|
||||
{
|
||||
type Error = Error;
|
||||
type Iter<'b> = PatchesIter<'b>
|
||||
where
|
||||
Self: 'b;
|
||||
|
||||
fn get(&self, id: &PatchId) -> Result<Option<Patch>, Self::Error> {
|
||||
query::get(&self.cache.db, &self.rid(), id)
|
||||
}
|
||||
|
||||
fn find_by_revision(&self, id: &RevisionId) -> Result<Option<ByRevision>, Error> {
|
||||
query::find_by_revision(&self.cache.db, &self.rid(), id)
|
||||
}
|
||||
|
||||
fn list(&self) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
query::list(&self.cache.db, &self.rid())
|
||||
}
|
||||
|
||||
fn list_by_status(&self, status: &Status) -> Result<Self::Iter<'_>, Self::Error> {
|
||||
query::list_by_status(&self.cache.db, &self.rid(), status)
|
||||
}
|
||||
|
||||
fn counts(&self) -> Result<PatchCounts, Self::Error> {
|
||||
query::counts(&self.cache.db, &self.rid())
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper SQL queries for [ `Patches`] trait implementations.
|
||||
mod query {
|
||||
use sqlite as sql;
|
||||
|
||||
use crate::patch::Status;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(super) fn get(
|
||||
db: &sql::ConnectionThreadSafe,
|
||||
rid: &RepoId,
|
||||
id: &PatchId,
|
||||
) -> Result<Option<Patch>, Error> {
|
||||
let id = sql::Value::String(id.to_string());
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT patch
|
||||
FROM patches
|
||||
WHERE id = ?1 AND repo = ?2",
|
||||
)?;
|
||||
|
||||
stmt.bind((1, id))?;
|
||||
stmt.bind((2, rid))?;
|
||||
|
||||
match stmt.into_iter().next().transpose()? {
|
||||
None => Ok(None),
|
||||
Some(row) => {
|
||||
let patch = row.read::<&str, _>("patch");
|
||||
let patch = serde_json::from_str(patch)?;
|
||||
Ok(Some(patch))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn find_by_revision(
|
||||
db: &sql::ConnectionThreadSafe,
|
||||
rid: &RepoId,
|
||||
id: &RevisionId,
|
||||
) -> Result<Option<ByRevision>, Error> {
|
||||
let revision_id = *id;
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT patches.id, patch, revisions.value AS revision
|
||||
FROM patches, json_tree(patches.patch, '$.revisions') AS revisions
|
||||
WHERE repo = ?1
|
||||
AND revisions.key = ?2
|
||||
",
|
||||
)?;
|
||||
stmt.bind((1, rid))?;
|
||||
stmt.bind((2, sql::Value::String(id.to_string())))?;
|
||||
|
||||
match stmt.into_iter().next().transpose()? {
|
||||
None => Ok(None),
|
||||
Some(row) => {
|
||||
let id = PatchId::from_str(row.read::<&str, _>("id"))?;
|
||||
let patch = serde_json::from_str::<Patch>(row.read::<&str, _>("patch"))?;
|
||||
let revision = serde_json::from_str::<Revision>(row.read::<&str, _>("revision"))?;
|
||||
Ok(Some(ByRevision {
|
||||
id,
|
||||
patch,
|
||||
revision_id,
|
||||
revision,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn list<'a>(
|
||||
db: &'a sql::ConnectionThreadSafe,
|
||||
rid: &RepoId,
|
||||
) -> Result<PatchesIter<'a>, Error> {
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT id, patch
|
||||
FROM patches
|
||||
WHERE repo = ?1
|
||||
ORDER BY id
|
||||
",
|
||||
)?;
|
||||
stmt.bind((1, rid))?;
|
||||
Ok(PatchesIter {
|
||||
inner: stmt.into_iter(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn list_by_status<'a>(
|
||||
db: &'a sql::ConnectionThreadSafe,
|
||||
rid: &RepoId,
|
||||
filter: &Status,
|
||||
) -> Result<PatchesIter<'a>, Error> {
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT patches.id, patch
|
||||
FROM patches
|
||||
WHERE repo = ?1
|
||||
AND patch->>'$.state.status' = ?2
|
||||
ORDER BY id
|
||||
",
|
||||
)?;
|
||||
stmt.bind((1, rid))?;
|
||||
stmt.bind((2, sql::Value::String(filter.to_string())))?;
|
||||
Ok(PatchesIter {
|
||||
inner: stmt.into_iter(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn counts(
|
||||
db: &sql::ConnectionThreadSafe,
|
||||
rid: &RepoId,
|
||||
) -> Result<PatchCounts, Error> {
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT
|
||||
patch->'$.state' AS state,
|
||||
COUNT(*) AS count
|
||||
FROM patches
|
||||
WHERE repo = ?1
|
||||
GROUP BY patch->'$.state.status'",
|
||||
)?;
|
||||
stmt.bind((1, rid))?;
|
||||
|
||||
stmt.into_iter()
|
||||
.try_fold(PatchCounts::default(), |mut counts, row| {
|
||||
let row = row?;
|
||||
let count = row.read::<i64, _>("count") as usize;
|
||||
let status = serde_json::from_str::<State>(row.read::<&str, _>("state"))?;
|
||||
match status {
|
||||
State::Draft => counts.draft += count,
|
||||
State::Open { .. } => counts.open += count,
|
||||
State::Archived => counts.archived += count,
|
||||
State::Merged { .. } => counts.merged += count,
|
||||
}
|
||||
Ok(counts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::num::NonZeroU8;
|
||||
use std::str::FromStr;
|
||||
|
||||
use amplify::Wrapper;
|
||||
use radicle_cob::ObjectId;
|
||||
|
||||
use crate::cob::cache::{Store, Update, Write};
|
||||
use crate::cob::thread::{Comment, Thread};
|
||||
use crate::cob::{Author, Timestamp};
|
||||
use crate::patch::{
|
||||
ByRevision, MergeTarget, Patch, PatchCounts, PatchId, Revision, RevisionId, State, Status,
|
||||
};
|
||||
use crate::prelude::Did;
|
||||
use crate::test::arbitrary;
|
||||
use crate::test::storage::MockRepository;
|
||||
|
||||
use super::{Cache, Patches};
|
||||
|
||||
fn memory(store: MockRepository) -> Cache<MockRepository, Store<Write>> {
|
||||
let cache = Store::<Write>::memory().unwrap();
|
||||
Cache { store, cache }
|
||||
}
|
||||
|
||||
fn revision() -> (RevisionId, Revision) {
|
||||
let author = arbitrary::gen::<Did>(1);
|
||||
let description = arbitrary::gen::<String>(1);
|
||||
let base = arbitrary::oid();
|
||||
let oid = arbitrary::oid();
|
||||
let timestamp = Timestamp::now();
|
||||
let resolves = BTreeSet::new();
|
||||
let mut revision = Revision::new(
|
||||
Author { id: author },
|
||||
description,
|
||||
base,
|
||||
oid,
|
||||
timestamp,
|
||||
resolves,
|
||||
);
|
||||
let comment = Comment::new(
|
||||
*author,
|
||||
"#1 comment".to_string(),
|
||||
None,
|
||||
None,
|
||||
vec![],
|
||||
Timestamp::now(),
|
||||
);
|
||||
let thread = Thread::new(arbitrary::oid(), comment);
|
||||
revision.discussion = thread;
|
||||
let id = RevisionId::from(arbitrary::oid());
|
||||
(id, revision)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_empty() {
|
||||
let repo = arbitrary::gen::<MockRepository>(1);
|
||||
let mut cache = memory(repo);
|
||||
assert!(cache.is_empty().unwrap());
|
||||
|
||||
let patch = Patch::new("Patch #1".to_string(), MergeTarget::Delegates, revision());
|
||||
let id = ObjectId::from_str("47799cbab2eca047b6520b9fce805da42b49ecab").unwrap();
|
||||
cache.update(&cache.rid(), &id, &patch).unwrap();
|
||||
|
||||
let patch = Patch {
|
||||
state: State::Archived,
|
||||
..Patch::new("Patch #2".to_string(), MergeTarget::Delegates, revision())
|
||||
};
|
||||
let id = ObjectId::from_str("ae981ded6ed2ed2cdba34c8603714782667f18a3").unwrap();
|
||||
cache.update(&cache.rid(), &id, &patch).unwrap();
|
||||
|
||||
assert!(!cache.is_empty().unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_counts() {
|
||||
let repo = arbitrary::gen::<MockRepository>(1);
|
||||
let mut cache = memory(repo);
|
||||
let n_open = arbitrary::gen::<u8>(0);
|
||||
let n_draft = arbitrary::gen::<u8>(1);
|
||||
let n_archived = arbitrary::gen::<u8>(1);
|
||||
let n_merged = arbitrary::gen::<u8>(1);
|
||||
let open_ids = (0..n_open)
|
||||
.map(|_| PatchId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<PatchId>>();
|
||||
let draft_ids = (0..n_draft)
|
||||
.map(|_| PatchId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<PatchId>>();
|
||||
let archived_ids = (0..n_archived)
|
||||
.map(|_| PatchId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<PatchId>>();
|
||||
let merged_ids = (0..n_merged)
|
||||
.map(|_| PatchId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<PatchId>>();
|
||||
|
||||
for id in open_ids.iter() {
|
||||
let patch = Patch::new(id.to_string(), MergeTarget::Delegates, revision());
|
||||
cache
|
||||
.update(&cache.rid(), &PatchId::from(*id), &patch)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
for id in draft_ids.iter() {
|
||||
let patch = Patch {
|
||||
state: State::Draft,
|
||||
..Patch::new(id.to_string(), MergeTarget::Delegates, revision())
|
||||
};
|
||||
cache
|
||||
.update(&cache.rid(), &PatchId::from(*id), &patch)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
for id in archived_ids.iter() {
|
||||
let patch = Patch {
|
||||
state: State::Archived,
|
||||
..Patch::new(id.to_string(), MergeTarget::Delegates, revision())
|
||||
};
|
||||
cache
|
||||
.update(&cache.rid(), &PatchId::from(*id), &patch)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
for id in merged_ids.iter() {
|
||||
let patch = Patch {
|
||||
state: State::Merged {
|
||||
revision: arbitrary::oid().into(),
|
||||
commit: arbitrary::oid(),
|
||||
},
|
||||
..Patch::new(id.to_string(), MergeTarget::Delegates, revision())
|
||||
};
|
||||
cache
|
||||
.update(&cache.rid(), &PatchId::from(*id), &patch)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
cache.counts().unwrap(),
|
||||
PatchCounts {
|
||||
open: open_ids.len(),
|
||||
draft: draft_ids.len(),
|
||||
archived: archived_ids.len(),
|
||||
merged: merged_ids.len(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get() {
|
||||
let repo = arbitrary::gen::<MockRepository>(1);
|
||||
let mut cache = memory(repo);
|
||||
let ids = (0..arbitrary::gen::<u8>(1))
|
||||
.map(|_| PatchId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<PatchId>>();
|
||||
let missing = (0..arbitrary::gen::<u8>(2))
|
||||
.filter_map(|_| {
|
||||
let id = PatchId::from(arbitrary::oid());
|
||||
(!ids.contains(&id)).then_some(id)
|
||||
})
|
||||
.collect::<BTreeSet<PatchId>>();
|
||||
let mut patches = Vec::with_capacity(ids.len());
|
||||
|
||||
for id in ids.iter() {
|
||||
let patch = Patch::new(id.to_string(), MergeTarget::Delegates, revision());
|
||||
cache
|
||||
.update(&cache.rid(), &PatchId::from(*id), &patch)
|
||||
.unwrap();
|
||||
patches.push((*id, patch));
|
||||
}
|
||||
|
||||
for (id, patch) in patches.into_iter() {
|
||||
assert_eq!(Some(patch), cache.get(&id).unwrap());
|
||||
}
|
||||
|
||||
for id in &missing {
|
||||
assert_eq!(cache.get(id).unwrap(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_by_revision() {
|
||||
let repo = arbitrary::gen::<MockRepository>(1);
|
||||
let mut cache = memory(repo);
|
||||
let patch_id = PatchId::from(arbitrary::oid());
|
||||
let revisions = (0..arbitrary::gen::<NonZeroU8>(1).into())
|
||||
.map(|_| revision())
|
||||
.collect::<BTreeMap<RevisionId, Revision>>();
|
||||
let (rev_id, rev) = revisions
|
||||
.iter()
|
||||
.next()
|
||||
.expect("at least one revision should have been created");
|
||||
let mut patch = Patch::new(
|
||||
patch_id.to_string(),
|
||||
MergeTarget::Delegates,
|
||||
(*rev_id, rev.clone()),
|
||||
);
|
||||
let timeline = revisions.keys().copied().collect::<Vec<_>>();
|
||||
patch
|
||||
.timeline
|
||||
.extend(timeline.iter().map(|id| id.into_inner()));
|
||||
patch
|
||||
.revisions
|
||||
.extend(revisions.iter().map(|(id, rev)| (*id, Some(rev.clone()))));
|
||||
cache
|
||||
.update(&cache.rid(), &PatchId::from(*patch_id), &patch)
|
||||
.unwrap();
|
||||
|
||||
for entry in timeline {
|
||||
let rev = revisions.get(&entry).unwrap().clone();
|
||||
assert_eq!(
|
||||
Some(ByRevision {
|
||||
id: patch_id,
|
||||
patch: patch.clone(),
|
||||
revision_id: entry,
|
||||
revision: rev
|
||||
}),
|
||||
cache.find_by_revision(&entry).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
let repo = arbitrary::gen::<MockRepository>(1);
|
||||
let mut cache = memory(repo);
|
||||
let ids = (0..arbitrary::gen::<u8>(1))
|
||||
.map(|_| PatchId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<PatchId>>();
|
||||
let mut patches = Vec::with_capacity(ids.len());
|
||||
|
||||
for id in ids.iter() {
|
||||
let patch = Patch::new(id.to_string(), MergeTarget::Delegates, revision());
|
||||
cache
|
||||
.update(&cache.rid(), &PatchId::from(*id), &patch)
|
||||
.unwrap();
|
||||
patches.push((*id, patch));
|
||||
}
|
||||
|
||||
let mut list = cache
|
||||
.list()
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
list.sort_by_key(|(id, _)| *id);
|
||||
patches.sort_by_key(|(id, _)| *id);
|
||||
assert_eq!(patches, list);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_by_status() {
|
||||
let repo = arbitrary::gen::<MockRepository>(1);
|
||||
let mut cache = memory(repo);
|
||||
let ids = (0..arbitrary::gen::<u8>(1))
|
||||
.map(|_| PatchId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<PatchId>>();
|
||||
let mut patches = Vec::with_capacity(ids.len());
|
||||
|
||||
for id in ids.iter() {
|
||||
let patch = Patch::new(id.to_string(), MergeTarget::Delegates, revision());
|
||||
cache
|
||||
.update(&cache.rid(), &PatchId::from(*id), &patch)
|
||||
.unwrap();
|
||||
patches.push((*id, patch));
|
||||
}
|
||||
|
||||
let mut list = cache
|
||||
.list_by_status(&Status::Open)
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
list.sort_by_key(|(id, _)| *id);
|
||||
patches.sort_by_key(|(id, _)| *id);
|
||||
assert_eq!(patches, list);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove() {
|
||||
let repo = arbitrary::gen::<MockRepository>(1);
|
||||
let mut cache = memory(repo);
|
||||
let ids = (0..arbitrary::gen::<u8>(1))
|
||||
.map(|_| PatchId::from(arbitrary::oid()))
|
||||
.collect::<BTreeSet<PatchId>>();
|
||||
|
||||
for id in ids.iter() {
|
||||
let patch = Patch::new(id.to_string(), MergeTarget::Delegates, revision());
|
||||
cache
|
||||
.update(&cache.rid(), &PatchId::from(*id), &patch)
|
||||
.unwrap();
|
||||
assert_eq!(Some(patch), cache.get(id).unwrap());
|
||||
super::Remove::remove(&mut cache, id).unwrap();
|
||||
assert_eq!(None, cache.get(id).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ use std::fmt::Debug;
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use radicle_cob::CollaborativeObject;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::cob::op::Op;
|
||||
|
|
@ -112,7 +113,10 @@ impl<'a, T, R> AsRef<R> for Store<'a, T, R> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T, R: ReadRepository + cob::Store> Store<'a, T, R> {
|
||||
impl<'a, T, R> Store<'a, T, R>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
/// Open a new generic store.
|
||||
pub fn open(repo: &'a R) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
|
|
@ -234,7 +238,9 @@ where
|
|||
}
|
||||
|
||||
/// Return all objects.
|
||||
pub fn all(&self) -> Result<impl Iterator<Item = Result<(ObjectId, T), Error>> + 'a, Error> {
|
||||
pub fn all(
|
||||
&self,
|
||||
) -> Result<impl ExactSizeIterator<Item = Result<(ObjectId, T), Error>> + 'a, Error> {
|
||||
let raw = cob::list::<T, _>(self.repo, T::type_name())?;
|
||||
|
||||
Ok(raw.into_iter().map(|o| Ok((*o.id(), o.object))))
|
||||
|
|
@ -271,7 +277,10 @@ impl<T: Cob + cob::Evaluate<R>, R> Default for Transaction<T, R> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Cob + cob::Evaluate<R>, R> Transaction<T, R> {
|
||||
impl<T, R> Transaction<T, R>
|
||||
where
|
||||
T: Cob + cob::Evaluate<R>,
|
||||
{
|
||||
/// Create a new transaction to be used as the initial set of operations for a COB.
|
||||
pub fn initial<G, F>(
|
||||
message: &str,
|
||||
|
|
@ -324,9 +333,13 @@ impl<T: Cob + cob::Evaluate<R>, R> Transaction<T, R> {
|
|||
{
|
||||
let actions = NonEmpty::from_vec(self.actions)
|
||||
.expect("Transaction::commit: transaction must not be empty");
|
||||
let Updated { head, object, .. } = store.update(id, msg, actions, self.embeds, signer)?;
|
||||
let Updated {
|
||||
head,
|
||||
object: CollaborativeObject { object, .. },
|
||||
..
|
||||
} = store.update(id, msg, actions, self.embeds, signer)?;
|
||||
|
||||
Ok((object.object, head))
|
||||
Ok((object, head))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ use crate::prelude::Did;
|
|||
use crate::prelude::NodeId;
|
||||
use crate::storage::git::transport;
|
||||
use crate::storage::git::Storage;
|
||||
use crate::{cli, git, node, web};
|
||||
use crate::storage::{self, ReadRepository};
|
||||
use crate::{cli, cob, git, node, web};
|
||||
|
||||
/// Environment variables used by radicle.
|
||||
pub mod env {
|
||||
|
|
@ -104,6 +105,10 @@ pub enum Error {
|
|||
NotificationsStore(#[from] node::notifications::store::Error),
|
||||
#[error(transparent)]
|
||||
DatabaseStore(#[from] node::db::Error),
|
||||
#[error(transparent)]
|
||||
Repository(#[from] storage::RepositoryError),
|
||||
#[error(transparent)]
|
||||
CobsCache(#[from] cob::cache::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
@ -410,7 +415,7 @@ impl Home {
|
|||
path: path.canonicalize()?,
|
||||
};
|
||||
|
||||
for dir in &[home.storage(), home.keys(), home.node()] {
|
||||
for dir in &[home.storage(), home.keys(), home.node(), home.cobs()] {
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(dir)?;
|
||||
}
|
||||
|
|
@ -439,6 +444,10 @@ impl Home {
|
|||
self.path.join("node")
|
||||
}
|
||||
|
||||
pub fn cobs(&self) -> PathBuf {
|
||||
self.path.join("cobs")
|
||||
}
|
||||
|
||||
pub fn socket(&self) -> PathBuf {
|
||||
env::var_os(env::RAD_SOCKET)
|
||||
.map(PathBuf::from)
|
||||
|
|
@ -478,6 +487,62 @@ impl Home {
|
|||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
/// Return a read-only handle for the issues cache.
|
||||
pub fn issues<'a, R>(
|
||||
&self,
|
||||
repository: &'a R,
|
||||
) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreReader>, Error>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
let path = self.cobs().join(cob::cache::COBS_DB_FILE);
|
||||
let db = cob::cache::Store::reader(path)?;
|
||||
let store = cob::issue::Issues::open(repository)?;
|
||||
Ok(cob::issue::Cache::reader(store, db))
|
||||
}
|
||||
|
||||
/// Return a read-write handle for the issues cache.
|
||||
pub fn issues_mut<'a, R>(
|
||||
&self,
|
||||
repository: &'a R,
|
||||
) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreWriter>, Error>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
let path = self.cobs().join(cob::cache::COBS_DB_FILE);
|
||||
let db = cob::cache::Store::open(path)?;
|
||||
let store = cob::issue::Issues::open(repository)?;
|
||||
Ok(cob::issue::Cache::open(store, db))
|
||||
}
|
||||
|
||||
/// Return a read-only handle for the patches cache.
|
||||
pub fn patches<'a, R>(
|
||||
&self,
|
||||
repository: &'a R,
|
||||
) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreReader>, Error>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
let path = self.cobs().join(cob::cache::COBS_DB_FILE);
|
||||
let db = cob::cache::Store::reader(path)?;
|
||||
let store = cob::patch::Patches::open(repository)?;
|
||||
Ok(cob::patch::Cache::reader(store, db))
|
||||
}
|
||||
|
||||
/// Return a read-write handle for the patches cache.
|
||||
pub fn patches_mut<'a, R>(
|
||||
&self,
|
||||
repository: &'a R,
|
||||
) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreWriter>, Error>
|
||||
where
|
||||
R: ReadRepository + cob::Store,
|
||||
{
|
||||
let path = self.cobs().join(cob::cache::COBS_DB_FILE);
|
||||
let db = cob::cache::Store::open(path)?;
|
||||
let store = cob::patch::Patches::open(repository)?;
|
||||
Ok(cob::patch::Cache::open(store, db))
|
||||
}
|
||||
}
|
||||
|
||||
// Private methods.
|
||||
|
|
|
|||
|
|
@ -404,6 +404,17 @@ pub trait WriteStorage: ReadStorage {
|
|||
fn clean(&self, rid: RepoId) -> Result<Vec<RemoteId>, RepositoryError>;
|
||||
}
|
||||
|
||||
/// Anything can return the [`RepoId`] that it is associated with.
|
||||
pub trait HasRepoId {
|
||||
fn rid(&self) -> RepoId;
|
||||
}
|
||||
|
||||
impl<T: ReadRepository> HasRepoId for T {
|
||||
fn rid(&self) -> RepoId {
|
||||
ReadRepository::id(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows read-only access to a repository.
|
||||
pub trait ReadRepository: Sized + ValidateRepository {
|
||||
/// Return the repository id.
|
||||
|
|
|
|||
Loading…
Reference in New Issue