Show push step progress in publish dialogs
Rust / build (macos-latest, stable) (push) Canceled after 0s
Rust / build (ubuntu-latest, stable) (push) Canceled after 0s
Rust / build (windows-latest, stable) (push) Canceled after 0s
Rust / build (macos-latest, stable) (pull_request) Canceled after 0s
Rust / build (ubuntu-latest, stable) (pull_request) Canceled after 0s
Rust / build (windows-latest, stable) (pull_request) Canceled after 0s
Rust / build (macos-latest, stable) (push) Canceled after 0s
Rust / build (ubuntu-latest, stable) (push) Canceled after 0s
Rust / build (windows-latest, stable) (push) Canceled after 0s
Rust / build (macos-latest, stable) (pull_request) Canceled after 0s
Rust / build (ubuntu-latest, stable) (pull_request) Canceled after 0s
Rust / build (windows-latest, stable) (pull_request) Canceled after 0s
This commit is contained in:
@@ -528,6 +528,7 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let owner = public_key.to_bech32().unwrap();
|
let owner = public_key.to_bech32().unwrap();
|
||||||
|
let addr = RepoAddr::new(public_key, repo_id.clone());
|
||||||
let servers = grasp_servers.clone();
|
let servers = grasp_servers.clone();
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
let signer = self.signer.clone();
|
let signer = self.signer.clone();
|
||||||
@@ -542,7 +543,7 @@ impl Backend {
|
|||||||
folder.join(dir_name)
|
folder.join(dir_name)
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.spawn(async move |_this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let work = cx.background_spawn({
|
let work = cx.background_spawn({
|
||||||
let destination = destination.clone();
|
let destination = destination.clone();
|
||||||
let name = name.clone();
|
let name = name.clone();
|
||||||
@@ -589,8 +590,19 @@ impl Backend {
|
|||||||
mode: GitPushMode::Main,
|
mode: GitPushMode::Main,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let on_step = &mut |status: PushStepStatus, cx: &mut AsyncApp| {
|
||||||
|
this.update(cx, |_, cx| {
|
||||||
|
cx.emit(BackendEvent::PushProgress {
|
||||||
|
addr: addr.clone(),
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
Ok::<(), Error>(())
|
||||||
|
};
|
||||||
|
|
||||||
let event =
|
let event =
|
||||||
announce_and_push_or_retract(&client, &signer, &state, announcement, cx).await?;
|
announce_and_push_or_retract(&client, &signer, &state, announcement, cx, on_step)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let announcement = Announcement::from_event(&event)
|
let announcement = Announcement::from_event(&event)
|
||||||
.ok_or_else(|| anyhow!("failed to parse announcement"))?;
|
.ok_or_else(|| anyhow!("failed to parse announcement"))?;
|
||||||
@@ -638,12 +650,13 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let owner = public_key.to_bech32().unwrap();
|
let owner = public_key.to_bech32().unwrap();
|
||||||
|
let addr = RepoAddr::new(public_key, repo_id.clone());
|
||||||
let servers = grasp_servers.clone();
|
let servers = grasp_servers.clone();
|
||||||
|
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
let signer = self.signer.clone();
|
let signer = self.signer.clone();
|
||||||
|
|
||||||
cx.spawn(async move |_this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let (mut state, euc) = cx
|
let (mut state, euc) = cx
|
||||||
.background_spawn({
|
.background_spawn({
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
@@ -670,7 +683,19 @@ impl Backend {
|
|||||||
|
|
||||||
let euc = euc.and_then(|commit: String| Sha1Hash::from_str(&commit).ok());
|
let euc = euc.and_then(|commit: String| Sha1Hash::from_str(&commit).ok());
|
||||||
let ann = build_announcement(&repo_id, &name, &description, &owner, &servers, euc);
|
let ann = build_announcement(&repo_id, &name, &description, &owner, &servers, euc);
|
||||||
let event = announce_and_push_or_retract(&client, &signer, &state, ann, cx).await?;
|
|
||||||
|
let on_step = &mut |status: PushStepStatus, cx: &mut AsyncApp| {
|
||||||
|
this.update(cx, |_, cx| {
|
||||||
|
cx.emit(BackendEvent::PushProgress {
|
||||||
|
addr: addr.clone(),
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
Ok::<(), Error>(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let event =
|
||||||
|
announce_and_push_or_retract(&client, &signer, &state, ann, cx, on_step).await?;
|
||||||
|
|
||||||
if let Some(base) = servers.first().and_then(grasp_base_url) {
|
if let Some(base) = servers.first().and_then(grasp_base_url) {
|
||||||
let url = format!("{base}/{owner}/{repo_id}.git");
|
let url = format!("{base}/{owner}/{repo_id}.git");
|
||||||
@@ -1151,15 +1176,17 @@ fn build_announcement(
|
|||||||
|
|
||||||
/// Announces a repository and pushes its state to grasp servers,
|
/// Announces a repository and pushes its state to grasp servers,
|
||||||
/// or retracts the announcement if the push fails.
|
/// or retracts the announcement if the push fails.
|
||||||
async fn announce_and_push_or_retract<S>(
|
async fn announce_and_push_or_retract<S, P>(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
signer: &S,
|
signer: &S,
|
||||||
state: &SyncState,
|
state: &SyncState,
|
||||||
announcement: GitRepositoryAnnouncement,
|
announcement: GitRepositoryAnnouncement,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
|
on_step: &mut P,
|
||||||
) -> Result<Event, Error>
|
) -> Result<Event, Error>
|
||||||
where
|
where
|
||||||
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
|
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
|
||||||
|
P: FnMut(PushStepStatus, &mut AsyncApp) -> Result<(), Error>,
|
||||||
{
|
{
|
||||||
let state_event = state
|
let state_event = state
|
||||||
.state_event(&announcement.id)
|
.state_event(&announcement.id)
|
||||||
@@ -1168,13 +1195,7 @@ where
|
|||||||
let announcement_event = announcement.finalize_async(signer).await?;
|
let announcement_event = announcement.finalize_async(signer).await?;
|
||||||
|
|
||||||
match state
|
match state
|
||||||
.announce_and_push(
|
.announce_and_push(client, announcement_event.clone(), state_event, cx, on_step)
|
||||||
client,
|
|
||||||
announcement_event.clone(),
|
|
||||||
state_event,
|
|
||||||
cx,
|
|
||||||
&mut |_, _| Ok(()),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(event) => Ok(event),
|
Ok(event) => Ok(event),
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{AnyElement, App, SharedString, div};
|
use gpui::{AnyElement, App, Context, SharedString, Subscription, div};
|
||||||
use gpui_component::ActiveTheme;
|
use gpui_component::spinner::Spinner;
|
||||||
|
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex, v_flex};
|
||||||
|
use signed_core::RepoAddr;
|
||||||
|
use signed_state::{Backend, BackendEvent, PushStepStatus};
|
||||||
|
|
||||||
/// Progress of an async dialog action: a busy flag that disables the form and an error shown below it.
|
/// Progress of an async dialog action: a busy flag that disables the form and an error shown below it.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct DialogProgress {
|
pub struct DialogProgress {
|
||||||
pub busy: bool,
|
pub busy: bool,
|
||||||
pub error: Option<SharedString>,
|
pub error: Option<SharedString>,
|
||||||
|
// The repository whose push checklist is shown; `None` outside a publish.
|
||||||
|
push_addr: Option<RepoAddr>,
|
||||||
|
pub push_steps: Vec<PushStepStatus>,
|
||||||
|
_push_subscription: Option<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DialogProgress {
|
impl DialogProgress {
|
||||||
@@ -16,6 +23,37 @@ impl DialogProgress {
|
|||||||
self.error = None;
|
self.error = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starts a publish for the repository at `addr` and listens for its push steps.
|
||||||
|
pub fn begin_push(&mut self, addr: RepoAddr, cx: &mut Context<Self>) {
|
||||||
|
self.begin();
|
||||||
|
self.push_addr = Some(addr);
|
||||||
|
self.push_steps.clear();
|
||||||
|
|
||||||
|
if self._push_subscription.is_none() {
|
||||||
|
let backend = Backend::global(cx);
|
||||||
|
self._push_subscription = Some(cx.subscribe(&backend, |this, _backend, event, cx| {
|
||||||
|
let BackendEvent::PushProgress { addr, status } = event else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if this.push_addr.as_ref() != Some(addr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match this
|
||||||
|
.push_steps
|
||||||
|
.iter_mut()
|
||||||
|
.find(|step| step.step() == status.step())
|
||||||
|
{
|
||||||
|
Some(step) => *step = status.clone(),
|
||||||
|
None => this.push_steps.push(status.clone()),
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Marks an action as failed, enabling the form and showing `message`.
|
/// Marks an action as failed, enabling the form and showing `message`.
|
||||||
pub fn fail(&mut self, message: impl Into<SharedString>) {
|
pub fn fail(&mut self, message: impl Into<SharedString>) {
|
||||||
self.busy = false;
|
self.busy = false;
|
||||||
@@ -33,3 +71,46 @@ pub fn error_row(error: &Option<SharedString>, cx: &App) -> Option<AnyElement> {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The push checklist of a running publish, `None` before any step is reported.
|
||||||
|
pub fn push_steps_row(steps: &[PushStepStatus], cx: &App) -> Option<AnyElement> {
|
||||||
|
if steps.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = steps
|
||||||
|
.iter()
|
||||||
|
.map(|status| {
|
||||||
|
let label = SharedString::from(status.step().label());
|
||||||
|
|
||||||
|
match status {
|
||||||
|
PushStepStatus::Running(_) => h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(Spinner::new().small())
|
||||||
|
.child(label)
|
||||||
|
.into_any_element(),
|
||||||
|
PushStepStatus::Done(_) => h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().foreground)
|
||||||
|
.child(Icon::new(IconName::CircleCheck).small())
|
||||||
|
.child(label)
|
||||||
|
.into_any_element(),
|
||||||
|
PushStepStatus::Failed(_, reason) => h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().danger)
|
||||||
|
.child(Icon::new(IconName::Close).small())
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"{}: {reason}",
|
||||||
|
status.step().label()
|
||||||
|
)))
|
||||||
|
.into_any_element(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Some(v_flex().gap_1().children(rows).into_any_element())
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ use gpui_component::input::{Input, InputState, Textarea};
|
|||||||
use gpui_component::select::{Select, SelectState};
|
use gpui_component::select::{Select, SelectState};
|
||||||
use gpui_component::{ActiveTheme, Disableable, WindowExt};
|
use gpui_component::{ActiveTheme, Disableable, WindowExt};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
use signed_core::RepoAddr;
|
||||||
use signed_git::Repo;
|
use signed_git::Repo;
|
||||||
use signed_state::Backend;
|
use signed_state::Backend;
|
||||||
use signed_ui::SelectOption;
|
use signed_ui::SelectOption;
|
||||||
|
|
||||||
use super::RepoDetailView;
|
use super::RepoDetailView;
|
||||||
use crate::views::dialog_state::{DialogProgress, error_row};
|
use crate::views::dialog_state::{DialogProgress, error_row, push_steps_row};
|
||||||
use crate::views::sidebar::grasp_servers::{
|
use crate::views::sidebar::grasp_servers::{
|
||||||
GraspServersState, grasp_servers_field, load_user_grasp_servers,
|
GraspServersState, grasp_servers_field, load_user_grasp_servers,
|
||||||
};
|
};
|
||||||
@@ -120,6 +121,7 @@ pub fn open(
|
|||||||
)
|
)
|
||||||
.child(grasp_servers_field(&grasp_state, &relay_input, cx)),
|
.child(grasp_servers_field(&grasp_state, &relay_input, cx)),
|
||||||
)
|
)
|
||||||
|
.children(push_steps_row(&state.read(cx).push_steps, cx))
|
||||||
.children(error_row(&error, cx))
|
.children(error_row(&error, cx))
|
||||||
.child(
|
.child(
|
||||||
DialogFooter::new().justify_end().child(
|
DialogFooter::new().justify_end().child(
|
||||||
@@ -189,9 +191,15 @@ fn init_repository(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.update(cx, |state, _| state.begin());
|
|
||||||
|
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
|
let Some(user) = backend.read(cx).current_user() else {
|
||||||
|
state.update(cx, |state, _| state.fail("Sign in to publish a repository"));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let addr = RepoAddr::new(user, RepoAddr::identifier_from_name(&name));
|
||||||
|
state.update(cx, |state, cx| state.begin_push(addr, cx));
|
||||||
|
|
||||||
let task = backend.update(cx, |backend, cx| {
|
let task = backend.update(cx, |backend, cx| {
|
||||||
backend.publish_local_repo(
|
backend.publish_local_repo(
|
||||||
local_path.clone(),
|
local_path.clone(),
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ use gpui_component::form::{field, v_form};
|
|||||||
use gpui_component::input::{Input, InputState, Textarea};
|
use gpui_component::input::{Input, InputState, Textarea};
|
||||||
use gpui_component::{Disableable, IconName, WindowExt, h_flex};
|
use gpui_component::{Disableable, IconName, WindowExt, h_flex};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
use signed_core::RepoAddr;
|
||||||
use signed_state::{Backend, CheckoutsStore};
|
use signed_state::{Backend, CheckoutsStore};
|
||||||
|
|
||||||
use super::super::open_repo_panel;
|
use super::super::open_repo_panel;
|
||||||
use super::grasp_servers::{GraspServersState, grasp_servers_field, load_user_grasp_servers};
|
use super::grasp_servers::{GraspServersState, grasp_servers_field, load_user_grasp_servers};
|
||||||
use crate::views::dialog_state::{DialogProgress, error_row};
|
use crate::views::dialog_state::{DialogProgress, error_row, push_steps_row};
|
||||||
|
|
||||||
/// Progress of the create-repository flow, so async results can be rendered.
|
/// Progress of the create-repository flow, so async results can be rendered.
|
||||||
pub type CreateRepoState = DialogProgress;
|
pub type CreateRepoState = DialogProgress;
|
||||||
@@ -112,6 +113,7 @@ pub fn open(dock_area: WeakEntity<DockArea>, window: &mut Window, cx: &mut App)
|
|||||||
)
|
)
|
||||||
.child(grasp_servers_field(&grasp_state, &relay_input, cx)),
|
.child(grasp_servers_field(&grasp_state, &relay_input, cx)),
|
||||||
)
|
)
|
||||||
|
.children(push_steps_row(&state.read(cx).push_steps, cx))
|
||||||
.children(error_row(&error, cx))
|
.children(error_row(&error, cx))
|
||||||
.child(
|
.child(
|
||||||
DialogFooter::new().justify_end().child(
|
DialogFooter::new().justify_end().child(
|
||||||
@@ -209,9 +211,15 @@ fn create_repository(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.update(cx, |state, _| state.begin());
|
|
||||||
|
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
|
let Some(user) = backend.read(cx).current_user() else {
|
||||||
|
state.update(cx, |state, _| state.fail("Sign in to create a repository"));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let addr = RepoAddr::new(user, RepoAddr::identifier_from_name(&name));
|
||||||
|
state.update(cx, |state, cx| state.begin_push(addr, cx));
|
||||||
|
|
||||||
let task = backend.update(cx, |backend, cx| {
|
let task = backend.update(cx, |backend, cx| {
|
||||||
backend.create_repository(&name, &description, folder, servers, cx)
|
backend.create_repository(&name, &description, folder, servers, cx)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
|
|
||||||
## Push
|
## Push
|
||||||
|
|
||||||
- [ ] Refactor the push pipeline onto `SyncState` with step-by-step progress, see [push-refactor.md](./push-refactor.md).
|
|
||||||
- [ ] Add a fast-forward check and a force option (nak refuses non-FF without `--force`), today it relies entirely on grasp CAS.
|
- [ ] Add a fast-forward check and a force option (nak refuses non-FF without `--force`), today it relies entirely on grasp CAS.
|
||||||
- [ ] Allow NIP-34 maintainers to push, not just the owner.
|
- [ ] Allow NIP-34 maintainers to push, not just the owner.
|
||||||
|
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
# Push refactor
|
|
||||||
|
|
||||||
Move the push pipeline onto `SyncState` with step-by-step progress, so the UI can render a live checklist instead of a single spinner.
|
|
||||||
|
|
||||||
## Problems
|
|
||||||
|
|
||||||
- `push_staged_to_grasps` is a free function with 9 arguments; `refs`/`head` are just threaded through. Both call sites wrap it in another `background_spawn` with ~8 lines of cloning (`backend.rs` `push_repo_from`, `announce_repository_and_push`).
|
|
||||||
- The state event is re-signed per relay inside the loop.
|
|
||||||
- There is no progress surface: the whole flow is one opaque `Task<Result<PushOutcome>>`. The UI only has the `pushing` flag and the post-hoc `partial_warning`.
|
|
||||||
- `RepoState` lives in `signed_core`, but its only consumers are in `signed_state`. `signed_git::RepoRefState` is structurally identical (`refs` + `head`).
|
|
||||||
|
|
||||||
## Constraints
|
|
||||||
|
|
||||||
- Keep it simple; no edge-case handling, no over-engineering.
|
|
||||||
- No new struct wrapping the Nostr client or signer; reference them.
|
|
||||||
- No new global entity.
|
|
||||||
|
|
||||||
## Target shape
|
|
||||||
|
|
||||||
- `signed_state/src/state.rs` (moved from core, `RepoState` renamed to `SyncState`): the data (`refs`, `head`, push `path`, `mode`), builders (`from_repo`, `with_mode`), event `build`/`parse`, and the whole push flow as methods on `impl SyncState`.
|
|
||||||
- `signed_state/src/push.rs`: keeps `GraspServer`, `PushOutcome` and the grasp URL helpers. No flow logic.
|
|
||||||
- `Backend`: thin wiring, emits `BackendEvent::PushProgress` per step.
|
|
||||||
- `RepoStore`: owns the displayed step list (it already subscribes to `Backend`).
|
|
||||||
- `RepoDetailView`: renders the checklist.
|
|
||||||
|
|
||||||
## Step 1 — Move and rename
|
|
||||||
|
|
||||||
Mechanical, no behavior change.
|
|
||||||
|
|
||||||
- Move `crates/signed_core/src/state.rs` to `crates/signed_state/src/state.rs`, rename `RepoState` to `SyncState`. Tests move along.
|
|
||||||
- `signed_core/src/lib.rs`: drop `pub mod state;` and the re-export.
|
|
||||||
- `signed_state/src/lib.rs`: `mod state;` and `pub use state::SyncState;`.
|
|
||||||
- Update importers in `signed_state`: `push.rs`, `repo.rs`, `sync_status.rs`. Nothing else uses `signed_core::RepoState`. `Kind::RepoState` (the nostr kind) is untouched.
|
|
||||||
|
|
||||||
## Step 2 — Push flow under `impl SyncState`
|
|
||||||
|
|
||||||
Replace `push_staged_to_grasps` with methods on the type that owns the refs:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// How much to push to the grasp servers.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
||||||
pub enum GitPushMode {
|
|
||||||
/// Mirror all branches and tags.
|
|
||||||
#[default]
|
|
||||||
All,
|
|
||||||
/// Push `refs/heads/main` only (repo creation).
|
|
||||||
Main,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A git ref with the commit it points at.
|
|
||||||
pub struct GitRef {
|
|
||||||
/// Full ref name, like `refs/heads/main` or `refs/tags/v1.0`.
|
|
||||||
pub name: String,
|
|
||||||
/// Commit id, `bitcoin_hashes::sha1::Hash`, the type NIP-34 tags carry.
|
|
||||||
pub commit: Sha1Hash,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SyncState {
|
|
||||||
pub refs: Vec<GitRef>,
|
|
||||||
pub head: Option<String>,
|
|
||||||
/// Local repository to push from; `None` for states parsed from events.
|
|
||||||
pub path: Option<PathBuf>,
|
|
||||||
pub mode: GitPushMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum PushStep {
|
|
||||||
AnnounceRepository,
|
|
||||||
StageStateEvent(RelayUrl), // publish state event to one grasp server
|
|
||||||
PushObjects(RelayUrl), // git push to one grasp server
|
|
||||||
BroadcastStateEvent, // fan the state event out to relays
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum PushStepStatus {
|
|
||||||
Running(PushStep),
|
|
||||||
Done(PushStep),
|
|
||||||
Failed(PushStep, String),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl SyncState {
|
|
||||||
/// Reads the refs and head of a local repository, ready to push.
|
|
||||||
pub fn from_repo(path: &Path) -> Result<Self> {
|
|
||||||
let repo = Repo::open(path)?;
|
|
||||||
let state = repo.ref_state()?;
|
|
||||||
Ok(Self {
|
|
||||||
refs: state.refs,
|
|
||||||
head: state.head,
|
|
||||||
path: Some(path.to_path_buf()),
|
|
||||||
mode: GitPushMode::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_mode(mut self, mode: GitPushMode) -> Self {
|
|
||||||
self.mode = mode;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// State event for these refs; the caller signs it.
|
|
||||||
pub fn state_event(&self, repo_id: &str) -> EventBuilder {
|
|
||||||
Self::build(repo_id, &self.refs, self.head.as_deref())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per server: connect + send the pre-signed state event, then git push
|
|
||||||
// via cx.background_spawn (blocking gix work). Reports every transition
|
|
||||||
// through on_step. Ends with the relay fan-out broadcast. Errors once
|
|
||||||
// up front when self.path is None (state not built from a local repo).
|
|
||||||
pub async fn push_to_grasps<P>(
|
|
||||||
&self,
|
|
||||||
cx: &mut AsyncApp,
|
|
||||||
client: &Client,
|
|
||||||
event: Event,
|
|
||||||
announcement: &Announcement,
|
|
||||||
on_step: &mut P,
|
|
||||||
) -> Result<PushOutcome, Error>
|
|
||||||
where
|
|
||||||
P: FnMut(PushStepStatus, &mut AsyncApp) -> Result<(), Error>,
|
|
||||||
{ ... }
|
|
||||||
|
|
||||||
// Announces the pre-signed announcement (AnnounceRepository step), then
|
|
||||||
// delegates to push_to_grasps with the pre-signed state event. No signer:
|
|
||||||
// on failure the call site retracts the announcement by id.
|
|
||||||
pub async fn announce_and_push<P>(...) -> Result<Event, Error> { ... }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why these arguments
|
|
||||||
|
|
||||||
The struct carries the job description — what (`refs`, `head`), from where (`path`), how much (`mode`) — built with `SyncState::from_repo(path)?.with_mode(..)`. The method arguments are then only what varies per call or cannot live on the data: `cx`, `client`, the pre-signed `event`, `announcement`, `on_step`. Down from the 9-arg free function:
|
|
||||||
|
|
||||||
- `refs`, `head`, `path`, `mode` come from `&self`. The `push: fn(&Path, &str, &str, &str) -> Result<(), Error>` callback becomes the `mode` field, resolved inside to `Repo::push_main` / `Repo::push_all`; the wildcard-refspec mirror semantics of `push_all` are preserved. States built by `SyncState::parse` (remote events) have no local repo, so `path` is `Option<PathBuf>` and `push_to_grasps` errors once up front when it is `None`.
|
|
||||||
- `repo_id`, `owner`, `servers` collapse into `&Announcement` — its `id`, `owner` and `relays` are exactly those values, and both call sites already hold one. `push_repo_from` currently destructures the announcement into those three fields just to re-pass them. It stays an argument because states parsed from events carry no announcement.
|
|
||||||
- The state event is signed by the caller via `state_event(..).finalize_async(&signer)`, so `push_to_grasps` takes a signed `Event` and needs no signer or `S` generic — it only ever sends events. The signer lives entirely with the callers: `announce_and_push` takes the pre-signed announcement and state events, so a failing call site can retract the announcement by id.
|
|
||||||
- `client` stays as a reference per the constraints; `SyncState` remains plain data, so `SyncState::parse` keeps working for remote states that have no client or path.
|
|
||||||
- `on_step` takes `&mut AsyncApp` so it can update an entity directly. That is what lets the whole flow run in the caller's foreground `cx.spawn` task and emit progress without a channel.
|
|
||||||
|
|
||||||
### Progress mechanics
|
|
||||||
|
|
||||||
- Per-relay git push is backgrounded inside the method (`cx.background_spawn`), so the method itself runs in the caller's foreground task. Foreground nostr awaits are already precedent (`connect_grasp_relays`, `set_signer`).
|
|
||||||
- `on_step` returns `Result` so a dead entity propagates instead of being swallowed.
|
|
||||||
- The caller signs the state event up front instead of per relay; a signing failure aborts before any network work.
|
|
||||||
- The final fan-out broadcast moves into `push_to_grasps`; both existing flows did exactly push, bail if zero accepted, then broadcast.
|
|
||||||
|
|
||||||
## Step 3 — Backend wiring
|
|
||||||
|
|
||||||
- Add `BackendEvent::PushProgress { addr: RepoAddr, status: PushStepStatus }`.
|
|
||||||
- `push_repo_from`: background-spawn `SyncState::from_repo(&path)`, sign `state.state_event(&announcement.id)` with the signer, then call `push_to_grasps(cx, &client, event, &announcement, on_step)` in the same foreground task, `on_step` emitting the event via `this.update`. The nested `background_spawn` and its clone block disappear. Keep the `refs.is_empty()` early return at the call site.
|
|
||||||
- `announce_repository_and_push` is deleted from `backend.rs`; its callers build a `SyncState` first (`SyncState::from_repo` for publish; repo creation builds the single-ref state literally with `GitPushMode::Main`), sign the announcement and state events, then call `announce_and_push(client, announcement, state_event, cx, on_step)`. When it fails, the call site retracts via `SyncState::retract_announcement` — retraction is the very end of the flow, not inside the method.
|
|
||||||
- `push.rs` keeps `GraspServer`, `PushOutcome` and the URL helpers.
|
|
||||||
|
|
||||||
## Step 4 — UI checklist
|
|
||||||
|
|
||||||
- `RepoStore`: add `push_steps: Vec<PushStepStatus>`; clear it in `run_push` start/end; handle `BackendEvent::PushProgress` in the existing `subscribe_backend` (match on `addr`, replace by `PushStep` equality, `cx.notify()` — no refresh).
|
|
||||||
- `RepoDetailView`: while `pushing`, render the checklist where the spinner is today (header menu / push banner):
|
|
||||||
|
|
||||||
```
|
|
||||||
Announce repository ✓
|
|
||||||
Publish state event → wss://relay.damus.io ✓
|
|
||||||
Push objects → git.damus.io ⟳
|
|
||||||
Push objects → git.ngit.org ·
|
|
||||||
```
|
|
||||||
|
|
||||||
- Labels come from `PushStep::label()` so user-facing copy lives in one place.
|
|
||||||
- The publish dialog (`init_dialog.rs`) can opt in the same way by constructing the `RepoAddr` up front; optional follow-up.
|
|
||||||
|
|
||||||
Each step compiles green independently: Step 2 adds the methods while the old function still exists, Step 3 switches call sites and deletes it.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
- `cargo check -p signed_state` and `cargo test -p signed_state` after Steps 1 and 2 (the state round-trip tests move with the file).
|
|
||||||
- Manual: push a repo with two grasp servers where one is unreachable, and confirm the checklist shows running/✓/✗ per server and the existing `partial_warning` still appears.
|
|
||||||
Reference in New Issue
Block a user