Implement a custom list_patches method to read directly from cache

This commit is contained in:
Sebastian Martinez
2025-02-03 14:25:39 +01:00
parent 038a21cb51
commit f34522b689
16 changed files with 783 additions and 595 deletions
+41 -11
View File
@@ -2,25 +2,55 @@ use radicle::cob;
use radicle::git;
use radicle::identity;
use radicle::patch;
use radicle::patch::{Action, TYPENAME};
use radicle_types as types;
use radicle_types::cobs;
use radicle_types::domain::patch::models;
use radicle_types::domain::patch::service::Service;
use radicle_types::domain::patch::traits::PatchService;
use radicle_types::error::Error;
use radicle_types::outbound::sqlite::Sqlite;
use radicle_types::traits::cobs::Cobs;
use radicle_types::traits::patch::Patches;
use radicle_types::traits::patch::PatchesMut;
use radicle_types::traits::Profile;
use crate::AppState;
#[tauri::command]
pub async fn list_patches(
ctx: tauri::State<'_, AppState>,
sqlite_service: tauri::State<'_, Service<Sqlite>>,
rid: identity::RepoId,
status: Option<types::cobs::query::PatchStatus>,
skip: Option<usize>,
take: Option<usize>,
) -> Result<types::cobs::PaginatedQuery<Vec<types::cobs::patch::Patch>>, Error> {
ctx.list_patches(rid, status, skip, take)
) -> Result<types::cobs::PaginatedQuery<Vec<models::patch::Patch>>, Error> {
let profile = ctx.profile();
let cursor = skip.unwrap_or(0);
let take = take.unwrap_or(20);
let aliases = profile.aliases();
let patches = match status {
None => sqlite_service.list(rid)?.collect::<Vec<_>>(),
Some(s) => sqlite_service
.list_by_status(rid, s.into())?
.collect::<Vec<_>>(),
};
let more = cursor + take < patches.len();
let patches = patches
.into_iter()
.map(|(id, patch)| models::patch::Patch::new(id, &patch, &aliases))
.skip(cursor)
.take(take)
.collect::<Vec<_>>();
Ok::<_, Error>(cobs::PaginatedQuery {
cursor,
more,
content: patches,
})
}
#[tauri::command]
@@ -28,7 +58,7 @@ pub fn patch_by_id(
ctx: tauri::State<AppState>,
rid: identity::RepoId,
id: git::Oid,
) -> Result<Option<types::cobs::patch::Patch>, Error> {
) -> Result<Option<models::patch::Patch>, Error> {
ctx.get_patch(rid, id)
}
@@ -37,7 +67,7 @@ pub fn revisions_by_patch(
ctx: tauri::State<AppState>,
rid: identity::RepoId,
id: git::Oid,
) -> Result<Option<Vec<types::cobs::patch::Revision>>, Error> {
) -> Result<Option<Vec<models::patch::Revision>>, Error> {
ctx.revisions_by_patch(rid, id)
}
@@ -47,7 +77,7 @@ pub fn revision_by_patch_and_id(
rid: identity::RepoId,
id: git::Oid,
revision_id: git::Oid,
) -> Result<Option<types::cobs::patch::Revision>, Error> {
) -> Result<Option<models::patch::Revision>, Error> {
ctx.revision_by_id(rid, id, revision_id)
}
@@ -86,7 +116,7 @@ pub fn edit_draft_review(
ctx: tauri::State<AppState>,
rid: identity::RepoId,
cob_id: git::Oid,
edit: types::cobs::patch::ReviewEdit,
edit: models::patch::ReviewEdit,
) -> Result<(), Error> {
ctx.edit_draft_review(rid, cob_id, edit)
}
@@ -97,7 +127,7 @@ pub fn get_draft_review(
rid: identity::RepoId,
cob_id: git::Oid,
revision_id: patch::RevisionId,
) -> Option<types::cobs::patch::Review> {
) -> Option<models::patch::Review> {
ctx.get_draft_review(rid, cob_id, revision_id)
}
@@ -106,9 +136,9 @@ pub fn edit_patch(
ctx: tauri::State<AppState>,
rid: identity::RepoId,
cob_id: git::Oid,
action: types::cobs::patch::Action,
opts: types::cobs::CobOptions,
) -> Result<types::cobs::patch::Patch, Error> {
action: models::patch::Action,
opts: cobs::CobOptions,
) -> Result<models::patch::Patch, Error> {
ctx.edit_patch(rid, cob_id, action, opts)
}
+6
View File
@@ -2,6 +2,7 @@ mod commands;
use tauri::{Emitter, Manager};
use radicle::cob::cache::COBS_DB_FILE;
use radicle::node::{Handle, NOTIFICATIONS_DB_FILE};
use radicle::Node;
@@ -39,6 +40,10 @@ pub fn run() {
)?;
let inbox_service = domain::inbox::service::Service::new(inbox_db);
let patch_db =
radicle_types::outbound::sqlite::Sqlite::reader(profile.cobs().join(COBS_DB_FILE))?;
let patch_service = domain::patch::service::Service::new(patch_db);
let events_handler = app.handle().clone();
let node_handler = app.handle().clone();
@@ -46,6 +51,7 @@ pub fn run() {
let node_status = node.clone();
app.manage(inbox_service);
app.manage(patch_service);
app.manage(AppState { profile });
tauri::async_runtime::spawn(async move {
-499
View File
@@ -1,500 +1 @@
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use radicle::node::AliasStore;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use radicle::cob;
use radicle::git;
use radicle::identity;
use radicle::patch;
use crate::cobs;
#[derive(Debug, TS, Serialize)]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
#[serde(rename_all = "camelCase")]
pub struct Patch {
id: String,
author: cobs::Author,
title: String,
#[ts(as = "String")]
base: git::Oid,
#[ts(as = "String")]
head: git::Oid,
state: State,
assignees: Vec<cobs::Author>,
#[ts(as = "Vec<String>")]
labels: Vec<cob::Label>,
#[ts(type = "number")]
timestamp: cob::Timestamp,
revision_count: usize,
}
impl Patch {
pub fn new(id: patch::PatchId, patch: &patch::Patch, aliases: &impl AliasStore) -> Self {
Self {
id: id.to_string(),
author: cobs::Author::new(patch.author().id(), aliases),
title: patch.title().to_string(),
state: patch.state().clone().into(),
base: *patch.base(),
head: *patch.head(),
assignees: patch
.assignees()
.map(|did| cobs::Author::new(&did, aliases))
.collect::<Vec<_>>(),
labels: patch.labels().cloned().collect::<Vec<_>>(),
timestamp: patch.timestamp(),
revision_count: patch.revisions().count(),
}
}
pub fn timestamp(&self) -> u64 {
self.timestamp.as_millis()
}
}
#[derive(Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", tag = "status")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub enum State {
Draft,
Open {
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
#[ts(as = "Option<Vec<(String, String)>>", optional)]
conflicts: Vec<(patch::RevisionId, git::Oid)>,
},
Archived,
Merged {
#[ts(as = "String")]
revision: patch::RevisionId,
#[ts(as = "String")]
commit: git::Oid,
},
}
impl From<State> for patch::State {
fn from(value: State) -> Self {
match value {
State::Archived => Self::Archived,
State::Draft => Self::Draft,
State::Merged { revision, commit } => Self::Merged { revision, commit },
State::Open { conflicts } => Self::Open { conflicts },
}
}
}
impl From<patch::State> for State {
fn from(value: patch::State) -> Self {
match value {
patch::State::Archived => Self::Archived,
patch::State::Draft => Self::Draft,
patch::State::Merged { revision, commit } => Self::Merged { revision, commit },
patch::State::Open { conflicts } => Self::Open { conflicts },
}
}
}
#[derive(Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub struct ReviewEdit {
#[ts(as = "String")]
pub review_id: cob::patch::ReviewId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub verdict: Option<Verdict>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub summary: Option<String>,
#[ts(as = "Option<Vec<String>>", optional)]
pub labels: Vec<cob::Label>,
}
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub struct Revision {
#[ts(as = "String")]
id: patch::RevisionId,
author: cobs::Author,
description: Vec<Edit>,
#[ts(as = "String")]
base: git::Oid,
#[ts(as = "String")]
head: git::Oid,
#[ts(as = "Option<_>", optional)]
reviews: Vec<Review>,
#[ts(type = "number")]
timestamp: cob::common::Timestamp,
#[ts(as = "Option<_>", optional)]
discussion: Vec<cobs::thread::Comment<cobs::thread::CodeLocation>>,
#[ts(as = "Option<_>", optional)]
reactions: Vec<cobs::thread::Reaction>,
}
impl Revision {
pub fn new(value: cob::patch::Revision, aliases: &impl AliasStore) -> Self {
Self {
id: value.id(),
author: cobs::Author::new(value.author().id(), aliases),
description: value
.edits()
.map(|e| Edit::new(e, aliases))
.collect::<Vec<_>>(),
base: *value.base(),
head: value.head(),
reviews: value
.reviews()
.map(|(_, r)| Review::new(r.clone(), aliases))
.collect::<Vec<_>>(),
timestamp: value.timestamp(),
discussion: value
.discussion()
.comments()
.map(|(id, c)| {
cobs::thread::Comment::<cobs::thread::CodeLocation>::new(
*id,
c.clone(),
aliases,
)
})
.collect::<Vec<_>>(),
reactions: value
.reactions()
.iter()
.flat_map(|(location, reactions)| {
let reaction_by_author = reactions.iter().fold(
BTreeMap::new(),
|mut acc: BTreeMap<&cob::Reaction, Vec<_>>, (author, emoji)| {
acc.entry(emoji).or_default().push(author);
acc
},
);
reaction_by_author
.into_iter()
.map(|(emoji, authors)| {
cobs::thread::Reaction::new(
*emoji,
authors.into_iter().map(Into::into).collect::<Vec<_>>(),
location
.as_ref()
.map(|l| cobs::thread::CodeLocation::new(l.clone())),
aliases,
)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>(),
}
}
}
#[derive(TS, Serialize)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub struct Edit {
pub author: cobs::Author,
#[ts(type = "number")]
pub timestamp: cob::common::Timestamp,
pub body: String,
#[ts(as = "Option<_>", optional)]
pub embeds: Vec<cobs::thread::Embed>,
}
impl Edit {
pub fn new(edit: &cob::thread::Edit, aliases: &impl AliasStore) -> Self {
Self {
author: cobs::Author::new(&edit.author.into(), aliases),
timestamp: edit.timestamp,
body: edit.body.clone(),
embeds: edit
.embeds
.iter()
.cloned()
.map(|e| e.into())
.collect::<Vec<_>>(),
}
}
}
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub struct Review {
#[ts(as = "String")]
id: cob::patch::ReviewId,
author: cobs::Author,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
verdict: Option<Verdict>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
summary: Option<String>,
comments: Vec<cobs::thread::Comment<cobs::thread::CodeLocation>>,
#[ts(type = "number")]
timestamp: cob::common::Timestamp,
}
impl Review {
pub fn new(review: cob::patch::Review, aliases: &impl AliasStore) -> Self {
Self {
id: review.id(),
author: cobs::Author::new(&review.author().id, aliases),
verdict: review.verdict().map(|v| v.into()),
summary: review.summary().map(|s| s.to_string()),
comments: review
.comments()
.map(|(id, c)| {
cobs::thread::Comment::<cobs::thread::CodeLocation>::new(
*id,
c.clone(),
aliases,
)
})
.collect::<Vec<_>>(),
timestamp: review.timestamp(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub enum Verdict {
Accept,
Reject,
}
impl From<cob::patch::Verdict> for Verdict {
fn from(value: cob::patch::Verdict) -> Self {
match value {
cob::patch::Verdict::Accept => Self::Accept,
cob::patch::Verdict::Reject => Self::Reject,
}
}
}
impl From<Verdict> for cob::patch::Verdict {
fn from(value: Verdict) -> Self {
match value {
Verdict::Accept => Self::Accept,
Verdict::Reject => Self::Reject,
}
}
}
#[derive(Debug, Serialize, Deserialize, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub enum Action {
#[serde(rename = "edit")]
Edit {
title: String,
#[ts(as = "String")]
target: patch::MergeTarget,
},
#[serde(rename = "label")]
Label {
#[ts(as = "Vec<String>")]
labels: BTreeSet<cob::Label>,
},
#[serde(rename = "lifecycle")]
Lifecycle {
#[ts(type = "{ status: 'draft' | 'open' | 'archived' }")]
state: patch::Lifecycle,
},
#[serde(rename = "assign")]
Assign {
#[ts(as = "Vec<String>")]
assignees: BTreeSet<identity::Did>,
},
#[serde(rename = "merge")]
Merge {
#[ts(as = "String")]
revision: patch::RevisionId,
#[ts(as = "String")]
commit: git::Oid,
},
#[serde(rename = "review")]
Review {
#[ts(as = "String")]
revision: patch::RevisionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
verdict: Option<Verdict>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<Vec<String>>", optional)]
labels: Vec<cob::Label>,
},
#[serde(rename = "review.edit")]
ReviewEdit {
#[ts(as = "String")]
review: patch::ReviewId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
verdict: Option<Verdict>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<Vec<String>>", optional)]
labels: Vec<cob::Label>,
},
#[serde(rename = "review.redact")]
ReviewRedact {
#[ts(as = "String")]
review: patch::ReviewId,
},
#[serde(rename = "review.comment")]
ReviewComment {
#[ts(as = "String")]
review: patch::ReviewId,
body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
location: Option<cobs::thread::CodeLocation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(as = "Option<String>", optional)]
reply_to: Option<cob::thread::CommentId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<_>", optional)]
embeds: Vec<cobs::thread::Embed>,
},
#[serde(rename = "review.comment.edit")]
ReviewCommentEdit {
#[ts(as = "String")]
review: patch::ReviewId,
#[ts(as = "String")]
comment: cob::EntryId,
body: String,
#[ts(as = "Option<_>", optional)]
embeds: Vec<cobs::thread::Embed>,
},
#[serde(rename = "review.comment.redact")]
ReviewCommentRedact {
#[ts(as = "String")]
review: patch::ReviewId,
#[ts(as = "String")]
comment: cob::EntryId,
},
#[serde(rename = "review.comment.react")]
ReviewCommentReact {
#[ts(as = "String")]
review: patch::ReviewId,
#[ts(as = "String")]
comment: cob::EntryId,
#[ts(as = "String")]
reaction: cob::Reaction,
active: bool,
},
#[serde(rename = "review.comment.resolve")]
ReviewCommentResolve {
#[ts(as = "String")]
review: patch::ReviewId,
#[ts(as = "String")]
comment: cob::EntryId,
},
#[serde(rename = "review.comment.unresolve")]
ReviewCommentUnresolve {
#[ts(as = "String")]
review: patch::ReviewId,
#[ts(as = "String")]
comment: cob::EntryId,
},
#[serde(rename = "revision")]
Revision {
description: String,
#[ts(as = "String")]
base: git::Oid,
#[ts(as = "String")]
oid: git::Oid,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
#[ts(as = "Option<BTreeSet<(String, String)>>", optional)]
resolves: BTreeSet<(cob::EntryId, cob::thread::CommentId)>,
},
#[serde(rename = "revision.edit")]
RevisionEdit {
#[ts(as = "String")]
revision: patch::RevisionId,
description: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<_>", optional)]
embeds: Vec<cobs::thread::Embed>,
},
#[serde(rename = "revision.react")]
RevisionReact {
#[ts(as = "String")]
revision: patch::RevisionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
location: Option<cobs::thread::CodeLocation>,
#[ts(as = "String")]
reaction: cob::Reaction,
active: bool,
},
#[serde(rename = "revision.redact")]
RevisionRedact {
#[ts(as = "String")]
revision: patch::RevisionId,
},
#[serde(rename_all = "camelCase")]
#[serde(rename = "revision.comment")]
RevisionComment {
#[ts(as = "String")]
revision: patch::RevisionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
location: Option<cobs::thread::CodeLocation>,
body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(as = "Option<String>", optional)]
reply_to: Option<cob::thread::CommentId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<_>", optional)]
embeds: Vec<cobs::thread::Embed>,
},
#[serde(rename = "revision.comment.edit")]
RevisionCommentEdit {
#[ts(as = "String")]
revision: patch::RevisionId,
#[ts(as = "String")]
comment: cob::thread::CommentId,
body: String,
#[ts(as = "Option<_>", optional)]
embeds: Vec<cobs::thread::Embed>,
},
#[serde(rename = "revision.comment.redact")]
RevisionCommentRedact {
#[ts(as = "String")]
revision: patch::RevisionId,
#[ts(as = "String")]
comment: cob::thread::CommentId,
},
#[serde(rename = "revision.comment.react")]
RevisionCommentReact {
#[ts(as = "String")]
revision: patch::RevisionId,
#[ts(as = "String")]
comment: cob::thread::CommentId,
#[ts(as = "String")]
reaction: cob::Reaction,
active: bool,
},
}
+4 -3
View File
@@ -5,6 +5,7 @@ use radicle::node::AliasStore;
use radicle::{cob, git, identity};
use crate::cobs;
use crate::domain::patch::models;
#[derive(TS, Serialize, Deserialize)]
#[ts(export)]
@@ -39,7 +40,7 @@ pub struct Comment<T = cobs::Never> {
#[ts(as = "String")]
id: cob::thread::CommentId,
author: cobs::Author,
edits: Vec<cobs::patch::Edit>,
edits: Vec<models::patch::Edit>,
reactions: Vec<cobs::thread::Reaction>,
#[ts(as = "Option<String>", optional)]
reply_to: Option<cob::thread::CommentId>,
@@ -62,7 +63,7 @@ impl Comment<CodeLocation> {
author: cobs::Author::new(&comment.author().into(), aliases),
edits: comment
.edits()
.map(|e| cobs::patch::Edit::new(e, aliases))
.map(|e| models::patch::Edit::new(e, aliases))
.collect::<Vec<_>>(),
reactions: comment
.reactions()
@@ -100,7 +101,7 @@ impl Comment<cobs::Never> {
author: cobs::Author::new(&comment.author().into(), aliases),
edits: comment
.edits()
.map(|e| cobs::patch::Edit::new(e, aliases))
.map(|e| models::patch::Edit::new(e, aliases))
.collect::<Vec<_>>(),
reactions: comment
.reactions()
+1
View File
@@ -1 +1,2 @@
pub mod inbox;
pub mod patch;
@@ -9,6 +9,7 @@ use ts_rs::TS;
use crate::cobs::stream::{self, CobStream};
use crate::cobs::{self, Author};
use crate::domain::patch::models;
#[derive(Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -157,8 +158,8 @@ pub struct Patch {
#[ts(type = "number")]
pub timestamp: localtime::LocalTime,
pub title: String,
pub status: cobs::patch::State,
pub actions: Vec<ActionWithAuthor<cobs::patch::Action>>,
pub status: models::patch::State,
pub actions: Vec<ActionWithAuthor<models::patch::Action>>,
}
/// Type of notification.
+3
View File
@@ -0,0 +1,3 @@
pub mod models;
pub mod service;
pub mod traits;
@@ -0,0 +1 @@
pub mod patch;
@@ -0,0 +1,510 @@
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use radicle::node::AliasStore;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use radicle::cob;
use radicle::git;
use radicle::identity;
use radicle::patch;
use crate::cobs;
#[derive(Debug, TS, Serialize)]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
#[serde(rename_all = "camelCase")]
pub struct Patch {
id: String,
author: cobs::Author,
title: String,
#[ts(as = "String")]
base: git::Oid,
#[ts(as = "String")]
head: git::Oid,
state: State,
assignees: Vec<cobs::Author>,
#[ts(as = "Vec<String>")]
labels: Vec<cob::Label>,
#[ts(type = "number")]
timestamp: cob::Timestamp,
revision_count: usize,
}
#[derive(Debug, thiserror::Error)]
pub enum ListPatchesError {
#[error(transparent)]
Sqlite(#[from] sqlite::Error),
#[error(transparent)]
Unknown(#[from] anyhow::Error),
// to be extended as new error scenarios are introduced
}
impl Patch {
pub fn new(id: patch::PatchId, patch: &patch::Patch, aliases: &impl AliasStore) -> Self {
Self {
id: id.to_string(),
author: cobs::Author::new(patch.author().id(), aliases),
title: patch.title().to_string(),
state: patch.state().clone().into(),
base: *patch.base(),
head: *patch.head(),
assignees: patch
.assignees()
.map(|did| cobs::Author::new(&did, aliases))
.collect::<Vec<_>>(),
labels: patch.labels().cloned().collect::<Vec<_>>(),
timestamp: patch.timestamp(),
revision_count: patch.revisions().count(),
}
}
pub fn timestamp(&self) -> u64 {
self.timestamp.as_millis()
}
}
#[derive(Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", tag = "status")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub enum State {
Draft,
Open {
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
#[ts(as = "Option<Vec<(String, String)>>", optional)]
conflicts: Vec<(patch::RevisionId, git::Oid)>,
},
Archived,
Merged {
#[ts(as = "String")]
revision: patch::RevisionId,
#[ts(as = "String")]
commit: git::Oid,
},
}
impl From<State> for patch::State {
fn from(value: State) -> Self {
match value {
State::Archived => Self::Archived,
State::Draft => Self::Draft,
State::Merged { revision, commit } => Self::Merged { revision, commit },
State::Open { conflicts } => Self::Open { conflicts },
}
}
}
impl From<patch::State> for State {
fn from(value: patch::State) -> Self {
match value {
patch::State::Archived => Self::Archived,
patch::State::Draft => Self::Draft,
patch::State::Merged { revision, commit } => Self::Merged { revision, commit },
patch::State::Open { conflicts } => Self::Open { conflicts },
}
}
}
#[derive(Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub struct ReviewEdit {
#[ts(as = "String")]
pub review_id: cob::patch::ReviewId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub verdict: Option<Verdict>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub summary: Option<String>,
#[ts(as = "Option<Vec<String>>", optional)]
pub labels: Vec<cob::Label>,
}
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub struct Revision {
#[ts(as = "String")]
id: patch::RevisionId,
author: cobs::Author,
description: Vec<Edit>,
#[ts(as = "String")]
base: git::Oid,
#[ts(as = "String")]
head: git::Oid,
#[ts(as = "Option<_>", optional)]
reviews: Vec<Review>,
#[ts(type = "number")]
timestamp: cob::common::Timestamp,
#[ts(as = "Option<_>", optional)]
discussion: Vec<cobs::thread::Comment<cobs::thread::CodeLocation>>,
#[ts(as = "Option<_>", optional)]
reactions: Vec<cobs::thread::Reaction>,
}
impl Revision {
pub fn new(value: cob::patch::Revision, aliases: &impl AliasStore) -> Self {
Self {
id: value.id(),
author: cobs::Author::new(value.author().id(), aliases),
description: value
.edits()
.map(|e| Edit::new(e, aliases))
.collect::<Vec<_>>(),
base: *value.base(),
head: value.head(),
reviews: value
.reviews()
.map(|(_, r)| Review::new(r.clone(), aliases))
.collect::<Vec<_>>(),
timestamp: value.timestamp(),
discussion: value
.discussion()
.comments()
.map(|(id, c)| {
cobs::thread::Comment::<cobs::thread::CodeLocation>::new(
*id,
c.clone(),
aliases,
)
})
.collect::<Vec<_>>(),
reactions: value
.reactions()
.iter()
.flat_map(|(location, reactions)| {
let reaction_by_author = reactions.iter().fold(
BTreeMap::new(),
|mut acc: BTreeMap<&cob::Reaction, Vec<_>>, (author, emoji)| {
acc.entry(emoji).or_default().push(author);
acc
},
);
reaction_by_author
.into_iter()
.map(|(emoji, authors)| {
cobs::thread::Reaction::new(
*emoji,
authors.into_iter().map(Into::into).collect::<Vec<_>>(),
location
.as_ref()
.map(|l| cobs::thread::CodeLocation::new(l.clone())),
aliases,
)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>(),
}
}
}
#[derive(TS, Serialize)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub struct Edit {
pub author: cobs::Author,
#[ts(type = "number")]
pub timestamp: cob::common::Timestamp,
pub body: String,
#[ts(as = "Option<_>", optional)]
pub embeds: Vec<cobs::thread::Embed>,
}
impl Edit {
pub fn new(edit: &cob::thread::Edit, aliases: &impl AliasStore) -> Self {
Self {
author: cobs::Author::new(&edit.author.into(), aliases),
timestamp: edit.timestamp,
body: edit.body.clone(),
embeds: edit
.embeds
.iter()
.cloned()
.map(|e| e.into())
.collect::<Vec<_>>(),
}
}
}
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub struct Review {
#[ts(as = "String")]
id: cob::patch::ReviewId,
author: cobs::Author,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
verdict: Option<Verdict>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
summary: Option<String>,
comments: Vec<cobs::thread::Comment<cobs::thread::CodeLocation>>,
#[ts(type = "number")]
timestamp: cob::common::Timestamp,
}
impl Review {
pub fn new(review: cob::patch::Review, aliases: &impl AliasStore) -> Self {
Self {
id: review.id(),
author: cobs::Author::new(&review.author().id, aliases),
verdict: review.verdict().map(|v| v.into()),
summary: review.summary().map(|s| s.to_string()),
comments: review
.comments()
.map(|(id, c)| {
cobs::thread::Comment::<cobs::thread::CodeLocation>::new(
*id,
c.clone(),
aliases,
)
})
.collect::<Vec<_>>(),
timestamp: review.timestamp(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub enum Verdict {
Accept,
Reject,
}
impl From<cob::patch::Verdict> for Verdict {
fn from(value: cob::patch::Verdict) -> Self {
match value {
cob::patch::Verdict::Accept => Self::Accept,
cob::patch::Verdict::Reject => Self::Reject,
}
}
}
impl From<Verdict> for cob::patch::Verdict {
fn from(value: Verdict) -> Self {
match value {
Verdict::Accept => Self::Accept,
Verdict::Reject => Self::Reject,
}
}
}
#[derive(Debug, Serialize, Deserialize, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(export)]
#[ts(export_to = "cob/patch/")]
pub enum Action {
#[serde(rename = "edit")]
Edit {
title: String,
#[ts(as = "String")]
target: patch::MergeTarget,
},
#[serde(rename = "label")]
Label {
#[ts(as = "Vec<String>")]
labels: BTreeSet<cob::Label>,
},
#[serde(rename = "lifecycle")]
Lifecycle {
#[ts(type = "{ status: 'draft' | 'open' | 'archived' }")]
state: patch::Lifecycle,
},
#[serde(rename = "assign")]
Assign {
#[ts(as = "Vec<String>")]
assignees: BTreeSet<identity::Did>,
},
#[serde(rename = "merge")]
Merge {
#[ts(as = "String")]
revision: patch::RevisionId,
#[ts(as = "String")]
commit: git::Oid,
},
#[serde(rename = "review")]
Review {
#[ts(as = "String")]
revision: patch::RevisionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
verdict: Option<Verdict>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<Vec<String>>", optional)]
labels: Vec<cob::Label>,
},
#[serde(rename = "review.edit")]
ReviewEdit {
#[ts(as = "String")]
review: patch::ReviewId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
verdict: Option<Verdict>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<Vec<String>>", optional)]
labels: Vec<cob::Label>,
},
#[serde(rename = "review.redact")]
ReviewRedact {
#[ts(as = "String")]
review: patch::ReviewId,
},
#[serde(rename = "review.comment")]
ReviewComment {
#[ts(as = "String")]
review: patch::ReviewId,
body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
location: Option<cobs::thread::CodeLocation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(as = "Option<String>", optional)]
reply_to: Option<cob::thread::CommentId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<_>", optional)]
embeds: Vec<cobs::thread::Embed>,
},
#[serde(rename = "review.comment.edit")]
ReviewCommentEdit {
#[ts(as = "String")]
review: patch::ReviewId,
#[ts(as = "String")]
comment: cob::EntryId,
body: String,
#[ts(as = "Option<_>", optional)]
embeds: Vec<cobs::thread::Embed>,
},
#[serde(rename = "review.comment.redact")]
ReviewCommentRedact {
#[ts(as = "String")]
review: patch::ReviewId,
#[ts(as = "String")]
comment: cob::EntryId,
},
#[serde(rename = "review.comment.react")]
ReviewCommentReact {
#[ts(as = "String")]
review: patch::ReviewId,
#[ts(as = "String")]
comment: cob::EntryId,
#[ts(as = "String")]
reaction: cob::Reaction,
active: bool,
},
#[serde(rename = "review.comment.resolve")]
ReviewCommentResolve {
#[ts(as = "String")]
review: patch::ReviewId,
#[ts(as = "String")]
comment: cob::EntryId,
},
#[serde(rename = "review.comment.unresolve")]
ReviewCommentUnresolve {
#[ts(as = "String")]
review: patch::ReviewId,
#[ts(as = "String")]
comment: cob::EntryId,
},
#[serde(rename = "revision")]
Revision {
description: String,
#[ts(as = "String")]
base: git::Oid,
#[ts(as = "String")]
oid: git::Oid,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
#[ts(as = "Option<BTreeSet<(String, String)>>", optional)]
resolves: BTreeSet<(cob::EntryId, cob::thread::CommentId)>,
},
#[serde(rename = "revision.edit")]
RevisionEdit {
#[ts(as = "String")]
revision: patch::RevisionId,
description: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<_>", optional)]
embeds: Vec<cobs::thread::Embed>,
},
#[serde(rename = "revision.react")]
RevisionReact {
#[ts(as = "String")]
revision: patch::RevisionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
location: Option<cobs::thread::CodeLocation>,
#[ts(as = "String")]
reaction: cob::Reaction,
active: bool,
},
#[serde(rename = "revision.redact")]
RevisionRedact {
#[ts(as = "String")]
revision: patch::RevisionId,
},
#[serde(rename_all = "camelCase")]
#[serde(rename = "revision.comment")]
RevisionComment {
#[ts(as = "String")]
revision: patch::RevisionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
location: Option<cobs::thread::CodeLocation>,
body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(as = "Option<String>", optional)]
reply_to: Option<cob::thread::CommentId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(as = "Option<_>", optional)]
embeds: Vec<cobs::thread::Embed>,
},
#[serde(rename = "revision.comment.edit")]
RevisionCommentEdit {
#[ts(as = "String")]
revision: patch::RevisionId,
#[ts(as = "String")]
comment: cob::thread::CommentId,
body: String,
#[ts(as = "Option<_>", optional)]
embeds: Vec<cobs::thread::Embed>,
},
#[serde(rename = "revision.comment.redact")]
RevisionCommentRedact {
#[ts(as = "String")]
revision: patch::RevisionId,
#[ts(as = "String")]
comment: cob::thread::CommentId,
},
#[serde(rename = "revision.comment.react")]
RevisionCommentReact {
#[ts(as = "String")]
revision: patch::RevisionId,
#[ts(as = "String")]
comment: cob::thread::CommentId,
#[ts(as = "String")]
reaction: cob::Reaction,
active: bool,
},
}
@@ -0,0 +1,45 @@
use radicle::identity;
use radicle::patch;
use radicle::patch::Patch;
use radicle::patch::PatchId;
use crate::domain::patch::traits::{PatchService, PatchStorage};
#[derive(Debug, Clone)]
pub struct Service<I>
where
I: PatchStorage,
{
patches: I,
}
impl<I> Service<I>
where
I: PatchStorage,
{
pub fn new(patches: I) -> Self {
Self { patches }
}
}
impl<I> PatchService for Service<I>
where
I: PatchStorage,
{
fn list(
&self,
rid: identity::RepoId,
) -> Result<impl Iterator<Item = (PatchId, Patch)>, super::models::patch::ListPatchesError>
{
self.patches.list(rid)
}
fn list_by_status(
&self,
rid: identity::RepoId,
status: patch::Status,
) -> Result<impl Iterator<Item = (PatchId, Patch)>, super::models::patch::ListPatchesError>
{
self.patches.list_by_status(rid, status)
}
}
@@ -0,0 +1,32 @@
use radicle::identity;
use radicle::patch;
use radicle::patch::Patch;
use radicle::patch::PatchId;
use crate::domain::patch::models;
pub trait PatchStorage {
fn list(
&self,
rid: identity::RepoId,
) -> Result<impl Iterator<Item = (PatchId, Patch)>, models::patch::ListPatchesError>;
fn list_by_status(
&self,
rid: identity::RepoId,
status: patch::Status,
) -> Result<impl Iterator<Item = (PatchId, Patch)>, models::patch::ListPatchesError>;
}
pub trait PatchService {
fn list(
&self,
rid: identity::RepoId,
) -> Result<impl Iterator<Item = (PatchId, Patch)>, models::patch::ListPatchesError>;
fn list_by_status(
&self,
rid: identity::RepoId,
status: patch::Status,
) -> Result<impl Iterator<Item = (PatchId, Patch)>, models::patch::ListPatchesError>;
}
+4
View File
@@ -17,6 +17,10 @@ pub enum Error {
#[from] crate::domain::inbox::models::notification::ListNotificationsError,
),
/// CobStore error.
#[error(transparent)]
ListPatchesError(#[from] crate::domain::patch::models::patch::ListPatchesError),
/// CobStore error.
#[error(transparent)]
CobStore(#[from] radicle::cob::store::Error),
@@ -1,13 +1,17 @@
use std::collections::BTreeMap;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use std::time;
use radicle::patch::{Patch, PatchId, Status};
use radicle::{git, identity};
use sqlite as sql;
use crate::domain::inbox::models::notification;
use crate::domain::inbox::traits::InboxStorage;
use crate::domain::patch::models::patch::ListPatchesError;
use crate::domain::patch::traits::PatchStorage;
use crate::error::Error;
pub struct Sqlite {
@@ -29,6 +33,57 @@ impl Sqlite {
}
}
impl PatchStorage for Sqlite {
fn list(
&self,
rid: identity::RepoId,
) -> Result<impl Iterator<Item = (PatchId, Patch)>, ListPatchesError> {
let mut stmt = self.db.prepare(
"SELECT id, patch, (
SELECT MIN(JSON_EXTRACT(revision.value, '$.timestamp'))
FROM JSON_EACH(JSON_EXTRACT(p.patch, '$.revisions')) AS revision
) AS last_revision_timestamp
FROM patches AS p
WHERE repo = ?1
ORDER BY last_revision_timestamp DESC;
",
)?;
stmt.bind((1, &rid))?;
Ok(stmt.into_iter().filter_map(|row| {
let row = row.ok()?;
let id = PatchId::from_str(row.read::<&str, _>("id")).ok()?;
let patch = serde_json::from_str::<Patch>(row.read::<&str, _>("patch")).ok()?;
Some((id, patch))
}))
}
fn list_by_status(
&self,
rid: identity::RepoId,
status: Status,
) -> Result<impl Iterator<Item = (PatchId, Patch)>, ListPatchesError> {
let mut stmt = self.db.prepare(
"SELECT id, patch, (
SELECT MIN(JSON_EXTRACT(revision.value, '$.timestamp'))
FROM JSON_EACH(JSON_EXTRACT(p.patch, '$.revisions')) AS revision
) AS last_revision_timestamp
FROM patches AS p
WHERE repo = ?1
AND patch->>'$.state.status' = ?2
ORDER BY last_revision_timestamp DESC;
",
)?;
stmt.bind((1, &rid))?;
stmt.bind((2, sql::Value::String(status.to_string())))?;
Ok(stmt.into_iter().filter_map(|row| {
let row = row.ok()?;
let id = PatchId::from_str(row.read::<&str, _>("id")).ok()?;
let patch = serde_json::from_str::<Patch>(row.read::<&str, _>("patch")).ok()?;
Some((id, patch))
}))
}
}
impl InboxStorage for Sqlite {
fn counts_by_repo(
&self,
+35 -72
View File
@@ -5,59 +5,22 @@ use radicle::storage::ReadStorage;
use radicle::{cob, git, identity, patch, Node};
use crate::cobs;
use crate::domain::patch::models;
use crate::error::Error;
use crate::traits::Profile;
pub trait Patches: Profile {
fn list_patches(
&self,
rid: identity::RepoId,
status: Option<cobs::query::PatchStatus>,
skip: Option<usize>,
take: Option<usize>,
) -> Result<cobs::PaginatedQuery<Vec<cobs::patch::Patch>>, Error> {
let profile = self.profile();
let cursor = skip.unwrap_or(0);
let take = take.unwrap_or(20);
let repo = profile.storage.repository(rid)?;
let aliases = &profile.aliases();
let cache = profile.patches(&repo)?;
let patches = match status {
None => cache.list()?.collect::<Vec<_>>(),
Some(s) => cache.list_by_status(&s.into())?.collect::<Vec<_>>(),
};
let more = cursor + take < patches.len();
let mut patches = patches
.into_iter()
.filter_map(|p| {
p.map(|(id, patch)| cobs::patch::Patch::new(id, &patch, aliases))
.ok()
})
.skip(cursor)
.take(take)
.collect::<Vec<_>>();
patches.sort_by_key(|b| std::cmp::Reverse(b.timestamp()));
Ok::<_, Error>(cobs::PaginatedQuery {
cursor,
more,
content: patches,
})
}
fn get_patch(
&self,
rid: identity::RepoId,
id: git::Oid,
) -> Result<Option<cobs::patch::Patch>, Error> {
) -> Result<Option<models::patch::Patch>, Error> {
let profile = self.profile();
let repo = profile.storage.repository(rid)?;
let patches = profile.patches(&repo)?;
let patch = patches.get(&id.into())?;
let aliases = &profile.aliases();
let patches = patch.map(|patch| cobs::patch::Patch::new(id.into(), &patch, aliases));
let patches = patch.map(|patch| models::patch::Patch::new(id.into(), &patch, aliases));
Ok::<_, Error>(patches)
}
@@ -66,7 +29,7 @@ pub trait Patches: Profile {
&self,
rid: identity::RepoId,
id: git::Oid,
) -> Result<Option<Vec<cobs::patch::Revision>>, Error> {
) -> Result<Option<Vec<models::patch::Revision>>, Error> {
let profile = self.profile();
let repo = profile.storage.repository(rid)?;
let patches = profile.patches(&repo)?;
@@ -75,7 +38,7 @@ pub trait Patches: Profile {
patch
.revisions()
.map(|(_, r)| cobs::patch::Revision::new(r.clone(), aliases))
.map(|(_, r)| models::patch::Revision::new(r.clone(), aliases))
.collect::<Vec<_>>()
});
@@ -87,7 +50,7 @@ pub trait Patches: Profile {
rid: identity::RepoId,
id: git::Oid,
revision_id: git::Oid,
) -> Result<Option<cobs::patch::Revision>, Error> {
) -> Result<Option<models::patch::Revision>, Error> {
let profile = self.profile();
let repo = profile.storage.repository(rid)?;
let patches = profile.patches(&repo)?;
@@ -96,7 +59,7 @@ pub trait Patches: Profile {
patch
.revision(&revision_id.into())
.map(|r| cobs::patch::Revision::new(r.clone(), aliases))
.map(|r| models::patch::Revision::new(r.clone(), aliases))
});
Ok::<_, Error>(revision)
@@ -108,9 +71,9 @@ pub trait PatchesMut: Profile {
&self,
rid: identity::RepoId,
cob_id: git::Oid,
action: cobs::patch::Action,
action: models::patch::Action,
opts: cobs::CobOptions,
) -> Result<cobs::patch::Patch, Error> {
) -> Result<models::patch::Patch, Error> {
let profile = self.profile();
let mut node = Node::new(profile.socket());
let repo = profile.storage.repository(rid)?;
@@ -120,7 +83,7 @@ pub trait PatchesMut: Profile {
let mut patch = patches.get_mut(&cob_id.into())?;
match action {
cobs::patch::Action::RevisionEdit {
models::patch::Action::RevisionEdit {
revision,
description,
embeds,
@@ -132,13 +95,13 @@ pub trait PatchesMut: Profile {
&signer,
)?;
}
cobs::patch::Action::RevisionCommentRedact { revision, comment } => {
models::patch::Action::RevisionCommentRedact { revision, comment } => {
patch.comment_redact(revision, comment, &signer)?;
}
cobs::patch::Action::ReviewCommentRedact { review, comment } => {
models::patch::Action::ReviewCommentRedact { review, comment } => {
patch.redact_review_comment(review, comment, &signer)?;
}
cobs::patch::Action::ReviewCommentReact {
models::patch::Action::ReviewCommentReact {
review,
comment,
reaction,
@@ -146,16 +109,16 @@ pub trait PatchesMut: Profile {
} => {
patch.react_review_comment(review, comment, reaction, active, &signer)?;
}
cobs::patch::Action::ReviewCommentResolve { review, comment } => {
models::patch::Action::ReviewCommentResolve { review, comment } => {
patch.resolve_review_comment(review, comment, &signer)?;
}
cobs::patch::Action::ReviewCommentUnresolve { review, comment } => {
models::patch::Action::ReviewCommentUnresolve { review, comment } => {
patch.unresolve_review_comment(review, comment, &signer)?;
}
cobs::patch::Action::Edit { title, target } => {
models::patch::Action::Edit { title, target } => {
patch.edit(title, target, &signer)?;
}
cobs::patch::Action::ReviewEdit {
models::patch::Action::ReviewEdit {
review,
summary,
verdict,
@@ -163,7 +126,7 @@ pub trait PatchesMut: Profile {
} => {
patch.review_edit(review, verdict.map(|v| v.into()), summary, labels, &signer)?;
}
cobs::patch::Action::Review {
models::patch::Action::Review {
revision,
summary,
verdict,
@@ -177,10 +140,10 @@ pub trait PatchesMut: Profile {
&signer,
)?;
}
cobs::patch::Action::ReviewRedact { review } => {
models::patch::Action::ReviewRedact { review } => {
patch.redact_review(review, &signer)?;
}
cobs::patch::Action::ReviewComment {
models::patch::Action::ReviewComment {
review,
body,
location,
@@ -196,7 +159,7 @@ pub trait PatchesMut: Profile {
&signer,
)?;
}
cobs::patch::Action::ReviewCommentEdit {
models::patch::Action::ReviewCommentEdit {
review,
comment,
body,
@@ -210,16 +173,16 @@ pub trait PatchesMut: Profile {
&signer,
)?;
}
cobs::patch::Action::Lifecycle { state } => {
models::patch::Action::Lifecycle { state } => {
patch.lifecycle(state, &signer)?;
}
cobs::patch::Action::Assign { assignees } => {
models::patch::Action::Assign { assignees } => {
patch.assign(assignees, &signer)?;
}
cobs::patch::Action::Label { labels } => {
models::patch::Action::Label { labels } => {
patch.label(labels, &signer)?;
}
cobs::patch::Action::RevisionReact {
models::patch::Action::RevisionReact {
revision,
reaction,
location,
@@ -233,7 +196,7 @@ pub trait PatchesMut: Profile {
&signer,
)?;
}
cobs::patch::Action::RevisionComment {
models::patch::Action::RevisionComment {
revision,
location,
body,
@@ -249,7 +212,7 @@ pub trait PatchesMut: Profile {
&signer,
)?;
}
cobs::patch::Action::RevisionCommentEdit {
models::patch::Action::RevisionCommentEdit {
revision,
comment,
body,
@@ -263,7 +226,7 @@ pub trait PatchesMut: Profile {
&signer,
)?;
}
cobs::patch::Action::RevisionCommentReact {
models::patch::Action::RevisionCommentReact {
revision,
comment,
reaction,
@@ -271,13 +234,13 @@ pub trait PatchesMut: Profile {
} => {
patch.comment_react(revision, comment, reaction, active, &signer)?;
}
cobs::patch::Action::RevisionRedact { revision } => {
models::patch::Action::RevisionRedact { revision } => {
patch.redact(revision, &signer)?;
}
cobs::patch::Action::Merge { .. } => {
models::patch::Action::Merge { .. } => {
unimplemented!("We don't support merging of patches through the desktop")
}
cobs::patch::Action::Revision { .. } => {
models::patch::Action::Revision { .. } => {
unimplemented!("We don't support creating new revisions through the desktop")
}
}
@@ -288,7 +251,7 @@ pub trait PatchesMut: Profile {
}
}
Ok::<_, Error>(cobs::patch::Patch::new(*patch.id(), &patch, &aliases))
Ok::<_, Error>(models::patch::Patch::new(*patch.id(), &patch, &aliases))
}
/// Gets the draft review of the local user for a specific patch revision in a repository.
@@ -302,7 +265,7 @@ pub trait PatchesMut: Profile {
rid: identity::RepoId,
cob_id: git::Oid,
revision_id: patch::RevisionId,
) -> Option<cobs::patch::Review> {
) -> Option<models::patch::Review> {
let profile = self.profile();
let aliases = profile.aliases();
let repo = profile.storage.repository(rid).ok()?;
@@ -315,7 +278,7 @@ pub trait PatchesMut: Profile {
let review: Option<patch::Review> =
revision.and_then(|rev| rev.review_by(signer.public_key()).cloned());
review.map(|r| cobs::patch::Review::new(r, &aliases))
review.map(|r| models::patch::Review::new(r, &aliases))
}
/// Edits a draft review for a specific patch revision in a repository.
@@ -326,7 +289,7 @@ pub trait PatchesMut: Profile {
&self,
rid: identity::RepoId,
cob_id: git::Oid,
edit: cobs::patch::ReviewEdit,
edit: models::patch::ReviewEdit,
) -> Result<(), Error> {
let profile = self.profile();
let repo = profile.storage.repository(rid)?;
+34 -7
View File
@@ -8,17 +8,21 @@ use axum::routing::post;
use axum::Router;
use hyper::header::CONTENT_TYPE;
use hyper::Method;
use radicle_types::domain::inbox::models::notification::NotificationCount;
use serde::{Deserialize, Serialize};
use tower_http::cors::{self, CorsLayer};
use radicle::{git, identity};
use radicle_types as types;
use radicle_types::cobs;
use radicle_types::cobs::issue;
use radicle_types::cobs::issue::NewIssue;
use radicle_types::cobs::patch;
use radicle_types::cobs::CobOptions;
use radicle_types::domain::inbox::models::notification::NotificationCount;
use radicle_types::domain::patch::models;
use radicle_types::domain::patch::service::Service;
use radicle_types::domain::patch::traits::PatchService;
use radicle_types::error::Error;
use radicle_types::outbound::sqlite::Sqlite;
use radicle_types::traits::auth::Auth;
use radicle_types::traits::cobs::Cobs;
use radicle_types::traits::issue::{Issues, IssuesMut};
@@ -30,6 +34,7 @@ use radicle_types::traits::Profile;
#[derive(Clone)]
pub struct Context {
profile: Arc<radicle::Profile>,
patches: Arc<Service<Sqlite>>,
}
impl Auth for Context {}
@@ -47,8 +52,8 @@ impl Profile for Context {
}
impl Context {
pub fn new(profile: Arc<radicle::Profile>) -> Self {
Self { profile }
pub fn new(profile: Arc<radicle::Profile>, patches: Arc<Service<Sqlite>>) -> Self {
Self { profile, patches }
}
}
@@ -70,7 +75,7 @@ pub fn router(ctx: Context) -> Router {
)
.route(
"/activity_by_patch",
post(activity_patch_handler::<patch::Action>),
post(activity_patch_handler::<models::patch::Action>),
)
.route("/get_diff", post(diff_handler))
.route("/list_issues", post(issues_handler))
@@ -345,9 +350,31 @@ async fn patches_handler(
status,
}): Json<PatchesBody>,
) -> impl IntoResponse {
let patches = ctx.list_patches(rid, status, skip, take)?;
let profile = ctx.profile;
let cursor = skip.unwrap_or(0);
let take = take.unwrap_or(20);
let aliases = profile.aliases();
let patches = match status {
None => ctx.patches.list(rid)?.collect::<Vec<_>>(),
Some(s) => ctx
.patches
.list_by_status(rid, s.into())?
.collect::<Vec<_>>(),
};
let more = cursor + take < patches.len();
Ok::<_, Error>(Json(patches))
let patches = patches
.into_iter()
.map(|(id, patch)| models::patch::Patch::new(id, &patch, &aliases))
.skip(cursor)
.take(take)
.collect::<Vec<_>>();
Ok::<_, Error>(Json(cobs::PaginatedQuery {
cursor,
more,
content: patches,
}))
}
#[derive(Serialize, Deserialize)]
+9 -1
View File
@@ -4,8 +4,11 @@ use std::sync::Arc;
use axum::Router;
use tokio::net::TcpListener;
use radicle::cob::cache::COBS_DB_FILE;
use radicle::Profile;
use radicle_types::domain::patch::service::Service as PatchService;
mod api;
#[derive(Debug, Clone)]
@@ -25,7 +28,12 @@ pub async fn run(options: Options) -> anyhow::Result<()> {
fn router(profile: Profile) -> anyhow::Result<Router> {
let profile = Arc::new(profile);
let ctx = api::Context::new(profile);
let patch_db =
radicle_types::outbound::sqlite::Sqlite::reader(profile.cobs().join(COBS_DB_FILE))?;
let patch_service = PatchService::new(patch_db);
let ctx = api::Context::new(profile, Arc::new(patch_service));
Ok(api::router(ctx))
}