diff --git a/crates/radicle-tauri/src/commands/auth.rs b/crates/radicle-tauri/src/commands/auth.rs index 5d2dbc363..dd86d11d9 100644 --- a/crates/radicle-tauri/src/commands/auth.rs +++ b/crates/radicle-tauri/src/commands/auth.rs @@ -1,11 +1,5 @@ -use std::str::FromStr; - -use radicle::crypto::Seed; -use radicle::crypto::ssh::{self, Passphrase}; -use radicle::node::Alias; -use radicle::profile::env; +use radicle::crypto::ssh::Passphrase; use radicle_types::error::Error; -use ssh_key::rand_core::{OsRng, RngCore}; use crate::AppState; @@ -14,78 +8,10 @@ pub fn authenticate( ctx: tauri::State, passphrase: Option, ) -> 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) { - return Ok(()); - } - - match passphrase { - Some(passphrase) => { - profile.keystore.secret_key(Some(passphrase.clone()))?; - register(&mut agent, profile, passphrase) - } - None => Err(Error::Crypto( - radicle::crypto::ssh::keystore::Error::PassphraseMissing, - )), - } - } - Err(e) if e.is_not_running() => Err(Error::AgentNotRunning)?, - Err(e) => Err(e)?, - } + radicle_types::auth::authenticate(&ctx.profile, passphrase) } #[tauri::command] pub(crate) fn init(alias: String, passphrase: Passphrase) -> Result<(), Error> { - let home = radicle::profile::home()?; - let alias = Alias::from_str(&alias)?; - - if passphrase.is_empty() { - return Err(Error::Crypto( - radicle::crypto::ssh::keystore::Error::PassphraseMissing, - )); - } - let seed = env::seed().unwrap_or_else(|| { - let mut bytes = [0u8; 32]; - OsRng.fill_bytes(&mut bytes); - Seed::new(bytes) - }); - let profile = radicle::Profile::init(home, alias, Some(passphrase.clone()), seed)?; - match ssh::agent::Agent::connect() { - Ok(mut agent) => register(&mut agent, &profile, passphrase.clone())?, - Err(e) if e.is_not_running() => return Err(Error::AgentNotRunning), - Err(e) => Err(e)?, - } - - Ok(()) -} - -pub fn register( - agent: &mut ssh::agent::Agent, - profile: &radicle::Profile, - passphrase: ssh::Passphrase, -) -> Result<(), Error> { - let secret = profile - .keystore - .secret_key(Some(passphrase)) - .map_err(|e| { - if e.is_crypto_err() { - Error::Crypto(radicle::crypto::ssh::keystore::Error::Ssh( - ssh_key::Error::Crypto, - )) - } else { - e.into() - } - })? - .ok_or(Error::Crypto(radicle::crypto::ssh::keystore::Error::Ssh( - ssh_key::Error::Crypto, - )))?; - - agent.register(&secret)?; - - Ok(()) + radicle_types::auth::init(alias, passphrase) } diff --git a/crates/radicle-tauri/src/commands/cob/issue.rs b/crates/radicle-tauri/src/commands/cob/issue.rs index 44385a031..c63b40da7 100644 --- a/crates/radicle-tauri/src/commands/cob/issue.rs +++ b/crates/radicle-tauri/src/commands/cob/issue.rs @@ -1,8 +1,5 @@ -use std::ops::ControlFlow; - use radicle::git; use radicle::identity; -use radicle::storage::ReadStorage; use radicle::issue::TYPENAME; use radicle_types as types; @@ -97,30 +94,5 @@ pub async fn rebuild_issue_cache( rid: identity::RepoId, on_event: tauri::ipc::Channel, ) -> Result<(), Error> { - let repo = ctx.profile.storage.repository(rid)?; - let signer = ctx.profile.signer()?; - let mut issues = ctx.profile.issues_mut(&repo, &signer)?; - on_event.send(types::cobs::CacheEvent::Started { rid })?; - issues.write_all(|result, progress| { - match result { - Ok((id, _)) => { - if on_event - .send(types::cobs::CacheEvent::Progress { - rid, - oid: **id, - current: progress.current(), - total: progress.total(), - }) - .is_err() - { - log::error!("Failed to send progress"); - } - } - Err(err) => log::warn!("Failed to retrieve issue: {err}"), - }; - ControlFlow::Continue(()) - })?; - on_event.send(types::cobs::CacheEvent::Finished { rid })?; - - Ok(()) + ctx.rebuild_issue_cache(rid, |event| on_event.send(event).map_err(Error::from)) } diff --git a/crates/radicle-tauri/src/commands/cob/patch.rs b/crates/radicle-tauri/src/commands/cob/patch.rs index d015e45ec..2ad210cd0 100644 --- a/crates/radicle-tauri/src/commands/cob/patch.rs +++ b/crates/radicle-tauri/src/commands/cob/patch.rs @@ -1,5 +1,3 @@ -use std::ops::ControlFlow; - use radicle::patch::{ReviewId, TYPENAME}; use radicle::storage::{ReadRepository as _, ReadStorage}; use radicle::{git, identity}; @@ -97,30 +95,5 @@ pub async fn rebuild_patch_cache( rid: identity::RepoId, on_event: tauri::ipc::Channel, ) -> Result<(), Error> { - let repo = ctx.profile.storage.repository(rid)?; - let signer = ctx.profile.signer()?; - let mut patches = ctx.profile.patches_mut(&repo, &signer)?; - on_event.send(types::cobs::CacheEvent::Started { rid })?; - patches.write_all(|result, progress| { - match result { - Ok((id, _)) => { - if on_event - .send(cobs::CacheEvent::Progress { - rid, - oid: **id, - current: progress.current(), - total: progress.total(), - }) - .is_err() - { - log::error!("Failed to send progress"); - } - } - Err(err) => log::warn!("Failed to retrieve patch: {err}"), - }; - ControlFlow::Continue(()) - })?; - on_event.send(types::cobs::CacheEvent::Finished { rid })?; - - Ok(()) + ctx.rebuild_patch_cache(rid, |event| on_event.send(event).map_err(Error::from)) } diff --git a/crates/radicle-tauri/src/commands/inbox.rs b/crates/radicle-tauri/src/commands/inbox.rs index 4c0994aed..dbe43c9f6 100644 --- a/crates/radicle-tauri/src/commands/inbox.rs +++ b/crates/radicle-tauri/src/commands/inbox.rs @@ -1,15 +1,9 @@ -use radicle::identity; -use radicle::issue::cache::Issues; -use radicle::node; -use radicle::patch::cache::Patches; -use radicle::storage::{ReadRepository, ReadStorage}; - use radicle_types::AppState; -use radicle_types::domain::inbox::models::notification::{self, RepoGroupByItem}; +use radicle_types::domain::inbox::models::notification; use radicle_types::domain::inbox::service::Service; -use radicle_types::domain::inbox::traits::InboxService; use radicle_types::error::Error; use radicle_types::outbound::sqlite::Sqlite; +use radicle_types::traits::inbox::Inbox; #[tauri::command] pub fn list_notifications( @@ -17,177 +11,15 @@ pub fn list_notifications( sqlite_service: tauri::State>, params: notification::RepoGroupParams, ) -> Result { - let profile = &ctx.profile; - let aliases = profile.aliases(); - let repos_with_groups = sqlite_service.repo_group(params.clone())?; - - let mut repo_counts = std::collections::HashMap::new(); - for (repo_id, count) in (sqlite_service.counts_by_repo()?).flatten() { - repo_counts.insert(repo_id, count); - } - - let take = if params.all.unwrap_or(false) { - usize::MAX - } else { - params.take.unwrap_or(20) - }; - - let mut grouped_repos = std::collections::HashMap::new(); - for (repo_id, group) in repos_with_groups { - grouped_repos - .entry(repo_id) - .or_insert_with(Vec::new) - .extend(group); - } - - let mut result = Vec::new(); - - for (repo_id, all) in grouped_repos { - let repo = match profile.storage.repository(repo_id) { - Ok(r) => r, - Err(e) => { - log::error!("Failed to open repository {}: {}", repo_id, e); - continue; - } - }; - - let name = match repo.identity_doc() { - Ok(identity::DocAt { doc, .. }) => match doc.project() { - Ok(project) => project.name().to_string(), - Err(_) => format!("Unknown project in {}", repo_id), - }, - Err(_) => format!("Unknown project in {}", repo_id), - }; - - let patches = match profile.patches(&repo) { - Ok(p) => p, - Err(e) => { - log::error!("Failed to get patches for {}: {}", repo_id, e); - continue; - } - }; - let issues = match profile.issues(&repo) { - Ok(i) => i, - Err(e) => { - log::error!("Failed to get issues for {}: {}", repo_id, e); - continue; - } - }; - - let content = all - .into_iter() - .take(take) - .map(|(qualified, n)| { - n.into_iter() - .filter_map(|s| { - let update: notification::RefUpdate = - (qualified.clone().into_refstring(), s.new, s.old).into(); - let update: radicle::storage::RefUpdate = update.into(); - let kind = - node::notifications::NotificationKind::try_from(qualified.clone()) - .ok()?; - - match kind { - node::notifications::NotificationKind::Cob { ref typed_id } => { - if typed_id.is_patch() { - let actions = notification::actions( - typed_id.type_name.clone(), - typed_id.id, - update.old(), - update.new(), - &repo, - &aliases, - ) - .unwrap_or_default(); - - match patches.get(&typed_id.id) { - Ok(Some(p)) => Some(notification::NotificationItem::Patch( - notification::Patch { - row_id: s.row_id, - id: typed_id.id, - update: update.into(), - timestamp: s.timestamp, - title: p.title().to_string(), - status: (p.state().clone()).into(), - actions, - repo_id: Some(repo_id), - }, - )), - Ok(None) => { - log::error!("No patch found"); - None - } - Err(e) => { - log::error!("{}", e); - None - } - } - } else if typed_id.is_issue() { - let actions = notification::actions( - typed_id.type_name.clone(), - typed_id.id, - update.old(), - update.new(), - &repo, - &aliases, - ) - .unwrap_or_default(); - - match issues.get(&typed_id.id) { - Ok(Some(i)) => Some(notification::NotificationItem::Issue( - notification::Issue { - row_id: s.row_id, - id: typed_id.id, - update: update.into(), - timestamp: s.timestamp, - title: i.title().to_string(), - status: (*i.state()).into(), - actions, - repo_id: Some(repo_id), - }, - )), - Ok(None) => { - log::error!("No issue found"); - None - } - Err(e) => { - log::error!("{}", e); - None - } - } - } else { - None - } - } - _ => None, - } - }) - .collect::>() - }) - .filter(|v| !v.is_empty()) - .collect::(); - - if !content.is_empty() { - let count = repo_counts - .get(&repo_id) - .copied() - .unwrap_or_else(|| content.iter().map(|items| items.len()).sum()); - - result.push(notification::NotificationsByRepo { - rid: repo_id, - name, - notifications: content, - count, - }); - } - } - - Ok(result) + ctx.list_notifications(&sqlite_service, params) } #[tauri::command] -pub fn notification_count(inbox: tauri::State>) -> Result { - inbox.notification_count().map_err(Error::from) +pub fn notification_count( + ctx: tauri::State, + inbox: tauri::State>, +) -> Result { + ctx.notification_count(&inbox) } #[tauri::command] @@ -195,13 +27,5 @@ pub fn clear_notifications( ctx: tauri::State, params: notification::SetStatusNotifications, ) -> Result<(), Error> { - let profile = &ctx.profile; - let mut notifications = profile.notifications_mut()?; - match params { - notification::SetStatusNotifications::Ids(ids) => notifications.clear(&ids)?, - notification::SetStatusNotifications::Repo(repo) => notifications.clear_by_repo(&repo)?, - notification::SetStatusNotifications::All => notifications.clear_all()?, - }; - - Ok(()) + ctx.clear_notifications(params) } diff --git a/crates/radicle-tauri/src/commands/startup.rs b/crates/radicle-tauri/src/commands/startup.rs index 73aea4ce0..3d2ff8896 100644 --- a/crates/radicle-tauri/src/commands/startup.rs +++ b/crates/radicle-tauri/src/commands/startup.rs @@ -19,17 +19,7 @@ pub(crate) fn version(app: AppHandle) -> Result { #[tauri::command] pub(crate) fn check_radicle_cli(ctx: tauri::State) -> Result<(), Error> { - // Where the official installer puts it, even if it never reached `PATH`. - let rad = ctx.profile().home().path().join("bin").join("rad"); - if rad.exists() { - return Ok(()); - } - - if radicle_types::binaries::rad().is_some() { - return Ok(()); - } - - Err(Error::RadicleNotInstalled) + radicle_types::binaries::check_radicle_cli(&ctx.profile()) } #[tauri::command] @@ -40,10 +30,9 @@ pub(crate) fn git_info() -> GitInfo { #[tauri::command] pub(crate) fn startup(app: AppHandle) -> Result { let profile = radicle::Profile::load()?; - let home = profile.home(); - - let cobs_cache = radicle::cob::cache::Store::open(home.cobs().join(COBS_DB_FILE))?; - cobs_cache.check_version()?; + let state = AppState { profile }; + state.check_cobs_cache()?; + let profile = &state.profile; let inbox_db = radicle_types::outbound::sqlite::Sqlite::reader( profile.node().join(NOTIFICATIONS_DB_FILE), @@ -70,7 +59,6 @@ pub(crate) fn startup(app: AppHandle) -> Result { } }); - let state = AppState { profile }; app.manage(state.clone()); Ok(state.config()) diff --git a/crates/radicle-types/src/auth.rs b/crates/radicle-types/src/auth.rs new file mode 100644 index 000000000..d7875865a --- /dev/null +++ b/crates/radicle-types/src/auth.rs @@ -0,0 +1,86 @@ +use std::str::FromStr; + +use crate::error::Error; +use radicle::crypto::Seed; +use radicle::crypto::ssh::{self, Passphrase}; +use radicle::node::Alias; +use radicle::profile::env; +use ssh_key::rand_core::{OsRng, RngCore}; + +pub fn authenticate( + profile: &radicle::Profile, + passphrase: Option, +) -> Result<(), Error> { + if !profile.keystore.is_encrypted()? { + return Ok(()); + } + match ssh::agent::Agent::connect() { + Ok(mut agent) => { + if agent.request_identities()?.contains(&profile.public_key) { + return Ok(()); + } + + match passphrase { + Some(passphrase) => { + profile.keystore.secret_key(Some(passphrase.clone()))?; + register(&mut agent, profile, passphrase) + } + None => Err(Error::Crypto( + radicle::crypto::ssh::keystore::Error::PassphraseMissing, + )), + } + } + Err(e) if e.is_not_running() => Err(Error::AgentNotRunning)?, + Err(e) => Err(e)?, + } +} + +pub fn init(alias: String, passphrase: Passphrase) -> Result<(), Error> { + let home = radicle::profile::home()?; + let alias = Alias::from_str(&alias)?; + + if passphrase.is_empty() { + return Err(Error::Crypto( + radicle::crypto::ssh::keystore::Error::PassphraseMissing, + )); + } + let seed = env::seed().unwrap_or_else(|| { + let mut bytes = [0u8; 32]; + OsRng.fill_bytes(&mut bytes); + Seed::new(bytes) + }); + let profile = radicle::Profile::init(home, alias, Some(passphrase.clone()), seed)?; + match ssh::agent::Agent::connect() { + Ok(mut agent) => register(&mut agent, &profile, passphrase.clone())?, + Err(e) if e.is_not_running() => return Err(Error::AgentNotRunning), + Err(e) => Err(e)?, + } + + Ok(()) +} + +pub fn register( + agent: &mut ssh::agent::Agent, + profile: &radicle::Profile, + passphrase: ssh::Passphrase, +) -> Result<(), Error> { + let secret = profile + .keystore + .secret_key(Some(passphrase)) + .map_err(|e| { + if e.is_crypto_err() { + Error::Crypto(radicle::crypto::ssh::keystore::Error::Ssh( + ssh_key::Error::Crypto, + )) + } else { + e.into() + } + })? + .ok_or(Error::Crypto(radicle::crypto::ssh::keystore::Error::Ssh( + ssh_key::Error::Crypto, + )))?; + + agent.register(&secret)?; + + Ok(()) +} diff --git a/crates/radicle-types/src/binaries.rs b/crates/radicle-types/src/binaries.rs index a2747e961..a8fb54282 100644 --- a/crates/radicle-types/src/binaries.rs +++ b/crates/radicle-types/src/binaries.rs @@ -104,6 +104,14 @@ pub fn git_command() -> Option { Some(command) } +pub fn check_radicle_cli(profile: &radicle::Profile) -> Result<(), crate::error::Error> { + if profile.home().path().join("bin").join("rad").exists() || rad().is_some() { + return Ok(()); + } + + Err(crate::error::Error::RadicleNotInstalled) +} + /// The `rad` CLI, if it is installed. /// /// Uncached: the app polls for it so that installing the CLI while the app is diff --git a/crates/radicle-types/src/lib.rs b/crates/radicle-types/src/lib.rs index 21801a7be..93627f8da 100644 --- a/crates/radicle-types/src/lib.rs +++ b/crates/radicle-types/src/lib.rs @@ -1,12 +1,14 @@ use traits::Profile; use traits::cobs::Cobs; use traits::identity::Identity; +use traits::inbox::Inbox; use traits::issue::{Issues, IssuesMut}; use traits::job::Jobs; use traits::patch::{Patches, PatchesMut}; use traits::repo::Repo; use traits::thread::Thread; +pub mod auth; pub mod binaries; pub mod cobs; pub mod config; @@ -29,6 +31,7 @@ impl Repo for AppState {} impl Thread for AppState {} impl Cobs for AppState {} impl Identity for AppState {} +impl Inbox for AppState {} impl Issues for AppState {} impl IssuesMut for AppState {} impl Jobs for AppState {} diff --git a/crates/radicle-types/src/traits.rs b/crates/radicle-types/src/traits.rs index d74ce5a63..59bce1e62 100644 --- a/crates/radicle-types/src/traits.rs +++ b/crates/radicle-types/src/traits.rs @@ -1,9 +1,11 @@ use radicle::node::{AliasStore, NodeId}; use crate::config::Config; +use crate::error::Error; pub mod cobs; pub mod identity; +pub mod inbox; pub mod issue; pub mod job; pub mod patch; @@ -25,6 +27,22 @@ pub trait Profile { } } + fn check_cobs_cache(&self) -> Result<(), Error> { + let p = self.profile(); + let cache = radicle::cob::cache::Store::open( + p.home().cobs().join(radicle::cob::cache::COBS_DB_FILE), + )?; + cache.check_version()?; + + Ok(()) + } + + fn node_running(&self) -> bool { + let p = self.profile(); + + radicle::node::Handle::is_running(&radicle::Node::new(p.home().socket_from_env())) + } + fn alias(&self, nid: NodeId) -> Option { let p = self.profile(); let aliases = p.aliases(); diff --git a/crates/radicle-types/src/traits/inbox.rs b/crates/radicle-types/src/traits/inbox.rs new file mode 100644 index 000000000..28a5ef692 --- /dev/null +++ b/crates/radicle-types/src/traits/inbox.rs @@ -0,0 +1,213 @@ +use radicle::identity; +use radicle::issue::cache::Issues; +use radicle::node; +use radicle::patch::cache::Patches; +use radicle::storage::{ReadRepository, ReadStorage}; + +use crate::domain::inbox::models::notification::{self, RepoGroupByItem}; +use crate::domain::inbox::service::Service; +use crate::domain::inbox::traits::InboxService; +use crate::error::Error; +use crate::outbound::sqlite::Sqlite; + +use crate::traits::Profile; + +pub trait Inbox: Profile { + fn list_notifications( + &self, + sqlite_service: &Service, + params: notification::RepoGroupParams, + ) -> Result { + let profile = &self.profile(); + let aliases = profile.aliases(); + let repos_with_groups = sqlite_service.repo_group(params.clone())?; + + let mut repo_counts = std::collections::HashMap::new(); + for (repo_id, count) in (sqlite_service.counts_by_repo()?).flatten() { + repo_counts.insert(repo_id, count); + } + + let take = if params.all.unwrap_or(false) { + usize::MAX + } else { + params.take.unwrap_or(20) + }; + + let mut grouped_repos = std::collections::HashMap::new(); + for (repo_id, group) in repos_with_groups { + grouped_repos + .entry(repo_id) + .or_insert_with(Vec::new) + .extend(group); + } + + let mut result = Vec::new(); + + for (repo_id, all) in grouped_repos { + let repo = match profile.storage.repository(repo_id) { + Ok(r) => r, + Err(e) => { + log::error!("Failed to open repository {}: {}", repo_id, e); + continue; + } + }; + + let name = match repo.identity_doc() { + Ok(identity::DocAt { doc, .. }) => match doc.project() { + Ok(project) => project.name().to_string(), + Err(_) => format!("Unknown project in {}", repo_id), + }, + Err(_) => format!("Unknown project in {}", repo_id), + }; + + let patches = match profile.patches(&repo) { + Ok(p) => p, + Err(e) => { + log::error!("Failed to get patches for {}: {}", repo_id, e); + continue; + } + }; + let issues = match profile.issues(&repo) { + Ok(i) => i, + Err(e) => { + log::error!("Failed to get issues for {}: {}", repo_id, e); + continue; + } + }; + + let content = all + .into_iter() + .take(take) + .map(|(qualified, n)| { + n.into_iter() + .filter_map(|s| { + let update: notification::RefUpdate = + (qualified.clone().into_refstring(), s.new, s.old).into(); + let update: radicle::storage::RefUpdate = update.into(); + let kind = + node::notifications::NotificationKind::try_from(qualified.clone()) + .ok()?; + + match kind { + node::notifications::NotificationKind::Cob { ref typed_id } => { + if typed_id.is_patch() { + let actions = notification::actions( + typed_id.type_name.clone(), + typed_id.id, + update.old(), + update.new(), + &repo, + &aliases, + ) + .unwrap_or_default(); + + match patches.get(&typed_id.id) { + Ok(Some(p)) => { + Some(notification::NotificationItem::Patch( + notification::Patch { + row_id: s.row_id, + id: typed_id.id, + update: update.into(), + timestamp: s.timestamp, + title: p.title().to_string(), + status: (p.state().clone()).into(), + actions, + repo_id: Some(repo_id), + }, + )) + } + Ok(None) => { + log::error!("No patch found"); + None + } + Err(e) => { + log::error!("{}", e); + None + } + } + } else if typed_id.is_issue() { + let actions = notification::actions( + typed_id.type_name.clone(), + typed_id.id, + update.old(), + update.new(), + &repo, + &aliases, + ) + .unwrap_or_default(); + + match issues.get(&typed_id.id) { + Ok(Some(i)) => { + Some(notification::NotificationItem::Issue( + notification::Issue { + row_id: s.row_id, + id: typed_id.id, + update: update.into(), + timestamp: s.timestamp, + title: i.title().to_string(), + status: (*i.state()).into(), + actions, + repo_id: Some(repo_id), + }, + )) + } + Ok(None) => { + log::error!("No issue found"); + None + } + Err(e) => { + log::error!("{}", e); + None + } + } + } else { + None + } + } + _ => None, + } + }) + .collect::>() + }) + .filter(|v| !v.is_empty()) + .collect::(); + + if !content.is_empty() { + let count = repo_counts + .get(&repo_id) + .copied() + .unwrap_or_else(|| content.iter().map(|items| items.len()).sum()); + + result.push(notification::NotificationsByRepo { + rid: repo_id, + name, + notifications: content, + count, + }); + } + } + + Ok(result) + } + + fn notification_count(&self, inbox: &Service) -> Result { + inbox.notification_count().map_err(Error::from) + } + + fn clear_notifications( + &self, + params: notification::SetStatusNotifications, + ) -> Result<(), Error> { + let profile = &self.profile(); + let mut notifications = profile.notifications_mut()?; + match params { + notification::SetStatusNotifications::Ids(ids) => notifications.clear(&ids)?, + notification::SetStatusNotifications::Repo(repo) => { + notifications.clear_by_repo(&repo)? + } + notification::SetStatusNotifications::All => notifications.clear_all()?, + }; + + Ok(()) + } +} diff --git a/crates/radicle-types/src/traits/issue.rs b/crates/radicle-types/src/traits/issue.rs index 53ec57171..9a9f2c3d8 100644 --- a/crates/radicle-types/src/traits/issue.rs +++ b/crates/radicle-types/src/traits/issue.rs @@ -68,6 +68,39 @@ pub trait Issues: Profile { } pub trait IssuesMut: Profile { + fn rebuild_issue_cache( + &self, + rid: identity::RepoId, + on_event: impl Fn(cobs::CacheEvent) -> Result<(), Error>, + ) -> Result<(), Error> { + let profile = self.profile(); + let repo = profile.storage.repository(rid)?; + let signer = profile.signer()?; + let mut issues = profile.issues_mut(&repo, &signer)?; + on_event(cobs::CacheEvent::Started { rid })?; + issues.write_all(|result, progress| { + match result { + Ok((id, _)) => { + if on_event(cobs::CacheEvent::Progress { + rid, + oid: **id, + current: progress.current(), + total: progress.total(), + }) + .is_err() + { + log::error!("Failed to send progress"); + } + } + Err(err) => log::warn!("Failed to retrieve issue: {err}"), + }; + std::ops::ControlFlow::Continue(()) + })?; + on_event(cobs::CacheEvent::Finished { rid })?; + + Ok(()) + } + fn create_issue( &self, rid: identity::RepoId, diff --git a/crates/radicle-types/src/traits/patch.rs b/crates/radicle-types/src/traits/patch.rs index 1f064ad08..8d9793465 100644 --- a/crates/radicle-types/src/traits/patch.rs +++ b/crates/radicle-types/src/traits/patch.rs @@ -51,6 +51,39 @@ pub trait Patches: Profile { } pub trait PatchesMut: Profile { + fn rebuild_patch_cache( + &self, + rid: identity::RepoId, + on_event: impl Fn(cobs::CacheEvent) -> Result<(), Error>, + ) -> Result<(), Error> { + let profile = self.profile(); + let repo = profile.storage.repository(rid)?; + let signer = profile.signer()?; + let mut patches = profile.patches_mut(&repo, &signer)?; + on_event(cobs::CacheEvent::Started { rid })?; + patches.write_all(|result, progress| { + match result { + Ok((id, _)) => { + if on_event(cobs::CacheEvent::Progress { + rid, + oid: **id, + current: progress.current(), + total: progress.total(), + }) + .is_err() + { + log::error!("Failed to send progress"); + } + } + Err(err) => log::warn!("Failed to retrieve patch: {err}"), + }; + std::ops::ControlFlow::Continue(()) + })?; + on_event(cobs::CacheEvent::Finished { rid })?; + + Ok(()) + } + /// Publish a review of a revision, together with its code comments. fn create_patch_review( &self,