Extract PatchSeries module and remove require_relay_accepted helper
This commit is contained in:
@@ -28,8 +28,6 @@ impl Deletions {
|
|||||||
.map(|c| (c, event.pubkey, event.created_at)),
|
.map(|c| (c, event.pubkey, event.created_at)),
|
||||||
);
|
);
|
||||||
} else if event.kind == Kind::RequestToVanish {
|
} else if event.kind == Kind::RequestToVanish {
|
||||||
// We can't verify which relay the request targeted, so any
|
|
||||||
// vanish request is honored for the author's events.
|
|
||||||
vanished.push((event.pubkey, event.created_at));
|
vanished.push((event.pubkey, event.created_at));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ use crate::bootstrap::{
|
|||||||
};
|
};
|
||||||
use crate::git_store::Mirrors;
|
use crate::git_store::Mirrors;
|
||||||
use crate::inbox::Inbox;
|
use crate::inbox::Inbox;
|
||||||
use crate::push::{
|
use crate::push::{PushOutcome, grasp_base_url, grasp_clone_url, push_staged_to_grasps};
|
||||||
PushOutcome, grasp_base_url, grasp_clone_url, push_staged_to_grasps, require_relay_accepted,
|
|
||||||
};
|
|
||||||
use crate::repos::RepoListStore;
|
use crate::repos::RepoListStore;
|
||||||
|
|
||||||
pub const USER_KEYRING: &str = "Signed Safe Storage";
|
pub const USER_KEYRING: &str = "Signed Safe Storage";
|
||||||
@@ -464,7 +462,16 @@ impl Backend {
|
|||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let event = builder.finalize_async(&signer).await?;
|
let event = builder.finalize_async(&signer).await?;
|
||||||
let output = client.send_event(&event).broadcast().await?;
|
let output = client.send_event(&event).broadcast().await?;
|
||||||
require_relay_accepted(output, event)?;
|
|
||||||
|
if output.success.is_empty() && !output.failed.is_empty() {
|
||||||
|
let reasons = output
|
||||||
|
.failed
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
bail!("event not accepted by any relay: {reasons}");
|
||||||
|
}
|
||||||
|
|
||||||
Ok::<(), anyhow::Error>(())
|
Ok::<(), anyhow::Error>(())
|
||||||
})
|
})
|
||||||
@@ -1202,12 +1209,18 @@ async fn announce_repository_and_push(
|
|||||||
client.add_relay(url).and_connect().await.ok();
|
client.add_relay(url).and_connect().await.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
let event = {
|
let event = announcement.finalize_async(signer).await?;
|
||||||
let builder = announcement.into_event_builder();
|
let output = client.send_event(&event).broadcast().await?;
|
||||||
let event = builder.finalize_async(signer).await?;
|
|
||||||
let output = client.send_event(&event).broadcast().await?;
|
if output.success.is_empty() && !output.failed.is_empty() {
|
||||||
require_relay_accepted(output, event)?
|
let reasons = output
|
||||||
};
|
.failed
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
bail!("event not accepted by any relay: {reasons}");
|
||||||
|
}
|
||||||
|
|
||||||
// The state event is the push authorization.
|
// The state event is the push authorization.
|
||||||
let outcome = if refs.is_empty() {
|
let outcome = if refs.is_empty() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod checkouts;
|
|||||||
mod git_store;
|
mod git_store;
|
||||||
mod inbox;
|
mod inbox;
|
||||||
mod local_repos;
|
mod local_repos;
|
||||||
|
mod patch_series;
|
||||||
mod profile;
|
mod profile;
|
||||||
mod push;
|
mod push;
|
||||||
mod refresh;
|
mod refresh;
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use bitcoin_hashes::sha1::Hash as Sha1Hash;
|
||||||
|
use signed_git::PatchParser;
|
||||||
|
|
||||||
|
/// NIP-34 suggests patches when each event is under 60kb.
|
||||||
|
pub(crate) const MAX_PATCH_EVENT_BYTES: usize = 60 * 1024;
|
||||||
|
|
||||||
|
pub(crate) struct PatchSeries {
|
||||||
|
pub(crate) parts: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PatchSeries {
|
||||||
|
pub(crate) fn parse(patch: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
parts: PatchParser::split_patch_series(patch)
|
||||||
|
.into_iter()
|
||||||
|
.map(str::to_owned)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the length of the first part that exceeds the NIP-34 recommended size.
|
||||||
|
pub(crate) fn oversized_length(&self) -> Option<usize> {
|
||||||
|
self.parts
|
||||||
|
.iter()
|
||||||
|
.map(String::len)
|
||||||
|
.find(|length| *length > MAX_PATCH_EVENT_BYTES)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The last commit of the series, which is the tip of the branch.
|
||||||
|
pub(crate) fn tip_commit(&self) -> Option<Sha1Hash> {
|
||||||
|
self.parts
|
||||||
|
.last()
|
||||||
|
.and_then(|part| patch_current_commit(part))
|
||||||
|
.and_then(|hex| hex.parse::<Sha1Hash>().ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn commit_of(&self, index: usize) -> Option<&str> {
|
||||||
|
self.parts
|
||||||
|
.get(index)
|
||||||
|
.and_then(|part| patch_current_commit(part))
|
||||||
|
.filter(|hex| hex.len() == 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `From <commit>` header on the first line.
|
||||||
|
fn patch_current_commit(patch: &str) -> Option<&str> {
|
||||||
|
let line = patch.lines().next()?;
|
||||||
|
let hex = line.strip_prefix("From ")?;
|
||||||
|
hex.split_whitespace().next().filter(|hex| hex.len() == 40)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Error, bail};
|
use anyhow::Error;
|
||||||
use gpui::BackgroundExecutor;
|
use gpui::BackgroundExecutor;
|
||||||
use nostr::prelude::Url;
|
use nostr::prelude::Url;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -10,7 +10,6 @@ use signed_git::{Repo, RepoRefState};
|
|||||||
use signed_nostr::UniversalSigner;
|
use signed_nostr::UniversalSigner;
|
||||||
|
|
||||||
const GRASP_PUSH_ATTEMPTS: usize = 3;
|
const GRASP_PUSH_ATTEMPTS: usize = 3;
|
||||||
|
|
||||||
const GRASP_RETRY_DELAY: Duration = Duration::from_secs(1);
|
const GRASP_RETRY_DELAY: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
/// `ws://` grasp servers, like ngit, use `http(s)://<host>`.
|
/// `ws://` grasp servers, like ngit, use `http(s)://<host>`.
|
||||||
@@ -136,24 +135,6 @@ impl PushOutcome {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn require_relay_accepted(
|
|
||||||
output: SendEventOutput,
|
|
||||||
event: Event,
|
|
||||||
) -> Result<Event, Error> {
|
|
||||||
if output.success.is_empty() && !output.failed.is_empty() {
|
|
||||||
let reasons = output
|
|
||||||
.failed
|
|
||||||
.values()
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
bail!("event not accepted by any relay: {reasons}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signs with a `created_at` strictly newer than `last_created_at`,
|
/// Signs with a `created_at` strictly newer than `last_created_at`,
|
||||||
/// since a relay treats a same-id resend as a duplicate.
|
/// since a relay treats a same-id resend as a duplicate.
|
||||||
async fn sign_state_event(
|
async fn sign_state_event(
|
||||||
|
|||||||
+51
-163
@@ -3,7 +3,6 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{Error, bail};
|
use anyhow::{Error, bail};
|
||||||
use bitcoin_hashes::sha1::Hash as Sha1Hash;
|
|
||||||
use gpui::{App, AppContext, Context, SharedString, Subscription, Task};
|
use gpui::{App, AppContext, Context, SharedString, Subscription, Task};
|
||||||
use nostr::event::IntoEventBuilder;
|
use nostr::event::IntoEventBuilder;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -12,21 +11,17 @@ use signed_core::{
|
|||||||
Announcement, Deletions, Filters, GitEvent, PullRequest, RepoAddr, RepoState, RepoStatus,
|
Announcement, Deletions, Filters, GitEvent, PullRequest, RepoAddr, RepoState, RepoStatus,
|
||||||
filters,
|
filters,
|
||||||
};
|
};
|
||||||
use signed_git::{Nip34Binding, PatchParser, Repo};
|
use signed_git::{Nip34Binding, Repo};
|
||||||
use signed_nostr::UniversalSigner;
|
use signed_nostr::UniversalSigner;
|
||||||
|
|
||||||
use crate::backend::{Backend, BackendEvent};
|
use crate::backend::{Backend, BackendEvent};
|
||||||
use crate::bootstrap::user_grasp_list_servers;
|
use crate::bootstrap::user_grasp_list_servers;
|
||||||
use crate::checkouts::CheckoutsStore;
|
use crate::checkouts::CheckoutsStore;
|
||||||
use crate::push::{
|
use crate::patch_series::{MAX_PATCH_EVENT_BYTES, PatchSeries};
|
||||||
PushOutcome, grasp_base_url, grasp06_prs_url, pr_clone_urls, require_relay_accepted,
|
use crate::push::{PushOutcome, grasp_base_url, grasp06_prs_url, pr_clone_urls};
|
||||||
};
|
|
||||||
use crate::repos::RepoListStore;
|
use crate::repos::RepoListStore;
|
||||||
use crate::sync_status::SyncStatusStore;
|
use crate::sync_status::SyncStatusStore;
|
||||||
|
|
||||||
// NIP-34 suggests patches when each event is under 60kb.
|
|
||||||
const MAX_PATCH_EVENT_BYTES: usize = 60 * 1024;
|
|
||||||
|
|
||||||
pub struct RepoStore {
|
pub struct RepoStore {
|
||||||
addr: Option<RepoAddr>,
|
addr: Option<RepoAddr>,
|
||||||
// Seeded from the open-time hint, replaced by the database's latest on the
|
// Seeded from the open-time hint, replaced by the database's latest on the
|
||||||
@@ -869,7 +864,18 @@ impl RepoStore {
|
|||||||
|
|
||||||
let publish_result: Result<Event, Error> = async {
|
let publish_result: Result<Event, Error> = async {
|
||||||
let output = client.send_event(&event).broadcast().await?;
|
let output = client.send_event(&event).broadcast().await?;
|
||||||
require_relay_accepted(output, event)
|
|
||||||
|
if output.success.is_empty() && !output.failed.is_empty() {
|
||||||
|
let reasons = output
|
||||||
|
.failed
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
bail!("event not accepted by any relay: {reasons}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(event)
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1047,7 +1053,18 @@ impl RepoStore {
|
|||||||
let publish_result: Result<Event, Error> = async {
|
let publish_result: Result<Event, Error> = async {
|
||||||
let event = builder.finalize_async(&signer).await?;
|
let event = builder.finalize_async(&signer).await?;
|
||||||
let output = client.send_event(&event).broadcast().await?;
|
let output = client.send_event(&event).broadcast().await?;
|
||||||
require_relay_accepted(output, event)
|
|
||||||
|
if output.success.is_empty() && !output.failed.is_empty() {
|
||||||
|
let reasons = output
|
||||||
|
.failed
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
bail!("event not accepted by any relay: {reasons}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(event)
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1302,8 +1319,7 @@ impl RepoStore {
|
|||||||
Task::ready(Err(anyhow::anyhow!("{message}")))
|
Task::ready(Err(anyhow::anyhow!("{message}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every one-shot repository event (issue, comment, status) goes through
|
/// Publish an event to the repository's relays, and update the store with any error.
|
||||||
// this; multi-step flows call the SDK directly instead.
|
|
||||||
fn publish(&mut self, builder: EventBuilder, cx: &mut Context<Self>) {
|
fn publish(&mut self, builder: EventBuilder, cx: &mut Context<Self>) {
|
||||||
self.last_error = None;
|
self.last_error = None;
|
||||||
|
|
||||||
@@ -1317,7 +1333,18 @@ impl RepoStore {
|
|||||||
let publish_result: Result<Event, Error> = async {
|
let publish_result: Result<Event, Error> = async {
|
||||||
let event = builder.finalize_async(&signer).await?;
|
let event = builder.finalize_async(&signer).await?;
|
||||||
let output = client.send_event(&event).broadcast().await?;
|
let output = client.send_event(&event).broadcast().await?;
|
||||||
require_relay_accepted(output, event)
|
|
||||||
|
if output.success.is_empty() && !output.failed.is_empty() {
|
||||||
|
let reasons = output
|
||||||
|
.failed
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
bail!("event not accepted by any relay: {reasons}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(event)
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1372,53 +1399,7 @@ fn resolve_statuses(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// The `From <commit>` header on the first line.
|
/// Publish a patch series as a sequence of `git/patch` events.
|
||||||
fn patch_current_commit(patch: &str) -> Option<&str> {
|
|
||||||
let line = patch.lines().next()?;
|
|
||||||
let hex = line.strip_prefix("From ")?;
|
|
||||||
hex.split_whitespace().next().filter(|hex| hex.len() == 40)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PatchSeries {
|
|
||||||
parts: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PatchSeries {
|
|
||||||
fn parse(patch: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
parts: PatchParser::split_patch_series(patch)
|
|
||||||
.into_iter()
|
|
||||||
.map(str::to_owned)
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The byte length of the first part over the NIP-34 size suggestion.
|
|
||||||
fn oversized_length(&self) -> Option<usize> {
|
|
||||||
self.parts
|
|
||||||
.iter()
|
|
||||||
.map(String::len)
|
|
||||||
.find(|length| *length > MAX_PATCH_EVENT_BYTES)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The tip of the series is its last commit; `git format-patch` orders
|
|
||||||
// patches oldest first.
|
|
||||||
fn tip_commit(&self) -> Option<Sha1Hash> {
|
|
||||||
self.parts
|
|
||||||
.last()
|
|
||||||
.and_then(|part| patch_current_commit(part))
|
|
||||||
.and_then(|hex| hex.parse::<Sha1Hash>().ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn commit_of(&self, index: usize) -> Option<&str> {
|
|
||||||
self.parts
|
|
||||||
.get(index)
|
|
||||||
.and_then(|part| patch_current_commit(part))
|
|
||||||
.filter(|hex| hex.len() == 40)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the root event, the one a PR references.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn publish_patch_series(
|
async fn publish_patch_series(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
@@ -1478,7 +1459,16 @@ async fn publish_patch_series(
|
|||||||
let builder = EventBuilder::new(Kind::GitPatch, part.clone()).tags(tags);
|
let builder = EventBuilder::new(Kind::GitPatch, part.clone()).tags(tags);
|
||||||
let event = builder.finalize_async(signer).await?;
|
let event = builder.finalize_async(signer).await?;
|
||||||
let output = client.send_event(&event).broadcast().await?;
|
let output = client.send_event(&event).broadcast().await?;
|
||||||
let event = require_relay_accepted(output, event)?;
|
|
||||||
|
if output.success.is_empty() && !output.failed.is_empty() {
|
||||||
|
let reasons = output
|
||||||
|
.failed
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
bail!("event not accepted by any relay: {reasons}");
|
||||||
|
}
|
||||||
|
|
||||||
if root.is_none() {
|
if root.is_none() {
|
||||||
root = Some(event.clone());
|
root = Some(event.clone());
|
||||||
@@ -1513,105 +1503,3 @@ fn comment_builder(
|
|||||||
.into_event_builder()
|
.into_event_builder()
|
||||||
.tags([Tag::coordinate(addr.clone().into(), None)])
|
.tags([Tag::coordinate(addr.clone().into(), None)])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use signed_core::GitEvent;
|
|
||||||
|
|
||||||
use super::{RepoStore, comment_builder, patch_current_commit};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_format_patch_header() {
|
|
||||||
let patch = "From 1f6c0c5f3f1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a Mon Sep 17 00:00:00 2001\nFrom: A <a@b.c>\nSubject: [PATCH] fix\n\n---\n";
|
|
||||||
assert_eq!(
|
|
||||||
patch_current_commit(patch),
|
|
||||||
Some("1f6c0c5f3f1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn comment_builder_follows_nip22() {
|
|
||||||
let keys = Keys::generate();
|
|
||||||
let root = EventBuilder::new(Kind::GitIssue, "issue body")
|
|
||||||
.finalize(&keys)
|
|
||||||
.expect("signed event");
|
|
||||||
let addr = signed_core::RepoAddr::new(root.pubkey, "my-repo");
|
|
||||||
let relay = RelayUrl::parse("wss://relay.example.com").expect("valid relay URL");
|
|
||||||
|
|
||||||
let event = comment_builder(&root, None, Some(&relay), &addr, "hi".into())
|
|
||||||
.finalize(&keys)
|
|
||||||
.expect("signed event");
|
|
||||||
|
|
||||||
assert_eq!(event.kind, Kind::Comment);
|
|
||||||
|
|
||||||
let kinds: Vec<&str> = event.tags.iter().map(Tag::kind).collect();
|
|
||||||
for expected in ["E", "K", "P", "e", "k", "p", "a"] {
|
|
||||||
assert!(kinds.contains(&expected), "missing {expected} tag");
|
|
||||||
}
|
|
||||||
|
|
||||||
let e = event.tags.iter().find(|t| t.kind() == "E").expect("E tag");
|
|
||||||
let slice = e.as_slice();
|
|
||||||
assert_eq!(slice[1], root.id.to_hex());
|
|
||||||
assert_eq!(slice[2], relay.as_str());
|
|
||||||
assert_eq!(slice[3], root.pubkey.to_hex());
|
|
||||||
|
|
||||||
let e = event.tags.iter().find(|t| t.kind() == "e").expect("e tag");
|
|
||||||
assert_eq!(e.as_slice()[1], root.id.to_hex());
|
|
||||||
|
|
||||||
assert!(event.references_root(&root.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn maintainer_filters_name_owner_and_maintainers() {
|
|
||||||
let owner = Keys::generate().public_key();
|
|
||||||
let maintainer = Keys::generate().public_key();
|
|
||||||
let addr = signed_core::RepoAddr::new(owner, "my-repo");
|
|
||||||
|
|
||||||
let filters = RepoStore::maintainer_filters(&addr, &[maintainer]);
|
|
||||||
assert_eq!(filters.len(), 4);
|
|
||||||
|
|
||||||
let expected = HashSet::from([owner, maintainer]);
|
|
||||||
|
|
||||||
let named = |filter: &Filter| -> HashSet<PublicKey> {
|
|
||||||
let authors = filter.authors.iter().flatten().copied();
|
|
||||||
let p_tag = filter
|
|
||||||
.generic_tags
|
|
||||||
.get(&SingleLetterTag::LOWERCASE_P)
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.filter_map(|value| PublicKey::from_hex(value).ok());
|
|
||||||
authors.chain(p_tag).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let announcement = &filters[0];
|
|
||||||
assert_eq!(named(announcement), expected);
|
|
||||||
assert!(
|
|
||||||
announcement
|
|
||||||
.generic_tags
|
|
||||||
.contains_key(&SingleLetterTag::LOWERCASE_D)
|
|
||||||
);
|
|
||||||
|
|
||||||
for filter in &filters[1..3] {
|
|
||||||
assert_eq!(named(filter), expected);
|
|
||||||
assert!(
|
|
||||||
filter
|
|
||||||
.generic_tags
|
|
||||||
.contains_key(&SingleLetterTag::LOWERCASE_A)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let deletions = &filters[3];
|
|
||||||
assert_eq!(
|
|
||||||
deletions
|
|
||||||
.authors
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.collect::<HashSet<_>>(),
|
|
||||||
expected
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -229,7 +229,9 @@ impl SyncStatusStore {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if again {
|
if again {
|
||||||
this.update(cx, |this, cx| this.refresh(cx))?;
|
this.update(cx, |this, cx| {
|
||||||
|
this.refresh(cx);
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<_, Error>(())
|
Ok::<_, Error>(())
|
||||||
|
|||||||
Reference in New Issue
Block a user