update
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user