This commit is contained in:
2026-09-06 10:35:16 +07:00
parent 9b6b615da2
commit 7b6b4b09a4
2 changed files with 378 additions and 180 deletions
+208 -1
View File
@@ -5,7 +5,7 @@ use std::time::Duration;
use anyhow::Error;
use bitcoin_hashes::sha1::Hash as Sha1Hash;
use gpui::{AppContext, AsyncApp, Context, SharedString, Subscription, Task, WeakEntity};
use gpui::{App, AppContext, AsyncApp, Context, SharedString, Subscription, Task, WeakEntity};
use nostr::event::IntoEventBuilder;
use nostr_sdk::prelude::*;
use signed_core::{
@@ -16,8 +16,10 @@ use signed_core::{
use crate::backend::{
Backend, BackendEvent, grasp_base_url, grasp06_prs_url, pr_clone_urls, user_grasp_list_servers,
};
use crate::checkouts::CheckoutsStore;
use crate::git_store::GitStore;
use crate::refresh::{RefreshGate, RefreshRequest};
use crate::repo_list::RepoListStore;
/// Delay between a refresh request and the actual re-query.
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300);
@@ -57,6 +59,14 @@ pub struct RepoStore {
///
/// Example, a PR published without its commit reaching a grasp server.
pub last_warning: Option<String>,
/// A republish or a checkout push is in flight.
///
/// Views show a spinner and disable their push triggers while it is set.
pub pushing: bool,
/// A clone-into-a-folder operation is in flight.
///
/// Views show a spinner and disable the clone trigger while it is set.
pub cloning: bool,
/// Relays already asked to connect to, from this repository's NIP-34 `relays` tag.
///
/// Avoids re-subscribing and re-fetching on every refresh.
@@ -127,6 +137,8 @@ impl RepoStore {
version: 0,
last_error: None,
last_warning: None,
pushing: false,
cloning: false,
repo_relays: HashSet::new(),
root_fetches: HashSet::new(),
refresh: RefreshGate::default(),
@@ -1048,6 +1060,201 @@ impl RepoStore {
}));
}
/// The latest announcement of this repository,
/// for operations that need its clone URLs and relays.
fn action_announcement(&self, cx: &App) -> Option<Announcement> {
self.announcement.clone().or_else(|| {
RepoListStore::global(cx)
.read(cx)
.announcements
.iter()
.find(|announcement| announcement.addr() == self.addr)
.cloned()
})
}
/// Re-push the repository's refs to its announced grasp servers, republish.
pub fn push_repository(&mut self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
if self.pushing {
return Task::ready(Err(anyhow::anyhow!(
"A push to this repository is already in progress"
)));
}
let Some(announcement) = self.action_announcement(cx) else {
return self.action_error("Repository announcement is not loaded yet", cx);
};
self.pushing = true;
self.last_error = None;
cx.notify();
let backend = Backend::global(cx);
let push = backend.update(cx, |backend, cx| backend.push_repository(announcement, cx));
cx.spawn(async move |this, cx| {
let result = push.await;
this.update(cx, |this, cx| {
this.pushing = false;
if let Err(e) = &result {
this.last_error = Some(format!("Push failed: {e}"));
}
cx.notify();
})?;
result
})
}
/// Push the unpushed commits of the local checkout at `path`.
pub fn push_checkout(
&mut self,
path: PathBuf,
cx: &mut Context<Self>,
) -> Task<Result<(), Error>> {
if self.pushing {
return Task::ready(Err(anyhow::anyhow!(
"A push to this repository is already in progress"
)));
}
let Some(announcement) = self.action_announcement(cx) else {
return self.action_error("Repository announcement is not loaded yet", cx);
};
// The state event's `HEAD` stays the announced default branch.
// The checkout may be on a side branch.
let head = self.head.clone();
let addr = self.addr.clone();
self.pushing = true;
self.last_error = None;
cx.notify();
let backend = Backend::global(cx);
let push = backend.update(cx, |backend, cx| {
backend.push_checkout(announcement, path.clone(), head, cx)
});
cx.spawn(async move |this, cx| {
let result = push.await;
this.update(cx, |this, cx| {
this.pushing = false;
match &result {
Ok(()) => {
// The remote moved, so recompute the ready-to-push statuses.
let checkouts = CheckoutsStore::global(cx);
checkouts.update(cx, |store, cx| {
store.request_push_statuses(&addr, cx);
cx.notify();
});
}
Err(e) => {
this.last_error = Some(format!("Push failed: {e}"));
}
}
cx.notify();
})?;
result
})
}
/// Delete the repository from nostr, announcement, state and activity.
///
/// Only the repository owner may delete it. The lists update when the
/// deletion events arrive.
pub fn delete_repository(&mut self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
let addr = self.addr.clone();
self.last_error = None;
let backend = Backend::global(cx);
let delete = backend.update(cx, |backend, cx| backend.delete_repository(addr, cx));
cx.spawn(async move |this, cx| {
let result = delete.await;
this.update(cx, |this, cx| {
if let Err(e) = &result {
this.last_error = Some(format!("Delete failed: {e}"));
}
cx.notify();
})?;
result
})
}
/// Clone the repository into `destination`, a user-chosen folder outside
/// the cache, and remember the clone as a checkout of this repository.
pub fn clone_to_folder(
&mut self,
destination: PathBuf,
cx: &mut Context<Self>,
) -> Task<Result<(), Error>> {
if self.cloning {
return Task::ready(Err(anyhow::anyhow!(
"A clone of this repository is already in progress"
)));
}
let Some(announcement) = self.action_announcement(cx) else {
return self.action_error("Repository announcement is not loaded yet", cx);
};
let clone_urls: Vec<String> = announcement.clone.iter().map(ToString::to_string).collect();
let addr = self.addr.clone();
self.cloning = true;
self.last_error = None;
cx.notify();
let clone = {
let destination = destination.clone();
cx.background_spawn(async move { signed_git::clone_repo(&clone_urls, &destination) })
};
cx.spawn(async move |this, cx| {
let result = clone.await;
this.update(cx, |this, cx| {
this.cloning = false;
match &result {
Ok(()) => {
// Remember the clone as a checkout of this repository.
let checkouts = CheckoutsStore::global(cx);
checkouts.update(cx, |store, cx| {
store.record(destination.clone(), addr.clone(), cx);
});
}
Err(e) => {
this.last_error = Some(format!("Failed to clone: {e}"));
}
}
cx.notify();
})?;
result
})
}
/// Fail an operation whose announcement is not loaded yet.
fn action_error(
&mut self,
message: impl Into<String>,
cx: &mut Context<Self>,
) -> Task<Result<(), Error>> {
let message = message.into();
self.last_error = Some(message.clone());
cx.notify();
Task::ready(Err(anyhow::anyhow!("{message}")))
}
/// Publish a kind-1631 Applied status event for `root` after a merge.
fn publish_applied_status(
&mut self,