mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-09 05:30:28 +08:00
Add test-http-api crate to make app work in browser
A new `invoke` function calls either the tauri backend or the `test-http-api` function if ran in a browser.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
node_modules/
|
||||
|
||||
# Tauri
|
||||
crates/test-http-api/target
|
||||
crates/radicle-tauri/target
|
||||
crates/radicle-tauri/gen/schemas
|
||||
|
||||
|
||||
Generated
+129
-8
@@ -292,7 +292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"axum-core 0.3.4",
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
@@ -308,7 +308,40 @@ dependencies = [
|
||||
"rustversion",
|
||||
"serde",
|
||||
"sync_wrapper 0.1.2",
|
||||
"tower",
|
||||
"tower 0.4.13",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core 0.4.5",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.5.0",
|
||||
"hyper-util",
|
||||
"itoa 1.0.11",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 1.0.1",
|
||||
"tokio",
|
||||
"tower 0.5.1",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
@@ -330,6 +363,26 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper 1.0.1",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.74"
|
||||
@@ -1114,8 +1167,8 @@ dependencies = [
|
||||
"tonic",
|
||||
"tonic-health",
|
||||
"tonic-web",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower 0.4.13",
|
||||
"tower-http 0.4.4",
|
||||
"tower-layer",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
@@ -2164,6 +2217,7 @@ dependencies = [
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 1.0.11",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
@@ -2537,6 +2591,12 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "lexopt"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401"
|
||||
|
||||
[[package]]
|
||||
name = "libappindicator"
|
||||
version = "0.9.0"
|
||||
@@ -3992,7 +4052,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"localtime",
|
||||
"log",
|
||||
"radicle",
|
||||
"radicle-surf",
|
||||
@@ -4014,6 +4073,9 @@ dependencies = [
|
||||
name = "radicle-types"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"localtime",
|
||||
"radicle",
|
||||
"radicle-surf",
|
||||
"serde",
|
||||
@@ -4506,6 +4568,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
|
||||
dependencies = [
|
||||
"itoa 1.0.11",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.19"
|
||||
@@ -5382,6 +5454,24 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "test-http-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.7",
|
||||
"hyper 1.5.0",
|
||||
"lexopt",
|
||||
"radicle",
|
||||
"radicle-surf",
|
||||
"radicle-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tower-http 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-slice"
|
||||
version = "0.1.1"
|
||||
@@ -5603,7 +5693,7 @@ checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum 0.6.20",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"h2",
|
||||
@@ -5616,7 +5706,7 @@ dependencies = [
|
||||
"prost",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower",
|
||||
"tower 0.4.13",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -5649,7 +5739,7 @@ dependencies = [
|
||||
"pin-project",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
"tower-http",
|
||||
"tower-http 0.4.4",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -5675,6 +5765,21 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper 0.1.2",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.4.4"
|
||||
@@ -5693,6 +5798,22 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bytes",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
|
||||
+2
-1
@@ -3,5 +3,6 @@ resolver = "1"
|
||||
|
||||
members = [
|
||||
"crates/radicle-tauri",
|
||||
"crates/radicle-types"
|
||||
"crates/radicle-types",
|
||||
"crates/test-http-api",
|
||||
]
|
||||
|
||||
@@ -18,7 +18,6 @@ tauri-build = { version = "2.0.1", features = ["isolation"] }
|
||||
anyhow = { version = "1.0.90" }
|
||||
base64 = { version = "0.22.1" }
|
||||
log = { version = "0.4.22" }
|
||||
localtime = { version = "1.3.1" }
|
||||
radicle = { git = "https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git" }
|
||||
radicle-types = { version = "0.1.0", path = "../radicle-types" }
|
||||
radicle-surf = { version = "0.22.1", features = ["serde"] }
|
||||
|
||||
@@ -1,28 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
|
||||
use radicle::crypto::ssh;
|
||||
use radicle_types::traits::auth::Auth;
|
||||
|
||||
use crate::{error::Error, AppState};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn authenticate(ctx: tauri::State<AppState>) -> Result<(), Error> {
|
||||
let profile = &ctx.profile;
|
||||
|
||||
if !profile.keystore.is_encrypted()? {
|
||||
return Ok(());
|
||||
}
|
||||
match ssh::agent::Agent::connect() {
|
||||
Ok(mut agent) => {
|
||||
if agent.request_identities()?.contains(&profile.public_key) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::WithHint {
|
||||
err: anyhow!("Not able to find your keys in the ssh agent"),
|
||||
hint: "Make sure to run <code>rad auth</code> in your terminal to add your keys to the ssh-agent.",
|
||||
})?
|
||||
}
|
||||
}
|
||||
Err(e) if e.is_not_running() => Err(Error::WithHint { err: anyhow!("SSH Agent is not running"), hint: "For now we require the user to have an ssh agent running, since we don't have passphrase inputs yet." })?,
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
pub(crate) fn authenticate(ctx: tauri::State<AppState>) -> Result<(), Error> {
|
||||
ctx.authenticate().map_err(Error::from)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
|
||||
use radicle::cob;
|
||||
use radicle::git;
|
||||
use radicle::identity;
|
||||
use radicle::storage::{ReadRepository, ReadStorage};
|
||||
use radicle_types as types;
|
||||
use radicle_types::traits::cobs::Cobs;
|
||||
use radicle_types::traits::thread::Thread;
|
||||
|
||||
use crate::{error, AppState};
|
||||
|
||||
@@ -18,10 +17,7 @@ pub async fn get_file_by_oid(
|
||||
rid: identity::RepoId,
|
||||
oid: git::Oid,
|
||||
) -> Result<String, error::Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let blob = repo.blob(oid)?;
|
||||
|
||||
Ok::<_, error::Error>(STANDARD.encode(blob.content()))
|
||||
ctx.get_embed(rid, oid).map_err(error::Error::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -31,10 +27,7 @@ pub async fn save_embed(
|
||||
name: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<git::Oid, error::Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let embed = cob::Embed::<git::Oid>::store(name, bytes, &repo.backend)?;
|
||||
|
||||
Ok::<_, error::Error>(embed.oid())
|
||||
ctx.save_embed(rid, name, bytes).map_err(error::Error::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -44,82 +37,6 @@ pub fn activity_by_id(
|
||||
type_name: cob::TypeName,
|
||||
id: git::Oid,
|
||||
) -> Result<Vec<types::cobs::issue::Operation>, error::Error> {
|
||||
let aliases = ctx.profile.aliases();
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let ops = cob::store::ops(&id.into(), &type_name, &repo).unwrap();
|
||||
let mut actions: Vec<types::cobs::issue::Operation> = Vec::new();
|
||||
|
||||
for op in ops.into_iter() {
|
||||
actions.extend(op.actions.iter().filter_map(
|
||||
|action: &Vec<u8>| -> Option<types::cobs::issue::Operation> {
|
||||
let action: types::cobs::issue::Action = serde_json::from_slice(action).ok()?;
|
||||
|
||||
Some(types::cobs::issue::Operation {
|
||||
entry_id: op.id,
|
||||
action,
|
||||
author: types::cobs::Author::new(op.author.into(), &aliases),
|
||||
timestamp: op.timestamp,
|
||||
})
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
Ok::<_, error::Error>(actions)
|
||||
}
|
||||
|
||||
mod query {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use radicle::issue;
|
||||
use radicle::patch;
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum IssueStatus {
|
||||
Closed,
|
||||
#[default]
|
||||
Open,
|
||||
All,
|
||||
}
|
||||
|
||||
impl IssueStatus {
|
||||
pub fn matches(&self, issue: &issue::State) -> bool {
|
||||
match self {
|
||||
Self::Open => matches!(issue, issue::State::Open),
|
||||
Self::Closed => matches!(issue, issue::State::Closed { .. }),
|
||||
Self::All => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PatchStatus {
|
||||
#[default]
|
||||
Open,
|
||||
Draft,
|
||||
Archived,
|
||||
Merged,
|
||||
}
|
||||
|
||||
impl From<patch::Status> for PatchStatus {
|
||||
fn from(value: patch::Status) -> Self {
|
||||
match value {
|
||||
patch::Status::Archived => Self::Archived,
|
||||
patch::Status::Draft => Self::Draft,
|
||||
patch::Status::Merged => Self::Merged,
|
||||
patch::Status::Open => Self::Open,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<PatchStatus> for patch::Status {
|
||||
fn from(value: PatchStatus) -> Self {
|
||||
match value {
|
||||
PatchStatus::Archived => Self::Archived,
|
||||
PatchStatus::Draft => Self::Draft,
|
||||
PatchStatus::Merged => Self::Merged,
|
||||
PatchStatus::Open => Self::Open,
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.activity_by_id(rid, type_name, id)
|
||||
.map_err(error::Error::from)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use radicle::cob;
|
||||
use radicle::cob::object::Storage;
|
||||
use radicle::git;
|
||||
use radicle::git::refs::storage::draft;
|
||||
use radicle::identity;
|
||||
use radicle::storage;
|
||||
use radicle::storage::ReadStorage;
|
||||
|
||||
use radicle_types::traits::cobs::Cobs;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::AppState;
|
||||
@@ -20,26 +18,6 @@ pub fn publish_draft(
|
||||
cob_id: git::Oid,
|
||||
type_name: cob::TypeName,
|
||||
) -> Result<(), Error> {
|
||||
let signer = ctx.profile.signer()?;
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let draft_oid =
|
||||
repo.backend
|
||||
.refname_to_id(&draft::cob(signer.public_key(), &type_name, &cob_id.into()))?;
|
||||
repo.update(
|
||||
signer.public_key(),
|
||||
&type_name,
|
||||
&cob_id.into(),
|
||||
&draft_oid.into(),
|
||||
)?;
|
||||
|
||||
let mut patches = ctx.profile.patches_mut(&repo)?;
|
||||
patches.write(&cob_id.into())?;
|
||||
|
||||
storage::git::cob::DraftStore::new(&repo, *signer.public_key()).remove(
|
||||
signer.public_key(),
|
||||
&type_name,
|
||||
&cob_id.into(),
|
||||
)?;
|
||||
|
||||
Ok::<_, Error>(())
|
||||
ctx.publish_draft(rid, cob_id, type_name)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use radicle::git;
|
||||
use radicle::identity;
|
||||
use radicle::issue::cache::Issues;
|
||||
use radicle::node::Handle;
|
||||
use radicle::node::Node;
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle_types as types;
|
||||
|
||||
use crate::cob::query;
|
||||
use radicle_types as types;
|
||||
use radicle_types::traits::issue::Issues;
|
||||
use radicle_types::traits::issue::IssuesMut;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::AppState;
|
||||
|
||||
@@ -17,25 +15,7 @@ pub fn create_issue(
|
||||
new: types::cobs::issue::NewIssue,
|
||||
opts: types::cobs::CobOptions,
|
||||
) -> Result<types::cobs::issue::Issue, Error> {
|
||||
let mut node = Node::new(ctx.profile.socket());
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let signer = ctx.profile.signer()?;
|
||||
let aliases = ctx.profile.aliases();
|
||||
let mut issues = ctx.profile.issues_mut(&repo)?;
|
||||
let issue = issues.create(
|
||||
new.title,
|
||||
new.description,
|
||||
&new.labels,
|
||||
&new.assignees,
|
||||
new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
|
||||
if opts.announce() {
|
||||
node.announce_refs(rid)?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(types::cobs::issue::Issue::new(issue.id(), &issue, &aliases))
|
||||
ctx.create_issue(rid, new, opts).map_err(Error::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -46,103 +26,24 @@ pub fn edit_issue(
|
||||
action: types::cobs::issue::Action,
|
||||
opts: types::cobs::CobOptions,
|
||||
) -> Result<types::cobs::issue::Issue, Error> {
|
||||
let mut node = Node::new(ctx.profile.socket());
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let signer = ctx.profile.signer()?;
|
||||
let aliases = ctx.profile.aliases();
|
||||
let mut issues = ctx.profile.issues_mut(&repo)?;
|
||||
let mut issue = issues.get_mut(&cob_id.into())?;
|
||||
|
||||
match action {
|
||||
types::cobs::issue::Action::Lifecycle { state } => {
|
||||
issue.lifecycle(state.into(), &signer)?;
|
||||
}
|
||||
types::cobs::issue::Action::Assign { assignees } => {
|
||||
issue.assign(assignees, &signer)?;
|
||||
}
|
||||
types::cobs::issue::Action::Label { labels } => {
|
||||
issue.label(labels, &signer)?;
|
||||
}
|
||||
types::cobs::issue::Action::CommentReact {
|
||||
id,
|
||||
reaction,
|
||||
active,
|
||||
} => {
|
||||
issue.react(id, reaction, active, &signer)?;
|
||||
}
|
||||
types::cobs::issue::Action::CommentRedact { id } => {
|
||||
issue.redact_comment(id, &signer)?;
|
||||
}
|
||||
types::cobs::issue::Action::Comment {
|
||||
body,
|
||||
reply_to,
|
||||
embeds,
|
||||
} => {
|
||||
issue.comment(
|
||||
body,
|
||||
reply_to.unwrap_or(cob_id),
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
types::cobs::issue::Action::CommentEdit { id, body, embeds } => {
|
||||
issue.edit_comment(
|
||||
id,
|
||||
body,
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
types::cobs::issue::Action::Edit { title } => {
|
||||
issue.edit(title, &signer)?;
|
||||
}
|
||||
}
|
||||
|
||||
if opts.announce() {
|
||||
node.announce_refs(rid)?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(types::cobs::issue::Issue::new(issue.id(), &issue, &aliases))
|
||||
ctx.edit_issue(rid, cob_id, action, opts)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_issues(
|
||||
pub(crate) fn list_issues(
|
||||
ctx: tauri::State<AppState>,
|
||||
rid: identity::RepoId,
|
||||
status: query::IssueStatus,
|
||||
status: Option<types::cobs::query::IssueStatus>,
|
||||
) -> Result<Vec<types::cobs::issue::Issue>, Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let issues = ctx.profile.issues(&repo)?;
|
||||
let mut issues: Vec<_> = issues
|
||||
.list()?
|
||||
.filter_map(|r| {
|
||||
let (id, issue) = r.ok()?;
|
||||
(status.matches(issue.state())).then_some((id, issue))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
issues.sort_by(|(_, a), (_, b)| b.timestamp().cmp(&a.timestamp()));
|
||||
let aliases = &ctx.profile.aliases();
|
||||
let issues = issues
|
||||
.into_iter()
|
||||
.map(|(id, issue)| types::cobs::issue::Issue::new(&id, &issue, aliases))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok::<_, Error>(issues)
|
||||
ctx.list_issues(rid, status).map_err(Error::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn issue_by_id(
|
||||
pub(crate) fn issue_by_id(
|
||||
ctx: tauri::State<AppState>,
|
||||
rid: identity::RepoId,
|
||||
id: git::Oid,
|
||||
) -> Result<Option<types::cobs::issue::Issue>, Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let issues = ctx.profile.issues(&repo)?;
|
||||
let issue = issues.get(&id.into())?;
|
||||
|
||||
let aliases = &ctx.profile.aliases();
|
||||
let issue = issue.map(|issue| types::cobs::issue::Issue::new(&id.into(), &issue, aliases));
|
||||
|
||||
Ok::<_, Error>(issue)
|
||||
ctx.issue_by_id(rid, id).map_err(Error::from)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
use radicle::cob;
|
||||
use radicle::git;
|
||||
use radicle::identity;
|
||||
use radicle::node::Handle;
|
||||
use radicle::node::Node;
|
||||
use radicle::patch;
|
||||
use radicle::patch::cache::Patches;
|
||||
use radicle::storage;
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle_types as types;
|
||||
|
||||
use crate::cob::query;
|
||||
use radicle_types as types;
|
||||
use radicle_types::traits::patch::Patches;
|
||||
use radicle_types::traits::patch::PatchesMut;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::AppState;
|
||||
|
||||
@@ -17,38 +14,12 @@ use crate::AppState;
|
||||
pub async fn list_patches(
|
||||
ctx: tauri::State<'_, AppState>,
|
||||
rid: identity::RepoId,
|
||||
status: Option<query::PatchStatus>,
|
||||
status: Option<types::cobs::query::PatchStatus>,
|
||||
skip: Option<usize>,
|
||||
take: Option<usize>,
|
||||
) -> Result<types::cobs::PaginatedQuery<Vec<types::cobs::patch::Patch>>, Error> {
|
||||
let cursor = skip.unwrap_or(0);
|
||||
let take = take.unwrap_or(20);
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let aliases = &ctx.profile.aliases();
|
||||
let cache = ctx.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)| types::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>(types::cobs::PaginatedQuery {
|
||||
cursor,
|
||||
more,
|
||||
content: patches,
|
||||
})
|
||||
ctx.list_patches(rid, status, skip, take)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -57,13 +28,7 @@ pub fn patch_by_id(
|
||||
rid: identity::RepoId,
|
||||
id: git::Oid,
|
||||
) -> Result<Option<types::cobs::patch::Patch>, Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let patches = ctx.profile.patches(&repo)?;
|
||||
let patch = patches.get(&id.into())?;
|
||||
let aliases = &ctx.profile.aliases();
|
||||
let patches = patch.map(|patch| types::cobs::patch::Patch::new(id.into(), &patch, aliases));
|
||||
|
||||
Ok::<_, Error>(patches)
|
||||
ctx.get_patch(rid, id).map_err(Error::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -72,18 +37,7 @@ pub fn revisions_by_patch(
|
||||
rid: identity::RepoId,
|
||||
id: git::Oid,
|
||||
) -> Result<Option<Vec<types::cobs::patch::Revision>>, Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let patches = ctx.profile.patches(&repo)?;
|
||||
let revisions = patches.get(&id.into())?.map(|patch| {
|
||||
let aliases = &ctx.profile.aliases();
|
||||
|
||||
patch
|
||||
.revisions()
|
||||
.map(|(_, r)| types::cobs::patch::Revision::new(r.clone(), aliases))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
Ok::<_, Error>(revisions)
|
||||
ctx.revisions_by_patch(rid, id).map_err(Error::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -93,23 +47,10 @@ pub fn revision_by_patch_and_id(
|
||||
id: git::Oid,
|
||||
revision_id: git::Oid,
|
||||
) -> Result<Option<types::cobs::patch::Revision>, Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let patches = ctx.profile.patches(&repo)?;
|
||||
let revision = patches.get(&id.into())?.and_then(|patch| {
|
||||
let aliases = &ctx.profile.aliases();
|
||||
|
||||
patch
|
||||
.revision(&revision_id.into())
|
||||
.map(|r| types::cobs::patch::Revision::new(r.clone(), aliases))
|
||||
});
|
||||
|
||||
Ok::<_, Error>(revision)
|
||||
ctx.revision_by_id(rid, id, revision_id)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Creates a draft review for a specific patch revision.
|
||||
///
|
||||
/// This Tauri command allows users to create a new draft review for a specific patch revision.
|
||||
/// The draft is associated with the user (signer) and the provided patch revision within the repository.
|
||||
#[tauri::command]
|
||||
pub fn create_draft_review(
|
||||
ctx: tauri::State<AppState>,
|
||||
@@ -118,37 +59,8 @@ pub fn create_draft_review(
|
||||
cob_id: git::Oid,
|
||||
labels: Vec<cob::Label>,
|
||||
) -> Result<patch::ReviewId, Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let signer = ctx.profile.signer()?;
|
||||
let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
|
||||
|
||||
let mut patches = cob::patch::Cache::no_cache(&drafts)?;
|
||||
let mut patch = patches.get_mut(&cob_id.into())?;
|
||||
let revision = patch
|
||||
.revision(&revision_id)
|
||||
.ok_or_else(|| Error::WithHint {
|
||||
err: anyhow::anyhow!("patch revision not found"),
|
||||
hint: "Not able to find the specified patch revision.",
|
||||
})?;
|
||||
|
||||
revision
|
||||
.review_by(signer.public_key())
|
||||
.ok_or(Error::WithHint {
|
||||
err: anyhow::anyhow!("duplicate patch review found"),
|
||||
hint: "Found an existing draft patch review on this patch revision and repo.",
|
||||
})?;
|
||||
|
||||
let review_id = patch.review(
|
||||
revision.id(),
|
||||
Some(cob::patch::Verdict::Reject),
|
||||
None,
|
||||
labels,
|
||||
&signer,
|
||||
)?;
|
||||
|
||||
patches.write(&cob_id.into())?;
|
||||
|
||||
Ok::<_, Error>(review_id)
|
||||
ctx.create_draft_review(rid, revision_id, cob_id, labels)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Creates a new review comment on a draft review for a specific patch.
|
||||
@@ -163,28 +75,8 @@ pub fn create_draft_review_comment(
|
||||
cob_id: git::Oid,
|
||||
new: types::cobs::thread::CreateReviewComment,
|
||||
) -> Result<(), Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let signer = ctx.profile.signer()?;
|
||||
let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
|
||||
|
||||
let mut patches = cob::patch::Cache::no_cache(&drafts)?;
|
||||
let mut patch = patches.get_mut(&cob_id.into())?;
|
||||
|
||||
patch.transaction("Review comments", &signer, |tx| {
|
||||
tx.review_comment(
|
||||
new.review_id,
|
||||
new.body,
|
||||
new.location.map(|l| l.into()),
|
||||
new.reply_to,
|
||||
new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
patches.write(&cob_id.into())?;
|
||||
|
||||
Ok::<_, Error>(())
|
||||
ctx.create_draft_review_comment(rid, cob_id, new)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Edits a draft review for a specific patch revision in a repository.
|
||||
@@ -198,31 +90,10 @@ pub fn edit_draft_review(
|
||||
cob_id: git::Oid,
|
||||
edit: types::cobs::patch::ReviewEdit,
|
||||
) -> Result<(), Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let signer = ctx.profile.signer()?;
|
||||
let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
|
||||
|
||||
let mut patches = cob::patch::Cache::no_cache(&drafts)?;
|
||||
let mut patch = patches.get_mut(&cob_id.into())?;
|
||||
patch.review_edit(
|
||||
edit.review_id,
|
||||
edit.verdict,
|
||||
edit.summary,
|
||||
edit.labels,
|
||||
&signer,
|
||||
)?;
|
||||
|
||||
patches.write(&cob_id.into())?;
|
||||
|
||||
Ok::<_, Error>(())
|
||||
ctx.edit_draft_review(rid, cob_id, edit)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Gets the draft review of the local user for a specific patch revision in a repository.
|
||||
///
|
||||
/// This Tauri command is used to retrieve a patch review draft for the local user
|
||||
/// on a given patch revision from a repository.
|
||||
/// It looks up the repository using the provided repository ID (`rid`) and patch object ID (`cob_id`),
|
||||
/// and gets the patch review of the local user associated with a specific revision (`revision_id`), if it exists.
|
||||
#[tauri::command]
|
||||
pub fn get_draft_review(
|
||||
ctx: tauri::State<AppState>,
|
||||
@@ -230,16 +101,7 @@ pub fn get_draft_review(
|
||||
cob_id: git::Oid,
|
||||
revision_id: patch::RevisionId,
|
||||
) -> Option<patch::Review> {
|
||||
let repo = ctx.profile.storage.repository(rid).ok()?;
|
||||
let signer = ctx.profile.signer().ok()?;
|
||||
let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
|
||||
let patches = cob::patch::Cache::no_cache(&drafts).ok()?;
|
||||
|
||||
let patch = patches.get(&cob_id.into()).ok()?;
|
||||
let revision = patch.and_then(|p| p.revision(&revision_id).cloned());
|
||||
let review = revision.and_then(|rev| rev.review_by(signer.public_key()).cloned());
|
||||
|
||||
review
|
||||
ctx.get_draft_review(rid, cob_id, revision_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -250,177 +112,6 @@ pub fn edit_patch(
|
||||
action: types::cobs::patch::Action,
|
||||
opts: types::cobs::CobOptions,
|
||||
) -> Result<types::cobs::patch::Patch, Error> {
|
||||
let mut node = Node::new(ctx.profile.socket());
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let signer = ctx.profile.signer()?;
|
||||
let aliases = ctx.profile.aliases();
|
||||
let mut patches = ctx.profile.patches_mut(&repo)?;
|
||||
let mut patch = patches.get_mut(&cob_id.into())?;
|
||||
|
||||
match action {
|
||||
types::cobs::patch::Action::RevisionEdit {
|
||||
revision,
|
||||
description,
|
||||
embeds,
|
||||
} => {
|
||||
patch.edit_revision(
|
||||
revision,
|
||||
description,
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
types::cobs::patch::Action::RevisionCommentRedact { revision, comment } => {
|
||||
patch.comment_redact(revision, comment, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::ReviewCommentRedact { review, comment } => {
|
||||
patch.redact_review_comment(review, comment, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::ReviewCommentReact {
|
||||
review,
|
||||
comment,
|
||||
reaction,
|
||||
active,
|
||||
} => {
|
||||
patch.react_review_comment(review, comment, reaction, active, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::ReviewCommentResolve { review, comment } => {
|
||||
patch.resolve_review_comment(review, comment, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::ReviewCommentUnresolve { review, comment } => {
|
||||
patch.unresolve_review_comment(review, comment, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::Edit { title, target } => {
|
||||
patch.edit(title, target, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::ReviewEdit {
|
||||
review,
|
||||
summary,
|
||||
verdict,
|
||||
labels,
|
||||
} => {
|
||||
patch.review_edit(review, verdict, summary, labels, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::Review {
|
||||
revision,
|
||||
summary,
|
||||
verdict,
|
||||
labels,
|
||||
} => {
|
||||
patch.review(revision, verdict, summary, labels, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::ReviewRedact { review } => {
|
||||
patch.redact_review(review, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::ReviewComment {
|
||||
review,
|
||||
body,
|
||||
location,
|
||||
reply_to,
|
||||
embeds,
|
||||
} => {
|
||||
patch.review_comment(
|
||||
review,
|
||||
body,
|
||||
location.map(|l| l.into()),
|
||||
reply_to,
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
types::cobs::patch::Action::ReviewCommentEdit {
|
||||
review,
|
||||
comment,
|
||||
body,
|
||||
embeds,
|
||||
} => {
|
||||
patch.edit_review_comment(
|
||||
review,
|
||||
comment,
|
||||
body,
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
types::cobs::patch::Action::Lifecycle { state } => {
|
||||
patch.lifecycle(state, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::Assign { assignees } => {
|
||||
patch.assign(assignees, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::Label { labels } => {
|
||||
patch.label(labels, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::RevisionReact {
|
||||
revision,
|
||||
reaction,
|
||||
location,
|
||||
active,
|
||||
} => {
|
||||
patch.react(
|
||||
revision,
|
||||
reaction,
|
||||
location.map(|l| l.into()),
|
||||
active,
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
types::cobs::patch::Action::RevisionComment {
|
||||
revision,
|
||||
location,
|
||||
body,
|
||||
reply_to,
|
||||
embeds,
|
||||
} => {
|
||||
patch.comment(
|
||||
revision,
|
||||
body,
|
||||
reply_to,
|
||||
location.map(|l| l.into()),
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
types::cobs::patch::Action::RevisionCommentEdit {
|
||||
revision,
|
||||
comment,
|
||||
body,
|
||||
embeds,
|
||||
} => {
|
||||
patch.comment_edit(
|
||||
revision,
|
||||
comment,
|
||||
body,
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
types::cobs::patch::Action::RevisionCommentReact {
|
||||
revision,
|
||||
comment,
|
||||
reaction,
|
||||
active,
|
||||
} => {
|
||||
patch.comment_react(revision, comment, reaction, active, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::RevisionRedact { revision } => {
|
||||
patch.redact(revision, &signer)?;
|
||||
}
|
||||
types::cobs::patch::Action::Merge { .. } => {
|
||||
unimplemented!("We don't support merging of patches through the desktop")
|
||||
}
|
||||
types::cobs::patch::Action::Revision { .. } => {
|
||||
unimplemented!("We don't support creating new revisions through the desktop")
|
||||
}
|
||||
}
|
||||
|
||||
if opts.announce() {
|
||||
node.announce_refs(rid)?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(types::cobs::patch::Patch::new(
|
||||
*patch.id(),
|
||||
&patch,
|
||||
&aliases,
|
||||
))
|
||||
ctx.edit_patch(rid, cob_id, action, opts)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
@@ -1,45 +1,15 @@
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle_surf as surf;
|
||||
|
||||
use radicle::git;
|
||||
use radicle::identity;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use radicle_types::traits::repo::Repo;
|
||||
|
||||
use crate::{error, AppState};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Options {
|
||||
pub base: git::Oid,
|
||||
pub head: git::Oid,
|
||||
pub unified: u32,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_diff(
|
||||
ctx: tauri::State<'_, AppState>,
|
||||
rid: identity::RepoId,
|
||||
options: Options,
|
||||
options: radicle_types::cobs::diff::Options,
|
||||
) -> Result<surf::diff::Diff, error::Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?.backend;
|
||||
let base = repo.find_commit(*options.base)?;
|
||||
let head = repo.find_commit(*options.head)?;
|
||||
|
||||
let mut opts = git::raw::DiffOptions::new();
|
||||
opts.patience(true)
|
||||
.minimal(true)
|
||||
.context_lines(options.unified);
|
||||
|
||||
let mut find_opts = git::raw::DiffFindOptions::new();
|
||||
find_opts.exact_match_only(true);
|
||||
find_opts.all(true);
|
||||
|
||||
let left = base.tree()?;
|
||||
let right = head.tree()?;
|
||||
|
||||
let mut diff = repo.diff_tree_to_tree(Some(&left), Some(&right), Some(&mut opts))?;
|
||||
diff.find_similar(Some(&mut find_opts))?;
|
||||
let diff = surf::diff::Diff::try_from(diff)?;
|
||||
|
||||
Ok::<_, error::Error>(diff)
|
||||
ctx.get_diff(rid, options).map_err(error::Error::from)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
use radicle_types::config::Config;
|
||||
use radicle_types::traits::Profile;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn config(ctx: tauri::State<AppState>) -> Result<Config, Error> {
|
||||
let config = Config {
|
||||
public_key: ctx.profile.public_key,
|
||||
alias: ctx.profile.config.node.alias.clone(),
|
||||
seeding_policy: ctx.profile.config.node.seeding_policy,
|
||||
};
|
||||
|
||||
Ok::<_, Error>(config)
|
||||
pub fn config(ctx: tauri::State<AppState>) -> Config {
|
||||
ctx.config()
|
||||
}
|
||||
|
||||
@@ -1,40 +1,15 @@
|
||||
use radicle::crypto::Verified;
|
||||
use radicle::identity::doc::PayloadId;
|
||||
use radicle::identity::{DocAt, RepoId};
|
||||
use radicle::issue::cache::Issues;
|
||||
use radicle::node::routing::Store;
|
||||
use radicle::patch::cache::Patches;
|
||||
use radicle::prelude::Doc;
|
||||
use radicle::storage::git::Repository;
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle::storage::{self, ReadRepository};
|
||||
use radicle::git;
|
||||
use radicle::identity::RepoId;
|
||||
|
||||
use radicle_types as types;
|
||||
use radicle_types::traits::repo::Repo;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::AppState;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_repos(ctx: tauri::State<AppState>) -> Result<Vec<types::repo::RepoInfo>, Error> {
|
||||
let storage = &ctx.profile.storage;
|
||||
let policies = ctx.profile.policies()?;
|
||||
let mut repos = storage.repositories()?.into_iter().collect::<Vec<_>>();
|
||||
repos.sort_by_key(|p| p.rid);
|
||||
|
||||
let infos = repos
|
||||
.into_iter()
|
||||
.filter_map(|info| {
|
||||
if !policies.is_seeding(&info.rid).unwrap_or_default() {
|
||||
return None;
|
||||
}
|
||||
let repo = ctx.profile.storage.repository(info.rid).ok()?;
|
||||
let DocAt { doc, .. } = repo.identity_doc().ok()?;
|
||||
let repo_info = repo_info(&ctx.profile, &repo, &doc).ok()?;
|
||||
|
||||
Some(repo_info)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok::<_, Error>(infos)
|
||||
ctx.list_repos().map_err(Error::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -42,72 +17,15 @@ pub fn repo_by_id(
|
||||
ctx: tauri::State<AppState>,
|
||||
rid: RepoId,
|
||||
) -> Result<types::repo::RepoInfo, Error> {
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let DocAt { doc, .. } = repo.identity_doc()?;
|
||||
|
||||
let repo_info = repo_info(&ctx.profile, &repo, &doc)?;
|
||||
|
||||
Ok::<_, Error>(repo_info)
|
||||
ctx.repo_by_id(rid).map_err(Error::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn diff_stats(
|
||||
ctx: tauri::State<'_, AppState>,
|
||||
rid: RepoId,
|
||||
base: String,
|
||||
head: String,
|
||||
base: git::Oid,
|
||||
head: git::Oid,
|
||||
) -> Result<types::cobs::Stats, Error> {
|
||||
let repo = radicle_surf::Repository::open(storage::git::paths::repository(
|
||||
&ctx.profile.storage,
|
||||
&rid,
|
||||
))?;
|
||||
let base = repo.commit(base)?;
|
||||
let commit = repo.commit(head)?;
|
||||
let diff = repo.diff(base.id, commit.id)?;
|
||||
let stats = diff.stats();
|
||||
|
||||
Ok::<_, Error>(types::cobs::Stats::new(stats))
|
||||
}
|
||||
|
||||
pub fn repo_info(
|
||||
profile: &radicle::Profile,
|
||||
repo: &Repository,
|
||||
doc: &Doc<Verified>,
|
||||
) -> Result<types::repo::RepoInfo, Error> {
|
||||
let aliases = profile.aliases();
|
||||
let delegates = doc
|
||||
.delegates
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|did| types::cobs::Author::new(did, &aliases))
|
||||
.collect::<Vec<_>>();
|
||||
let db = profile.database()?;
|
||||
let seeding = db.count(&repo.id).unwrap_or_default();
|
||||
let project = doc.payload.get(&PayloadId::project()).and_then(|payload| {
|
||||
let (_, head) = repo.head().ok()?;
|
||||
let commit = repo.commit(head).ok()?;
|
||||
let patches = profile.patches(repo).ok()?;
|
||||
let patches = patches.counts().ok()?;
|
||||
let issues = profile.issues(repo).ok()?;
|
||||
let issues = issues.counts().ok()?;
|
||||
|
||||
let data: types::repo::ProjectPayloadData = (*payload).clone().try_into().ok()?;
|
||||
let meta = types::repo::ProjectPayloadMeta {
|
||||
issues,
|
||||
patches,
|
||||
head,
|
||||
last_commit_timestamp: commit.time().seconds() * 1000,
|
||||
};
|
||||
|
||||
Some(types::repo::ProjectPayload::new(data, meta))
|
||||
});
|
||||
|
||||
Ok::<_, Error>(types::repo::RepoInfo {
|
||||
payloads: types::repo::SupportedPayloads { project },
|
||||
delegates,
|
||||
threshold: doc.threshold,
|
||||
visibility: doc.visibility.clone().into(),
|
||||
rid: repo.id,
|
||||
seeding,
|
||||
})
|
||||
ctx.diff_stats(rid, base, head).map_err(Error::from)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
use localtime::LocalTime;
|
||||
|
||||
use radicle::cob;
|
||||
use radicle::identity;
|
||||
use radicle::node::Handle;
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle::Node;
|
||||
|
||||
use radicle_types as types;
|
||||
use radicle_types::traits::thread::Thread;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::AppState;
|
||||
@@ -17,40 +13,8 @@ pub fn create_issue_comment(
|
||||
new: types::cobs::thread::NewIssueComment,
|
||||
opts: types::cobs::CobOptions,
|
||||
) -> Result<types::cobs::thread::Comment<types::cobs::Never>, Error> {
|
||||
let aliases = &ctx.profile.aliases();
|
||||
let mut node = Node::new(ctx.profile.socket());
|
||||
let signer = ctx.profile.signer()?;
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let mut issues = ctx.profile.issues_mut(&repo)?;
|
||||
let mut issue = issues.get_mut(&new.id.into())?;
|
||||
let id = new.reply_to.unwrap_or_else(|| {
|
||||
let (root_id, _) = issue.root();
|
||||
*root_id
|
||||
});
|
||||
let n = new.clone();
|
||||
let oid = issue.comment(
|
||||
n.body,
|
||||
id,
|
||||
n.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
|
||||
if opts.announce() {
|
||||
node.announce_refs(rid)?;
|
||||
}
|
||||
|
||||
Ok(types::cobs::thread::Comment::<types::cobs::Never>::new(
|
||||
oid,
|
||||
cob::thread::Comment::new(
|
||||
*signer.public_key(),
|
||||
new.body,
|
||||
id.into(),
|
||||
None,
|
||||
new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
LocalTime::now().into(),
|
||||
),
|
||||
aliases,
|
||||
))
|
||||
ctx.create_issue_comment(rid, new, opts)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -60,38 +24,6 @@ pub fn create_patch_comment(
|
||||
new: types::cobs::thread::NewPatchComment,
|
||||
opts: types::cobs::CobOptions,
|
||||
) -> Result<types::cobs::thread::Comment<types::cobs::thread::CodeLocation>, Error> {
|
||||
let aliases = &ctx.profile.aliases();
|
||||
let mut node = Node::new(ctx.profile.socket());
|
||||
let signer = ctx.profile.signer()?;
|
||||
let repo = ctx.profile.storage.repository(rid)?;
|
||||
let mut patches = ctx.profile.patches_mut(&repo)?;
|
||||
let mut patch = patches.get_mut(&new.id.into())?;
|
||||
let n = new.clone();
|
||||
let oid = patch.comment(
|
||||
new.revision.into(),
|
||||
n.body,
|
||||
n.reply_to,
|
||||
n.location.map(|l| l.into()),
|
||||
n.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
|
||||
if opts.announce() {
|
||||
node.announce_refs(rid)?;
|
||||
}
|
||||
|
||||
Ok(types::cobs::thread::Comment::<
|
||||
types::cobs::thread::CodeLocation,
|
||||
>::new(
|
||||
oid,
|
||||
cob::thread::Comment::new(
|
||||
*signer.public_key(),
|
||||
new.body,
|
||||
new.reply_to,
|
||||
new.location.map(|l| l.into()),
|
||||
new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
LocalTime::now().into(),
|
||||
),
|
||||
aliases,
|
||||
))
|
||||
ctx.create_patch_comment(rid, new, opts)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,10 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
Storage(#[from] radicle::storage::Error),
|
||||
|
||||
/// Types error.
|
||||
#[error(transparent)]
|
||||
Types(#[from] radicle_types::error::Error),
|
||||
|
||||
/// Surf error.
|
||||
#[error(transparent)]
|
||||
Surf(#[from] radicle_surf::Error),
|
||||
|
||||
@@ -6,6 +6,13 @@ use tauri::Manager;
|
||||
|
||||
use radicle::node::Handle;
|
||||
use radicle::Node;
|
||||
use radicle_types::traits::auth::Auth;
|
||||
use radicle_types::traits::cobs::Cobs;
|
||||
use radicle_types::traits::issue::{Issues, IssuesMut};
|
||||
use radicle_types::traits::patch::{Patches, PatchesMut};
|
||||
use radicle_types::traits::repo::Repo;
|
||||
use radicle_types::traits::thread::Thread;
|
||||
use radicle_types::traits::Profile;
|
||||
|
||||
use commands::{auth, cob, diff, profile, repo, thread};
|
||||
|
||||
@@ -13,6 +20,20 @@ struct AppState {
|
||||
profile: radicle::Profile,
|
||||
}
|
||||
|
||||
impl Auth for AppState {}
|
||||
impl Repo for AppState {}
|
||||
impl Thread for AppState {}
|
||||
impl Cobs for AppState {}
|
||||
impl Issues for AppState {}
|
||||
impl IssuesMut for AppState {}
|
||||
impl Patches for AppState {}
|
||||
impl PatchesMut for AppState {}
|
||||
impl Profile for AppState {
|
||||
fn profile(&self) -> radicle::Profile {
|
||||
self.profile.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
@@ -4,9 +4,12 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.90" }
|
||||
base64 = { version = "0.22.1" }
|
||||
radicle = { git = "https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git" }
|
||||
radicle-surf = { version = "0.22.1", features = ["serde"] }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = { version = "1.0.132" }
|
||||
thiserror = { version = "1.0.65" }
|
||||
ts-rs = { version = "10.0.0", features = ["serde-json-impl", "no-serde-warnings"] }
|
||||
ts-rs = { version = "10.0.0", features = [ "serde-json-impl", "no-serde-warnings" ] }
|
||||
localtime = { version = "1.3.1" }
|
||||
|
||||
@@ -4,11 +4,12 @@ use ts_rs::TS;
|
||||
use radicle::identity;
|
||||
use radicle::node::{Alias, AliasStore};
|
||||
|
||||
pub mod diff;
|
||||
pub mod issue;
|
||||
pub mod patch;
|
||||
pub mod thread;
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[derive(Debug, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "cob/")]
|
||||
@@ -78,3 +79,60 @@ impl Stats {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod query {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use radicle::issue;
|
||||
use radicle::patch;
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum IssueStatus {
|
||||
Closed,
|
||||
#[default]
|
||||
Open,
|
||||
All,
|
||||
}
|
||||
|
||||
impl IssueStatus {
|
||||
pub fn matches(&self, issue: &issue::State) -> bool {
|
||||
match self {
|
||||
Self::Open => matches!(issue, issue::State::Open),
|
||||
Self::Closed => matches!(issue, issue::State::Closed { .. }),
|
||||
Self::All => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PatchStatus {
|
||||
#[default]
|
||||
Open,
|
||||
Draft,
|
||||
Archived,
|
||||
Merged,
|
||||
}
|
||||
|
||||
impl From<patch::Status> for PatchStatus {
|
||||
fn from(value: patch::Status) -> Self {
|
||||
match value {
|
||||
patch::Status::Archived => Self::Archived,
|
||||
patch::Status::Draft => Self::Draft,
|
||||
patch::Status::Merged => Self::Merged,
|
||||
patch::Status::Open => Self::Open,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<PatchStatus> for patch::Status {
|
||||
fn from(value: PatchStatus) -> Self {
|
||||
match value {
|
||||
PatchStatus::Archived => Self::Archived,
|
||||
PatchStatus::Draft => Self::Draft,
|
||||
PatchStatus::Merged => Self::Merged,
|
||||
PatchStatus::Open => Self::Open,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
use radicle::git;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Options {
|
||||
pub base: git::Oid,
|
||||
pub head: git::Oid,
|
||||
pub unified: u32,
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use radicle::patch;
|
||||
|
||||
use crate::cobs;
|
||||
|
||||
#[derive(TS, Serialize)]
|
||||
#[derive(Debug, TS, Serialize)]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "cob/patch/")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -57,7 +57,7 @@ impl Patch {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, TS)]
|
||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase", tag = "status")]
|
||||
#[ts(export)]
|
||||
#[ts(export_to = "cob/patch/")]
|
||||
|
||||
@@ -2,11 +2,88 @@ use serde::Serialize;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Profile error.
|
||||
#[error(transparent)]
|
||||
Profile(#[from] radicle::profile::Error),
|
||||
|
||||
/// Crypto error.
|
||||
#[error(transparent)]
|
||||
Crypto(#[from] radicle::crypto::ssh::keystore::Error),
|
||||
|
||||
/// SSH Agent error.
|
||||
#[error(transparent)]
|
||||
Agent(#[from] radicle::crypto::ssh::agent::Error),
|
||||
|
||||
/// Node database error.
|
||||
#[error(transparent)]
|
||||
Database(#[from] radicle::node::db::Error),
|
||||
|
||||
/// Repository error.
|
||||
#[error(transparent)]
|
||||
Repository(#[from] radicle::storage::RepositoryError),
|
||||
|
||||
/// Policy store error.
|
||||
#[error(transparent)]
|
||||
PolicyStore(#[from] radicle::node::policy::store::Error),
|
||||
|
||||
/// Cob patch cache error.
|
||||
#[error(transparent)]
|
||||
CachePatch(#[from] radicle::cob::patch::cache::Error),
|
||||
|
||||
/// Diff error.
|
||||
#[error(transparent)]
|
||||
Diff(#[from] radicle_surf::diff::git::error::Diff),
|
||||
|
||||
/// Storage error.
|
||||
#[error(transparent)]
|
||||
Storage(#[from] radicle::storage::Error),
|
||||
|
||||
/// Radicle Git error.
|
||||
#[error(transparent)]
|
||||
Git(#[from] radicle::git::Error),
|
||||
|
||||
/// Surf error.
|
||||
#[error(transparent)]
|
||||
Surf(#[from] radicle_surf::Error),
|
||||
|
||||
/// Git2 error.
|
||||
#[error(transparent)]
|
||||
Git2(#[from] radicle::git::raw::Error),
|
||||
|
||||
/// Cob issue cache error.
|
||||
#[error(transparent)]
|
||||
CacheIssue(#[from] radicle::cob::issue::cache::Error),
|
||||
|
||||
/// Patch error.
|
||||
#[error(transparent)]
|
||||
Patch(#[from] radicle::patch::Error),
|
||||
|
||||
/// Issue error.
|
||||
#[error(transparent)]
|
||||
Issue(#[from] radicle::issue::Error),
|
||||
|
||||
/// Node error.
|
||||
#[error(transparent)]
|
||||
Node(#[from] radicle::node::Error),
|
||||
|
||||
/// An error with a hint.
|
||||
#[error("{err} {hint}")]
|
||||
WithHint {
|
||||
err: anyhow::Error,
|
||||
hint: &'static str,
|
||||
},
|
||||
|
||||
/// Serde JSON error.
|
||||
#[error(transparent)]
|
||||
SerdeJSON(#[from] serde_json::error::Error),
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorWrapperWithHint {
|
||||
err: String,
|
||||
hint: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorWrapper {
|
||||
err: String,
|
||||
@@ -17,6 +94,15 @@ impl Serialize for Error {
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
if let Error::WithHint { err, hint } = self {
|
||||
let error_wrapper = ErrorWrapperWithHint {
|
||||
err: err.to_string(),
|
||||
hint: hint.to_string(),
|
||||
};
|
||||
|
||||
return error_wrapper.serialize(serializer);
|
||||
}
|
||||
|
||||
let wrapper = ErrorWrapper {
|
||||
err: self.to_string(),
|
||||
};
|
||||
|
||||
@@ -2,3 +2,4 @@ pub mod cobs;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod repo;
|
||||
pub mod traits;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::config::Config;
|
||||
|
||||
pub mod auth;
|
||||
pub mod cobs;
|
||||
pub mod issue;
|
||||
pub mod patch;
|
||||
pub mod repo;
|
||||
pub mod thread;
|
||||
|
||||
pub trait Profile {
|
||||
fn profile(&self) -> radicle::Profile;
|
||||
|
||||
fn config(&self) -> Config {
|
||||
let p = self.profile();
|
||||
|
||||
Config {
|
||||
public_key: p.public_key,
|
||||
alias: p.config.node.alias.clone(),
|
||||
seeding_policy: p.config.node.seeding_policy,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use anyhow::anyhow;
|
||||
|
||||
use radicle::crypto::ssh;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
use super::Profile;
|
||||
|
||||
pub trait Auth: Profile {
|
||||
fn authenticate(&self) -> Result<(), Error> {
|
||||
let profile = &self.profile();
|
||||
|
||||
if !profile.keystore.is_encrypted()? {
|
||||
return Ok(());
|
||||
}
|
||||
match ssh::agent::Agent::connect() {
|
||||
Ok(mut agent) => {
|
||||
if agent.request_identities()?.contains(&profile.public_key) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::WithHint {
|
||||
err: anyhow!("Not able to find your keys in the ssh agent"),
|
||||
hint: "Make sure to run <code>rad auth</code> in your terminal to add your keys to the ssh-agent.",
|
||||
})?
|
||||
}
|
||||
}
|
||||
Err(e) if e.is_not_running() => Err(Error::WithHint { err: anyhow!("SSH Agent is not running"), hint: "For now we require the user to have an ssh agent running, since we don't have passphrase inputs yet." })?,
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use radicle::cob::object::Storage;
|
||||
use radicle::storage::refs::draft;
|
||||
use radicle::storage::{self, ReadStorage};
|
||||
use radicle::{cob, git, identity};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::traits::Profile;
|
||||
|
||||
pub trait Cobs: Profile {
|
||||
fn activity_by_id(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
type_name: cob::TypeName,
|
||||
id: git::Oid,
|
||||
) -> Result<Vec<crate::cobs::issue::Operation>, Error> {
|
||||
let profile = self.profile();
|
||||
let aliases = profile.aliases();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let ops = cob::store::ops(&id.into(), &type_name, &repo).unwrap();
|
||||
let mut actions: Vec<crate::cobs::issue::Operation> = Vec::new();
|
||||
|
||||
for op in ops.into_iter() {
|
||||
actions.extend(op.actions.iter().filter_map(
|
||||
|action: &Vec<u8>| -> Option<crate::cobs::issue::Operation> {
|
||||
let action: crate::cobs::issue::Action = serde_json::from_slice(action).ok()?;
|
||||
|
||||
Some(crate::cobs::issue::Operation {
|
||||
entry_id: op.id,
|
||||
action,
|
||||
author: crate::cobs::Author::new(op.author.into(), &aliases),
|
||||
timestamp: op.timestamp,
|
||||
})
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
Ok::<_, Error>(actions)
|
||||
}
|
||||
|
||||
fn publish_draft(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
cob_id: git::Oid,
|
||||
type_name: cob::TypeName,
|
||||
) -> Result<(), Error> {
|
||||
let profile = self.profile();
|
||||
let signer = profile.signer()?;
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let draft_oid = repo.backend.refname_to_id(&draft::cob(
|
||||
signer.public_key(),
|
||||
&type_name,
|
||||
&cob_id.into(),
|
||||
))?;
|
||||
repo.update(
|
||||
signer.public_key(),
|
||||
&type_name,
|
||||
&cob_id.into(),
|
||||
&draft_oid.into(),
|
||||
)?;
|
||||
|
||||
let mut patches = profile.patches_mut(&repo)?;
|
||||
patches.write(&cob_id.into())?;
|
||||
|
||||
storage::git::cob::DraftStore::new(&repo, *signer.public_key()).remove(
|
||||
signer.public_key(),
|
||||
&type_name,
|
||||
&cob_id.into(),
|
||||
)?;
|
||||
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
use radicle::issue::cache::Issues as _;
|
||||
use radicle::node::Handle;
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle::{git, identity, Node};
|
||||
|
||||
use crate::cobs;
|
||||
use crate::error::Error;
|
||||
use crate::traits::Profile;
|
||||
|
||||
pub trait Issues: Profile {
|
||||
fn list_issues(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
status: Option<cobs::query::IssueStatus>,
|
||||
) -> Result<Vec<cobs::issue::Issue>, Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let status = status.unwrap_or_default();
|
||||
let issues = profile.issues(&repo)?;
|
||||
let mut issues: Vec<_> = issues
|
||||
.list()?
|
||||
.filter_map(|r| {
|
||||
let (id, issue) = r.ok()?;
|
||||
(status.matches(issue.state())).then_some((id, issue))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
issues.sort_by(|(_, a), (_, b)| b.timestamp().cmp(&a.timestamp()));
|
||||
let aliases = &profile.aliases();
|
||||
let issues = issues
|
||||
.into_iter()
|
||||
.map(|(id, issue)| cobs::issue::Issue::new(&id, &issue, aliases))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok::<_, Error>(issues)
|
||||
}
|
||||
|
||||
fn issue_by_id(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
id: git::Oid,
|
||||
) -> Result<Option<cobs::issue::Issue>, Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let issues = profile.issues(&repo)?;
|
||||
let issue = issues.get(&id.into())?;
|
||||
|
||||
let aliases = &profile.aliases();
|
||||
let issue = issue.map(|issue| cobs::issue::Issue::new(&id.into(), &issue, aliases));
|
||||
|
||||
Ok::<_, Error>(issue)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IssuesMut: Profile {
|
||||
fn create_issue(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
new: cobs::issue::NewIssue,
|
||||
opts: cobs::CobOptions,
|
||||
) -> Result<cobs::issue::Issue, Error> {
|
||||
let profile = self.profile();
|
||||
let mut node = Node::new(profile.socket());
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let signer = profile.signer()?;
|
||||
let aliases = profile.aliases();
|
||||
let mut issues = profile.issues_mut(&repo)?;
|
||||
let issue = issues.create(
|
||||
new.title,
|
||||
new.description,
|
||||
&new.labels,
|
||||
&new.assignees,
|
||||
new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
|
||||
if opts.announce() {
|
||||
node.announce_refs(rid)?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(cobs::issue::Issue::new(issue.id(), &issue, &aliases))
|
||||
}
|
||||
|
||||
fn edit_issue(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
cob_id: git::Oid,
|
||||
action: cobs::issue::Action,
|
||||
opts: cobs::CobOptions,
|
||||
) -> Result<cobs::issue::Issue, Error> {
|
||||
let profile = self.profile();
|
||||
let mut node = Node::new(profile.socket());
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let signer = profile.signer()?;
|
||||
let aliases = profile.aliases();
|
||||
let mut issues = profile.issues_mut(&repo)?;
|
||||
let mut issue = issues.get_mut(&cob_id.into())?;
|
||||
|
||||
match action {
|
||||
cobs::issue::Action::Lifecycle { state } => {
|
||||
issue.lifecycle(state.into(), &signer)?;
|
||||
}
|
||||
cobs::issue::Action::Assign { assignees } => {
|
||||
issue.assign(assignees, &signer)?;
|
||||
}
|
||||
cobs::issue::Action::Label { labels } => {
|
||||
issue.label(labels, &signer)?;
|
||||
}
|
||||
cobs::issue::Action::CommentReact {
|
||||
id,
|
||||
reaction,
|
||||
active,
|
||||
} => {
|
||||
issue.react(id, reaction, active, &signer)?;
|
||||
}
|
||||
cobs::issue::Action::CommentRedact { id } => {
|
||||
issue.redact_comment(id, &signer)?;
|
||||
}
|
||||
cobs::issue::Action::Comment {
|
||||
body,
|
||||
reply_to,
|
||||
embeds,
|
||||
} => {
|
||||
issue.comment(
|
||||
body,
|
||||
reply_to.unwrap_or(cob_id),
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
cobs::issue::Action::CommentEdit { id, body, embeds } => {
|
||||
issue.edit_comment(
|
||||
id,
|
||||
body,
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
cobs::issue::Action::Edit { title } => {
|
||||
issue.edit(title, &signer)?;
|
||||
}
|
||||
}
|
||||
|
||||
if opts.announce() {
|
||||
node.announce_refs(rid)?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(cobs::issue::Issue::new(issue.id(), &issue, &aliases))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
use radicle::node::Handle;
|
||||
use radicle::patch::cache::Patches as _;
|
||||
use radicle::storage;
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle::{cob, git, identity, patch, Node};
|
||||
|
||||
use crate::cobs;
|
||||
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> {
|
||||
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));
|
||||
|
||||
Ok::<_, Error>(patches)
|
||||
}
|
||||
|
||||
fn revisions_by_patch(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
id: git::Oid,
|
||||
) -> Result<Option<Vec<cobs::patch::Revision>>, Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let patches = profile.patches(&repo)?;
|
||||
let revisions = patches.get(&id.into())?.map(|patch| {
|
||||
let aliases = &profile.aliases();
|
||||
|
||||
patch
|
||||
.revisions()
|
||||
.map(|(_, r)| cobs::patch::Revision::new(r.clone(), aliases))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
Ok::<_, Error>(revisions)
|
||||
}
|
||||
|
||||
fn revision_by_id(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
id: git::Oid,
|
||||
revision_id: git::Oid,
|
||||
) -> Result<Option<cobs::patch::Revision>, Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let patches = profile.patches(&repo)?;
|
||||
let revision = patches.get(&id.into())?.and_then(|patch| {
|
||||
let aliases = &profile.aliases();
|
||||
|
||||
patch
|
||||
.revision(&revision_id.into())
|
||||
.map(|r| cobs::patch::Revision::new(r.clone(), aliases))
|
||||
});
|
||||
|
||||
Ok::<_, Error>(revision)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PatchesMut: Profile {
|
||||
fn edit_patch(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
cob_id: git::Oid,
|
||||
action: cobs::patch::Action,
|
||||
opts: cobs::CobOptions,
|
||||
) -> Result<cobs::patch::Patch, Error> {
|
||||
let profile = self.profile();
|
||||
let mut node = Node::new(profile.socket());
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let signer = profile.signer()?;
|
||||
let aliases = profile.aliases();
|
||||
let mut patches = profile.patches_mut(&repo)?;
|
||||
let mut patch = patches.get_mut(&cob_id.into())?;
|
||||
|
||||
match action {
|
||||
cobs::patch::Action::RevisionEdit {
|
||||
revision,
|
||||
description,
|
||||
embeds,
|
||||
} => {
|
||||
patch.edit_revision(
|
||||
revision,
|
||||
description,
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
cobs::patch::Action::RevisionCommentRedact { revision, comment } => {
|
||||
patch.comment_redact(revision, comment, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::ReviewCommentRedact { review, comment } => {
|
||||
patch.redact_review_comment(review, comment, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::ReviewCommentReact {
|
||||
review,
|
||||
comment,
|
||||
reaction,
|
||||
active,
|
||||
} => {
|
||||
patch.react_review_comment(review, comment, reaction, active, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::ReviewCommentResolve { review, comment } => {
|
||||
patch.resolve_review_comment(review, comment, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::ReviewCommentUnresolve { review, comment } => {
|
||||
patch.unresolve_review_comment(review, comment, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::Edit { title, target } => {
|
||||
patch.edit(title, target, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::ReviewEdit {
|
||||
review,
|
||||
summary,
|
||||
verdict,
|
||||
labels,
|
||||
} => {
|
||||
patch.review_edit(review, verdict, summary, labels, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::Review {
|
||||
revision,
|
||||
summary,
|
||||
verdict,
|
||||
labels,
|
||||
} => {
|
||||
patch.review(revision, verdict, summary, labels, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::ReviewRedact { review } => {
|
||||
patch.redact_review(review, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::ReviewComment {
|
||||
review,
|
||||
body,
|
||||
location,
|
||||
reply_to,
|
||||
embeds,
|
||||
} => {
|
||||
patch.review_comment(
|
||||
review,
|
||||
body,
|
||||
location.map(|l| l.into()),
|
||||
reply_to,
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
cobs::patch::Action::ReviewCommentEdit {
|
||||
review,
|
||||
comment,
|
||||
body,
|
||||
embeds,
|
||||
} => {
|
||||
patch.edit_review_comment(
|
||||
review,
|
||||
comment,
|
||||
body,
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
cobs::patch::Action::Lifecycle { state } => {
|
||||
patch.lifecycle(state, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::Assign { assignees } => {
|
||||
patch.assign(assignees, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::Label { labels } => {
|
||||
patch.label(labels, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::RevisionReact {
|
||||
revision,
|
||||
reaction,
|
||||
location,
|
||||
active,
|
||||
} => {
|
||||
patch.react(
|
||||
revision,
|
||||
reaction,
|
||||
location.map(|l| l.into()),
|
||||
active,
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
cobs::patch::Action::RevisionComment {
|
||||
revision,
|
||||
location,
|
||||
body,
|
||||
reply_to,
|
||||
embeds,
|
||||
} => {
|
||||
patch.comment(
|
||||
revision,
|
||||
body,
|
||||
reply_to,
|
||||
location.map(|l| l.into()),
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
cobs::patch::Action::RevisionCommentEdit {
|
||||
revision,
|
||||
comment,
|
||||
body,
|
||||
embeds,
|
||||
} => {
|
||||
patch.comment_edit(
|
||||
revision,
|
||||
comment,
|
||||
body,
|
||||
embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
}
|
||||
cobs::patch::Action::RevisionCommentReact {
|
||||
revision,
|
||||
comment,
|
||||
reaction,
|
||||
active,
|
||||
} => {
|
||||
patch.comment_react(revision, comment, reaction, active, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::RevisionRedact { revision } => {
|
||||
patch.redact(revision, &signer)?;
|
||||
}
|
||||
cobs::patch::Action::Merge { .. } => {
|
||||
unimplemented!("We don't support merging of patches through the desktop")
|
||||
}
|
||||
cobs::patch::Action::Revision { .. } => {
|
||||
unimplemented!("We don't support creating new revisions through the desktop")
|
||||
}
|
||||
}
|
||||
|
||||
if opts.announce() {
|
||||
node.announce_refs(rid)?;
|
||||
}
|
||||
|
||||
Ok::<_, Error>(cobs::patch::Patch::new(*patch.id(), &patch, &aliases))
|
||||
}
|
||||
|
||||
/// Gets the draft review of the local user for a specific patch revision in a repository.
|
||||
///
|
||||
/// This Tauri command is used to retrieve a patch review draft for the local user
|
||||
/// on a given patch revision from a repository.
|
||||
/// It looks up the repository using the provided repository ID (`rid`) and patch object ID (`cob_id`),
|
||||
/// and gets the patch review of the local user associated with a specific revision (`revision_id`), if it exists.
|
||||
fn get_draft_review(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
cob_id: git::Oid,
|
||||
revision_id: patch::RevisionId,
|
||||
) -> Option<patch::Review> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid).ok()?;
|
||||
let signer = profile.signer().ok()?;
|
||||
let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
|
||||
let patches = cob::patch::Cache::no_cache(&drafts).ok()?;
|
||||
|
||||
let patch = patches.get(&cob_id.into()).ok()?;
|
||||
let revision = patch.and_then(|p| p.revision(&revision_id).cloned());
|
||||
let review = revision.and_then(|rev| rev.review_by(signer.public_key()).cloned());
|
||||
|
||||
review
|
||||
}
|
||||
|
||||
/// Edits a draft review for a specific patch revision in a repository.
|
||||
///
|
||||
/// This Tauri command allows users to edit a draft review for a specific patch review.
|
||||
/// The draft is associated with the user (signer) and the provided patch revision within the repository.
|
||||
fn edit_draft_review(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
cob_id: git::Oid,
|
||||
edit: cobs::patch::ReviewEdit,
|
||||
) -> Result<(), Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let signer = profile.signer()?;
|
||||
let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
|
||||
|
||||
let mut patches = cob::patch::Cache::no_cache(&drafts)?;
|
||||
let mut patch = patches.get_mut(&cob_id.into())?;
|
||||
patch.review_edit(
|
||||
edit.review_id,
|
||||
edit.verdict,
|
||||
edit.summary,
|
||||
edit.labels,
|
||||
&signer,
|
||||
)?;
|
||||
|
||||
patches.write(&cob_id.into())?;
|
||||
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
|
||||
/// Creates a draft review for a specific patch revision.
|
||||
///
|
||||
/// This Tauri command allows users to create a new draft review for a specific patch revision.
|
||||
/// The draft is associated with the user (signer) and the provided patch revision within the repository.
|
||||
fn create_draft_review(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
revision_id: cob::patch::RevisionId,
|
||||
cob_id: git::Oid,
|
||||
labels: Vec<cob::Label>,
|
||||
) -> Result<patch::ReviewId, Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let signer = profile.signer()?;
|
||||
let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
|
||||
|
||||
let mut patches = cob::patch::Cache::no_cache(&drafts)?;
|
||||
let mut patch = patches.get_mut(&cob_id.into())?;
|
||||
let revision = patch
|
||||
.revision(&revision_id)
|
||||
.ok_or_else(|| Error::WithHint {
|
||||
err: anyhow::anyhow!("patch revision not found"),
|
||||
hint: "Not able to find the specified patch revision.",
|
||||
})?;
|
||||
|
||||
revision
|
||||
.review_by(signer.public_key())
|
||||
.ok_or(Error::WithHint {
|
||||
err: anyhow::anyhow!("duplicate patch review found"),
|
||||
hint: "Found an existing draft patch review on this patch revision and repo.",
|
||||
})?;
|
||||
|
||||
let review_id = patch.review(
|
||||
revision.id(),
|
||||
Some(cob::patch::Verdict::Reject),
|
||||
None,
|
||||
labels,
|
||||
&signer,
|
||||
)?;
|
||||
|
||||
patches.write(&cob_id.into())?;
|
||||
|
||||
Ok::<_, Error>(review_id)
|
||||
}
|
||||
|
||||
/// Creates a new review comment on a draft review for a specific patch.
|
||||
///
|
||||
/// This Tauri command is used to add a comment to an existing draft review in a repository.
|
||||
/// It allows users to comment on a specific location in the code or leave general feedback
|
||||
/// on a review that belongs to a specific patch.
|
||||
fn create_draft_review_comment(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
cob_id: git::Oid,
|
||||
new: cobs::thread::CreateReviewComment,
|
||||
) -> Result<(), Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let signer = profile.signer()?;
|
||||
let drafts = storage::git::cob::DraftStore::new(&repo, *signer.public_key());
|
||||
|
||||
let mut patches = cob::patch::Cache::no_cache(&drafts)?;
|
||||
let mut patch = patches.get_mut(&cob_id.into())?;
|
||||
|
||||
patch.transaction("Review comments", &signer, |tx| {
|
||||
tx.review_comment(
|
||||
new.review_id,
|
||||
new.body,
|
||||
new.location.map(|l| l.into()),
|
||||
new.reply_to,
|
||||
new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
patches.write(&cob_id.into())?;
|
||||
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
use radicle::crypto::Verified;
|
||||
use radicle::identity::doc::PayloadId;
|
||||
use radicle::identity::DocAt;
|
||||
use radicle::issue::cache::Issues as _;
|
||||
use radicle::node::routing::Store;
|
||||
use radicle::patch::cache::Patches as _;
|
||||
use radicle::prelude::Doc;
|
||||
use radicle::storage;
|
||||
use radicle::storage::ReadRepository;
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle::{git, identity};
|
||||
use radicle_surf as surf;
|
||||
|
||||
use crate::cobs;
|
||||
use crate::cobs::diff::Options;
|
||||
use crate::error::Error;
|
||||
use crate::repo;
|
||||
use crate::traits::Profile;
|
||||
|
||||
pub trait Repo: Profile {
|
||||
fn list_repos(&self) -> Result<Vec<repo::RepoInfo>, Error> {
|
||||
let profile = self.profile();
|
||||
let storage = &profile.storage;
|
||||
let policies = profile.policies()?;
|
||||
let mut repos = storage.repositories()?.into_iter().collect::<Vec<_>>();
|
||||
repos.sort_by_key(|p| p.rid);
|
||||
|
||||
let infos = repos
|
||||
.into_iter()
|
||||
.filter_map(|info| {
|
||||
if !policies.is_seeding(&info.rid).unwrap_or_default() {
|
||||
return None;
|
||||
}
|
||||
let repo = profile.storage.repository(info.rid).ok()?;
|
||||
let DocAt { doc, .. } = repo.identity_doc().ok()?;
|
||||
let repo_info = self.repo_info(&repo, &doc).ok()?;
|
||||
|
||||
Some(repo_info)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok::<_, Error>(infos)
|
||||
}
|
||||
|
||||
fn repo_by_id(&self, rid: identity::RepoId) -> Result<repo::RepoInfo, Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let DocAt { doc, .. } = repo.identity_doc()?;
|
||||
|
||||
let repo_info = self.repo_info(&repo, &doc)?;
|
||||
|
||||
Ok::<_, Error>(repo_info)
|
||||
}
|
||||
|
||||
fn diff_stats(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
base: git::Oid,
|
||||
head: git::Oid,
|
||||
) -> Result<cobs::Stats, Error> {
|
||||
let profile = self.profile();
|
||||
let repo = radicle_surf::Repository::open(storage::git::paths::repository(
|
||||
&profile.storage,
|
||||
&rid,
|
||||
))?;
|
||||
let base = repo.commit(base)?;
|
||||
let commit = repo.commit(head)?;
|
||||
let diff = repo.diff(base.id, commit.id)?;
|
||||
let stats = diff.stats();
|
||||
|
||||
Ok::<_, Error>(cobs::Stats::new(stats))
|
||||
}
|
||||
|
||||
fn repo_info(
|
||||
&self,
|
||||
repo: &storage::git::Repository,
|
||||
doc: &Doc<Verified>,
|
||||
) -> Result<repo::RepoInfo, Error> {
|
||||
let profile = self.profile();
|
||||
let aliases = profile.aliases();
|
||||
let delegates = doc
|
||||
.delegates
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|did| cobs::Author::new(did, &aliases))
|
||||
.collect::<Vec<_>>();
|
||||
let db = profile.database()?;
|
||||
let seeding = db.count(&repo.id).unwrap_or_default();
|
||||
let project = doc.payload.get(&PayloadId::project()).and_then(|payload| {
|
||||
let (_, head) = repo.head().ok()?;
|
||||
let commit = repo.commit(head).ok()?;
|
||||
let patches = profile.patches(repo).ok()?;
|
||||
let patches = patches.counts().ok()?;
|
||||
let issues = profile.issues(repo).ok()?;
|
||||
let issues = issues.counts().ok()?;
|
||||
|
||||
let data: repo::ProjectPayloadData = (*payload).clone().try_into().ok()?;
|
||||
let meta = repo::ProjectPayloadMeta {
|
||||
issues,
|
||||
patches,
|
||||
head,
|
||||
last_commit_timestamp: commit.time().seconds() * 1000,
|
||||
};
|
||||
|
||||
Some(repo::ProjectPayload::new(data, meta))
|
||||
});
|
||||
|
||||
Ok::<_, Error>(repo::RepoInfo {
|
||||
payloads: repo::SupportedPayloads { project },
|
||||
delegates,
|
||||
threshold: doc.threshold,
|
||||
visibility: doc.visibility.clone().into(),
|
||||
rid: repo.id,
|
||||
seeding,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_diff(&self, rid: identity::RepoId, options: Options) -> Result<surf::diff::Diff, Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?.backend;
|
||||
let base = repo.find_commit(*options.base)?;
|
||||
let head = repo.find_commit(*options.head)?;
|
||||
|
||||
let mut opts = git::raw::DiffOptions::new();
|
||||
opts.patience(true)
|
||||
.minimal(true)
|
||||
.context_lines(options.unified);
|
||||
|
||||
let mut find_opts = git::raw::DiffFindOptions::new();
|
||||
find_opts.exact_match_only(true);
|
||||
find_opts.all(true);
|
||||
|
||||
let left = base.tree()?;
|
||||
let right = head.tree()?;
|
||||
|
||||
let mut diff = repo.diff_tree_to_tree(Some(&left), Some(&right), Some(&mut opts))?;
|
||||
diff.find_similar(Some(&mut find_opts))?;
|
||||
let diff = surf::diff::Diff::try_from(diff)?;
|
||||
|
||||
Ok::<_, Error>(diff)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use localtime::LocalTime;
|
||||
|
||||
use radicle::cob;
|
||||
use radicle::git;
|
||||
use radicle::identity;
|
||||
use radicle::node::Handle;
|
||||
use radicle::storage::ReadRepository;
|
||||
use radicle::storage::ReadStorage;
|
||||
use radicle::Node;
|
||||
|
||||
use crate::cobs;
|
||||
use crate::error::Error;
|
||||
use crate::traits::Profile;
|
||||
|
||||
pub trait Thread: Profile {
|
||||
fn get_embed(&self, rid: identity::RepoId, oid: git::Oid) -> Result<String, Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let blob = repo.blob(oid)?;
|
||||
|
||||
Ok::<_, Error>(STANDARD.encode(blob.content()))
|
||||
}
|
||||
|
||||
fn save_embed(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
name: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<git::Oid, Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let embed = radicle::cob::Embed::<git::Oid>::store(name, bytes, &repo.backend)?;
|
||||
|
||||
Ok(embed.oid())
|
||||
}
|
||||
|
||||
fn create_issue_comment(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
new: cobs::thread::NewIssueComment,
|
||||
opts: cobs::CobOptions,
|
||||
) -> Result<cobs::thread::Comment<cobs::Never>, Error> {
|
||||
let profile = self.profile();
|
||||
let aliases = &profile.aliases();
|
||||
let mut node = Node::new(profile.socket());
|
||||
let signer = profile.signer()?;
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let mut issues = profile.issues_mut(&repo)?;
|
||||
let mut issue = issues.get_mut(&new.id.into())?;
|
||||
let id = new.reply_to.unwrap_or_else(|| {
|
||||
let (root_id, _) = issue.root();
|
||||
*root_id
|
||||
});
|
||||
let n = new.clone();
|
||||
let oid = issue.comment(
|
||||
n.body,
|
||||
id,
|
||||
n.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
|
||||
if opts.announce() {
|
||||
node.announce_refs(rid)?;
|
||||
}
|
||||
|
||||
Ok(cobs::thread::Comment::<cobs::Never>::new(
|
||||
oid,
|
||||
cob::thread::Comment::new(
|
||||
*signer.public_key(),
|
||||
new.body,
|
||||
id.into(),
|
||||
None,
|
||||
new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
LocalTime::now().into(),
|
||||
),
|
||||
aliases,
|
||||
))
|
||||
}
|
||||
|
||||
fn create_patch_comment(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
new: cobs::thread::NewPatchComment,
|
||||
opts: cobs::CobOptions,
|
||||
) -> Result<cobs::thread::Comment<cobs::thread::CodeLocation>, Error> {
|
||||
let profile = self.profile();
|
||||
let aliases = &profile.aliases();
|
||||
let mut node = Node::new(profile.socket());
|
||||
let signer = profile.signer()?;
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
let mut patches = profile.patches_mut(&repo)?;
|
||||
let mut patch = patches.get_mut(&new.id.into())?;
|
||||
let n = new.clone();
|
||||
let oid = patch.comment(
|
||||
new.revision.into(),
|
||||
n.body,
|
||||
n.reply_to,
|
||||
n.location.map(|l| l.into()),
|
||||
n.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
&signer,
|
||||
)?;
|
||||
|
||||
if opts.announce() {
|
||||
node.announce_refs(rid)?;
|
||||
}
|
||||
|
||||
Ok(cobs::thread::Comment::<cobs::thread::CodeLocation>::new(
|
||||
oid,
|
||||
cob::thread::Comment::new(
|
||||
*signer.public_key(),
|
||||
new.body,
|
||||
new.reply_to,
|
||||
new.location.map(|l| l.into()),
|
||||
new.embeds.into_iter().map(|e| e.into()).collect::<Vec<_>>(),
|
||||
LocalTime::now().into(),
|
||||
),
|
||||
aliases,
|
||||
))
|
||||
}
|
||||
}
|
||||
Generated
+2810
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "test-http-api"
|
||||
description = "HTTP Test API"
|
||||
homepage = "https://radicle.xyz"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.90" }
|
||||
axum = { version = "0.7.5", default-features = false, features = ["json", "query", "tokio", "http1"] }
|
||||
hyper = { version = "1.4", default-features = false }
|
||||
lexopt = { version = "0.3.0" }
|
||||
radicle = { git = "https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git" }
|
||||
radicle-surf = { version = "0.22.1", default-features = false, features = ["serde"] }
|
||||
radicle-types = { path = "../radicle-types" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
thiserror = { version = "1" }
|
||||
tokio = { version = "1.40", default-features = false, features = ["macros", "rt-multi-thread"] }
|
||||
tower-http = { version = "0.5.2", default-features = false, features = ["cors", "set-header"] }
|
||||
@@ -0,0 +1,306 @@
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum::response::{IntoResponse, Json};
|
||||
use axum::routing::post;
|
||||
use axum::Router;
|
||||
use hyper::header::CONTENT_TYPE;
|
||||
use hyper::Method;
|
||||
use radicle::cob::TypeName;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_http::cors::{self, CorsLayer};
|
||||
|
||||
use radicle::{git, identity};
|
||||
use radicle_types as types;
|
||||
use radicle_types::cobs::issue::{Action, NewIssue};
|
||||
use radicle_types::cobs::CobOptions;
|
||||
use radicle_types::traits::auth::Auth;
|
||||
use radicle_types::traits::cobs::Cobs;
|
||||
use radicle_types::traits::issue::{Issues, IssuesMut};
|
||||
use radicle_types::traits::patch::{Patches, PatchesMut};
|
||||
use radicle_types::traits::repo::Repo;
|
||||
use radicle_types::traits::thread::Thread;
|
||||
use radicle_types::traits::Profile;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
profile: Arc<radicle::Profile>,
|
||||
}
|
||||
|
||||
impl Auth for Context {}
|
||||
impl Repo for Context {}
|
||||
impl Cobs for Context {}
|
||||
impl Thread for Context {}
|
||||
impl Issues for Context {}
|
||||
impl IssuesMut for Context {}
|
||||
impl Patches for Context {}
|
||||
impl PatchesMut for Context {}
|
||||
impl Profile for Context {
|
||||
fn profile(&self) -> radicle::Profile {
|
||||
self.profile.deref().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(profile: Arc<radicle::Profile>) -> Self {
|
||||
Self { profile }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn router(ctx: Context) -> Router {
|
||||
Router::new()
|
||||
.route("/config", post(config_handler))
|
||||
.route("/authenticate", post(auth_handler))
|
||||
.route("/list_repos", post(repo_root_handler))
|
||||
.route("/repo_by_id", post(repo_handler))
|
||||
.route("/diff_stats", post(diff_stats_handler))
|
||||
.route("/activity_by_id", post(activity_handler))
|
||||
.route("/get_diff", post(diff_handler))
|
||||
.route("/list_issues", post(issues_handler))
|
||||
.route("/create_issue", post(create_issue_handler))
|
||||
.route("/edit_issue", post(edit_issue_handler))
|
||||
.route("/issue_by_id", post(issue_handler))
|
||||
.route("/list_patches", post(patches_handler))
|
||||
.route("/patch_by_id", post(patch_handler))
|
||||
.route("/revisions_by_patch", post(revision_handler))
|
||||
.route("/get_file_by_oid", post(get_embeds_handler))
|
||||
.route("/save_embed", post(save_embed_handler))
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin(cors::Any)
|
||||
.allow_methods([Method::POST, Method::GET])
|
||||
.allow_headers([CONTENT_TYPE]),
|
||||
)
|
||||
.with_state(ctx)
|
||||
}
|
||||
|
||||
async fn config_handler(State(ctx): State<Context>) -> impl IntoResponse {
|
||||
let config = ctx.config();
|
||||
|
||||
Ok::<_, Error>(Json(config))
|
||||
}
|
||||
|
||||
async fn auth_handler(State(ctx): State<Context>) -> impl IntoResponse {
|
||||
ctx.authenticate()?;
|
||||
|
||||
Ok::<_, Error>(Json(()))
|
||||
}
|
||||
|
||||
async fn repo_root_handler(State(ctx): State<Context>) -> impl IntoResponse {
|
||||
let repos = ctx.list_repos()?;
|
||||
|
||||
Ok::<_, Error>(Json(repos))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct RepoBody {
|
||||
pub rid: identity::RepoId,
|
||||
}
|
||||
|
||||
async fn repo_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(RepoBody { rid }): Json<RepoBody>,
|
||||
) -> impl IntoResponse {
|
||||
let info = ctx.repo_by_id(rid)?;
|
||||
|
||||
Ok::<_, Error>(Json(info))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DiffStatsBody {
|
||||
pub rid: identity::RepoId,
|
||||
pub base: git::Oid,
|
||||
pub head: git::Oid,
|
||||
}
|
||||
|
||||
async fn diff_stats_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(DiffStatsBody { rid, base, head }): Json<DiffStatsBody>,
|
||||
) -> impl IntoResponse {
|
||||
let info = ctx.diff_stats(rid, base, head)?;
|
||||
|
||||
Ok::<_, Error>(Json(info))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DiffBody {
|
||||
pub rid: identity::RepoId,
|
||||
pub options: types::cobs::diff::Options,
|
||||
}
|
||||
|
||||
async fn diff_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(DiffBody { rid, options }): Json<DiffBody>,
|
||||
) -> impl IntoResponse {
|
||||
let info = ctx.get_diff(rid, options)?;
|
||||
|
||||
Ok::<_, Error>(Json(info))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct IssuesBody {
|
||||
pub rid: identity::RepoId,
|
||||
pub status: Option<types::cobs::query::IssueStatus>,
|
||||
}
|
||||
|
||||
async fn issues_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(IssuesBody { rid, status }): Json<IssuesBody>,
|
||||
) -> impl IntoResponse {
|
||||
let issues = ctx.list_issues(rid, status)?;
|
||||
|
||||
Ok::<_, Error>(Json(issues))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CreateIssuesBody {
|
||||
pub rid: identity::RepoId,
|
||||
pub opts: CobOptions,
|
||||
pub new: NewIssue,
|
||||
}
|
||||
|
||||
async fn create_issue_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(CreateIssuesBody { rid, opts, new }): Json<CreateIssuesBody>,
|
||||
) -> impl IntoResponse {
|
||||
let issues = ctx.create_issue(rid, new, opts)?;
|
||||
|
||||
Ok::<_, Error>(Json(issues))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct EditIssuesBody {
|
||||
pub rid: identity::RepoId,
|
||||
pub cob_id: git::Oid,
|
||||
pub action: Action,
|
||||
pub opts: CobOptions,
|
||||
}
|
||||
|
||||
async fn edit_issue_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(EditIssuesBody {
|
||||
rid,
|
||||
cob_id,
|
||||
action,
|
||||
opts,
|
||||
}): Json<EditIssuesBody>,
|
||||
) -> impl IntoResponse {
|
||||
let issues = ctx.edit_issue(rid, cob_id, action, opts)?;
|
||||
|
||||
Ok::<_, Error>(Json(issues))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ActivityBody {
|
||||
pub rid: identity::RepoId,
|
||||
pub type_name: TypeName,
|
||||
pub id: git::Oid,
|
||||
}
|
||||
|
||||
async fn activity_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(ActivityBody { rid, type_name, id }): Json<ActivityBody>,
|
||||
) -> impl IntoResponse {
|
||||
let activity = ctx.activity_by_id(rid, type_name, id)?;
|
||||
|
||||
Ok::<_, Error>(Json(activity))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct EmbedBody {
|
||||
pub rid: identity::RepoId,
|
||||
pub oid: git::Oid,
|
||||
}
|
||||
|
||||
async fn get_embeds_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(EmbedBody { rid, oid }): Json<EmbedBody>,
|
||||
) -> impl IntoResponse {
|
||||
let embed = ctx.get_embed(rid, oid)?;
|
||||
|
||||
Ok::<_, Error>(Json(embed))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CreateEmbedBody {
|
||||
pub rid: identity::RepoId,
|
||||
pub name: String,
|
||||
pub content: Vec<u8>,
|
||||
}
|
||||
|
||||
async fn save_embed_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(CreateEmbedBody { rid, name, content }): Json<CreateEmbedBody>,
|
||||
) -> impl IntoResponse {
|
||||
let embed = ctx.save_embed(rid, &name, &content)?;
|
||||
|
||||
Ok::<_, Error>(Json(embed))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct IssueBody {
|
||||
pub rid: identity::RepoId,
|
||||
pub id: git::Oid,
|
||||
}
|
||||
|
||||
async fn issue_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(IssueBody { rid, id }): Json<IssueBody>,
|
||||
) -> impl IntoResponse {
|
||||
let issue = ctx.issue_by_id(rid, id)?;
|
||||
|
||||
Ok::<_, Error>(Json(issue))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PatchesBody {
|
||||
pub rid: identity::RepoId,
|
||||
pub page: Option<usize>,
|
||||
pub per_page: Option<usize>,
|
||||
pub status: Option<types::cobs::query::PatchStatus>,
|
||||
}
|
||||
|
||||
async fn patches_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(PatchesBody {
|
||||
rid,
|
||||
page,
|
||||
per_page,
|
||||
status,
|
||||
}): Json<PatchesBody>,
|
||||
) -> impl IntoResponse {
|
||||
let patches = ctx.list_patches(rid, status, page, per_page)?;
|
||||
|
||||
Ok::<_, Error>(Json(patches))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct PatchBody {
|
||||
pub rid: identity::RepoId,
|
||||
pub id: git::Oid,
|
||||
}
|
||||
|
||||
async fn patch_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(PatchBody { rid, id }): Json<PatchBody>,
|
||||
) -> impl IntoResponse {
|
||||
let patch = ctx.get_patch(rid, id)?;
|
||||
|
||||
Ok::<_, Error>(Json(patch))
|
||||
}
|
||||
|
||||
async fn revision_handler(
|
||||
State(ctx): State<Context>,
|
||||
Json(PatchBody { rid, id }): Json<PatchBody>,
|
||||
) -> impl IntoResponse {
|
||||
let revisions = ctx.revisions_by_patch(rid, id)?;
|
||||
|
||||
Ok::<_, Error>(Json(revisions))
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// radicle_types error.
|
||||
#[error(transparent)]
|
||||
Types(#[from] radicle_types::error::Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, msg) = match self {
|
||||
other => {
|
||||
if cfg!(debug_assertions) {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Some(other.to_string()))
|
||||
} else {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, None)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let body = Json(json!({
|
||||
"error": msg.or_else(|| status.canonical_reason().map(|r| r.to_string())),
|
||||
"code": status.as_u16()
|
||||
}));
|
||||
|
||||
(status, body).into_response()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Router;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use radicle::Profile;
|
||||
|
||||
mod api;
|
||||
mod error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Options {
|
||||
pub listen: SocketAddr,
|
||||
}
|
||||
|
||||
pub async fn run(options: Options) -> anyhow::Result<()> {
|
||||
let profile = Profile::load()?;
|
||||
let listener = TcpListener::bind(options.listen).await?;
|
||||
let app = router(profile)?.into_make_service_with_connect_info::<SocketAddr>();
|
||||
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
fn router(profile: Profile) -> anyhow::Result<Router> {
|
||||
let profile = Arc::new(profile);
|
||||
let ctx = api::Context::new(profile);
|
||||
|
||||
Ok(api::router(ctx))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::process;
|
||||
|
||||
use test_http_api as api;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let options = parse_options()?;
|
||||
match api::run(options).await {
|
||||
Ok(()) => {}
|
||||
Err(_) => {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_options() -> Result<api::Options, lexopt::Error> {
|
||||
use lexopt::prelude::*;
|
||||
|
||||
let mut parser = lexopt::Parser::from_env();
|
||||
let mut listen = None;
|
||||
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Long("listen") => {
|
||||
let addr = parser.value()?.parse()?;
|
||||
listen = Some(addr);
|
||||
}
|
||||
_ => return Err(arg.unexpected()),
|
||||
}
|
||||
}
|
||||
Ok(api::Options {
|
||||
listen: listen.unwrap_or_else(|| ([0, 0, 0, 0], 8080).into()),
|
||||
})
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"start:http": "cargo run --manifest-path ./crates/test-http-api/Cargo.toml",
|
||||
"build": "vite build && scripts/copy-katex-assets && scripts/install-twemoji-assets",
|
||||
"postinstall": "scripts/copy-katex-assets && scripts/install-twemoji-assets",
|
||||
"preview": "vite preview",
|
||||
|
||||
+9
-7
@@ -3,7 +3,7 @@
|
||||
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
import * as router from "@app/lib/router";
|
||||
@@ -25,13 +25,15 @@
|
||||
let unlistenNodeEvents: UnlistenFn | undefined = undefined;
|
||||
|
||||
onMount(async () => {
|
||||
unlistenEvents = await listen("event", event => {
|
||||
console.log(event.payload);
|
||||
});
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
unlistenEvents = await listen("event", event => {
|
||||
console.log(event.payload);
|
||||
});
|
||||
|
||||
unlistenNodeEvents = await listen<boolean>("node_running", event => {
|
||||
nodeRunning.set(event.payload);
|
||||
});
|
||||
unlistenNodeEvents = await listen<boolean>("node_running", event => {
|
||||
nodeRunning.set(event.payload);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke("authenticate");
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderer, markdownWithExtensions } from "@app/lib/markdown";
|
||||
import { highlight } from "@app/lib/syntax";
|
||||
import { twemoji, scrollIntoView, isCommit } from "@app/lib/utils";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
|
||||
export let rid: string;
|
||||
export let content: string;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
patchStatusBackgroundColor,
|
||||
patchStatusColor,
|
||||
} from "@app/lib/utils";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import { onMount } from "svelte";
|
||||
import { push } from "@app/lib/router";
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as tauri from "@tauri-apps/api/core";
|
||||
|
||||
export async function invoke<T = null>(
|
||||
cmd: string,
|
||||
args?: tauri.InvokeArgs,
|
||||
options?: tauri.InvokeOptions,
|
||||
): Promise<T> {
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
return tauri.invoke(cmd, args, options);
|
||||
} else {
|
||||
return fetch(`http://127.0.0.1:8080/${cmd}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(args),
|
||||
})
|
||||
.then(data => data.json())
|
||||
.catch(() => {
|
||||
throw Error(`Issue with HTTP Route: ${JSON.stringify({ cmd, args })}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { Config } from "@bindings/config/Config";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
import type { LoadedRepoRoute, RepoRoute } from "@app/views/repo/router";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
|
||||
import {
|
||||
loadCreateIssue,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import type { Issue } from "@bindings/cob/issue/Issue";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
|
||||
import { issueStatusColor } from "@app/lib/utils";
|
||||
import * as router from "@app/lib/router";
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
publicKeyFromDid,
|
||||
scrollIntoView,
|
||||
} from "@app/lib/utils";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
|
||||
import { announce } from "@app/components/AnnounceSwitch.svelte";
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
formatTimestamp,
|
||||
patchStatusColor,
|
||||
} from "@app/lib/utils";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
|
||||
import Border from "@app/components/Border.svelte";
|
||||
import CopyableId from "@app/components/CopyableId.svelte";
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import type { PatchStatus } from "./router";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
|
||||
import Layout from "./Layout.svelte";
|
||||
import Border from "@app/components/Border.svelte";
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Patch } from "@bindings/cob/patch/Patch";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
import type { Revision } from "@bindings/cob/patch/Revision";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import { unreachable } from "@app/lib/utils";
|
||||
|
||||
export type IssueStatus = "all" | Issue["state"]["status"];
|
||||
|
||||
Reference in New Issue
Block a user