Move push flow into SyncState methods with step callbacks
This commit is contained in:
@@ -30,7 +30,7 @@ pub use repo::RepoStore;
|
|||||||
pub use repos::RepoListStore;
|
pub use repos::RepoListStore;
|
||||||
pub use signed_git::{Nip34Binding, Nip34Kind};
|
pub use signed_git::{Nip34Binding, Nip34Kind};
|
||||||
use signed_nostr::NostrBackend;
|
use signed_nostr::NostrBackend;
|
||||||
pub use state::{GitRef, SyncState};
|
pub use state::{GitPushMode, GitRef, PushStep, PushStepStatus, SyncState};
|
||||||
pub use sync_status::{CheckoutSyncStatus, SyncStatusStore};
|
pub use sync_status::{CheckoutSyncStatus, SyncStatusStore};
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
|
|||||||
@@ -51,16 +51,16 @@ pub struct GraspServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GraspServer {
|
impl GraspServer {
|
||||||
fn ok(relay: RelayUrl) -> Self {
|
pub(crate) fn ok(relay: RelayUrl) -> Self {
|
||||||
Self {
|
Self {
|
||||||
relay,
|
relay,
|
||||||
reason: None,
|
reason: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn failed(relay: RelayUrl, reason: impl Into<String>) -> Self {
|
pub(crate) fn failed(relay: &RelayUrl, reason: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
relay,
|
relay: relay.to_owned(),
|
||||||
reason: Some(reason.into()),
|
reason: Some(reason.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ where
|
|||||||
let Some(base) = grasp_base_url(relay) else {
|
let Some(base) = grasp_base_url(relay) else {
|
||||||
outcome
|
outcome
|
||||||
.servers
|
.servers
|
||||||
.push(GraspServer::failed(relay.clone(), "no domain"));
|
.push(GraspServer::failed(relay, "no domain"));
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -161,9 +161,7 @@ where
|
|||||||
Ok(event) => event,
|
Ok(event) => event,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let reason = format!("could not sign the state event: {e}");
|
let reason = format!("could not sign the state event: {e}");
|
||||||
outcome
|
outcome.servers.push(GraspServer::failed(relay, reason));
|
||||||
.servers
|
|
||||||
.push(GraspServer::failed(relay.clone(), reason));
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -183,10 +181,7 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Err(reason) = staging {
|
if let Err(reason) = staging {
|
||||||
log::warn!("grasp push failed: {relay}: {reason}");
|
outcome.servers.push(GraspServer::failed(relay, reason));
|
||||||
outcome
|
|
||||||
.servers
|
|
||||||
.push(GraspServer::failed(relay.clone(), reason));
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,10 +191,9 @@ where
|
|||||||
outcome.servers.push(GraspServer::ok(relay.clone()));
|
outcome.servers.push(GraspServer::ok(relay.clone()));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("grasp push failed: {relay}: {e}");
|
|
||||||
outcome
|
outcome
|
||||||
.servers
|
.servers
|
||||||
.push(GraspServer::failed(relay.clone(), e.to_string()));
|
.push(GraspServer::failed(relay, e.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow, bail};
|
||||||
use bitcoin_hashes::sha1::Hash as Sha1Hash;
|
use bitcoin_hashes::sha1::Hash as Sha1Hash;
|
||||||
|
use gpui::{AppContext, AsyncApp};
|
||||||
use nostr::prelude::*;
|
use nostr::prelude::*;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use signed_core::Announcement;
|
||||||
|
use signed_git::Repo;
|
||||||
|
|
||||||
|
use crate::push::{GraspServer, PushOutcome, grasp_base_url};
|
||||||
|
|
||||||
/// A git ref with the commit it points at.
|
/// A git ref with the commit it points at.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -23,10 +30,41 @@ impl GitRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct SyncState {
|
pub struct SyncState {
|
||||||
pub refs: Vec<GitRef>,
|
pub refs: Vec<GitRef>,
|
||||||
pub head: Option<String>,
|
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,
|
||||||
|
/// Publish the state event to one grasp server.
|
||||||
|
StageStateEvent(RelayUrl),
|
||||||
|
/// Git push to one grasp server.
|
||||||
|
PushObjects(RelayUrl),
|
||||||
|
/// Fan the state event out to the relays.
|
||||||
|
BroadcastStateEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PushStepStatus {
|
||||||
|
Running(PushStep),
|
||||||
|
Done(PushStep),
|
||||||
|
Failed(PushStep, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SyncState {
|
impl SyncState {
|
||||||
@@ -70,7 +108,243 @@ impl SyncState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Self { refs, head }
|
Self {
|
||||||
|
refs,
|
||||||
|
head,
|
||||||
|
path: None,
|
||||||
|
mode: GitPushMode::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the refs and head of a local repository, ready to push.
|
||||||
|
pub fn from_repo(path: &Path) -> Result<Self, Error> {
|
||||||
|
let repo = Repo::open(path)?;
|
||||||
|
let state = repo.ref_state()?;
|
||||||
|
|
||||||
|
let refs = state
|
||||||
|
.refs
|
||||||
|
.iter()
|
||||||
|
.map(|(name, commit)| GitRef::new(name, commit))
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push the state event and objects to the grasp servers,
|
||||||
|
/// calling `on_step` for each step.
|
||||||
|
pub async fn push_to_grasps<P>(
|
||||||
|
&self,
|
||||||
|
client: &Client,
|
||||||
|
event: Event,
|
||||||
|
announcement: &Announcement,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
on_step: &mut P,
|
||||||
|
) -> Result<PushOutcome, Error>
|
||||||
|
where
|
||||||
|
P: FnMut(PushStepStatus, &mut AsyncApp) -> Result<(), Error>,
|
||||||
|
{
|
||||||
|
let path = self
|
||||||
|
.path
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow!("push requires a local repository"))?;
|
||||||
|
|
||||||
|
let owner = announcement
|
||||||
|
.owner
|
||||||
|
.to_bech32()
|
||||||
|
.map_err(|error| anyhow!("could not encode the repository owner: {error}"))?;
|
||||||
|
|
||||||
|
let mut outcome = PushOutcome::default();
|
||||||
|
|
||||||
|
for relay in &announcement.relays {
|
||||||
|
let step = PushStep::StageStateEvent(relay.clone());
|
||||||
|
|
||||||
|
// The relay must accept the state event before the git push is allowed.
|
||||||
|
on_step(PushStepStatus::Running(step.clone()), cx)?;
|
||||||
|
|
||||||
|
// Get base URL for the git push
|
||||||
|
let base = grasp_base_url(relay).unwrap_or_default();
|
||||||
|
|
||||||
|
// Ensure the relay is connected before sending the event.
|
||||||
|
client.add_relay(relay).and_connect().await?;
|
||||||
|
|
||||||
|
let staging = client.send_event(&event).to([relay.clone()]).await;
|
||||||
|
// Whether the relay accepted the event
|
||||||
|
let sent_failed = !staging
|
||||||
|
.as_ref()
|
||||||
|
.is_ok_and(|output| output.success.contains_key(relay));
|
||||||
|
|
||||||
|
if sent_failed {
|
||||||
|
let reason = match &staging {
|
||||||
|
Ok(output) => output
|
||||||
|
.failed
|
||||||
|
.get(relay)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "relay did not confirm the event".to_owned()),
|
||||||
|
Err(e) => format!("could not send the state event to {relay}: {e}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run the callback to report the failure
|
||||||
|
on_step(PushStepStatus::Failed(step, reason.clone()), cx)?;
|
||||||
|
|
||||||
|
// Record the failure in the outcome and continue to the next relay
|
||||||
|
outcome.servers.push(GraspServer::failed(relay, reason));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the callback to report the success
|
||||||
|
on_step(PushStepStatus::Done(step), cx)?;
|
||||||
|
|
||||||
|
// Run the git push in the background, reporting progress through the callback
|
||||||
|
let step = PushStep::PushObjects(relay.clone());
|
||||||
|
on_step(PushStepStatus::Running(step.clone()), cx)?;
|
||||||
|
|
||||||
|
let pushed = cx.background_spawn({
|
||||||
|
let path = path.to_path_buf();
|
||||||
|
let base = base.clone();
|
||||||
|
let owner = owner.clone();
|
||||||
|
let repo_id = announcement.id.clone();
|
||||||
|
let mode = self.mode;
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let repo = Repo::open(&path)?;
|
||||||
|
match mode {
|
||||||
|
GitPushMode::Main => repo.push_main(&base, &owner, &repo_id),
|
||||||
|
GitPushMode::All => repo.push_all(&base, &owner, &repo_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match pushed.await {
|
||||||
|
Ok(()) => {
|
||||||
|
// Run the callback to report the success
|
||||||
|
on_step(PushStepStatus::Done(step), cx)?;
|
||||||
|
|
||||||
|
outcome.state_event = Some(event.clone());
|
||||||
|
outcome.servers.push(GraspServer::ok(relay.clone()));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Run the callback to report the failure
|
||||||
|
on_step(PushStepStatus::Failed(step, e.to_string()), cx)?;
|
||||||
|
|
||||||
|
outcome
|
||||||
|
.servers
|
||||||
|
.push(GraspServer::failed(relay, e.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fan the state out to the relays once a git server holds the objects.
|
||||||
|
if outcome.state_event.is_some() {
|
||||||
|
let step = PushStep::BroadcastStateEvent;
|
||||||
|
on_step(PushStepStatus::Running(step.clone()), cx)?;
|
||||||
|
|
||||||
|
if let Err(e) = client.send_event(&event).broadcast().await {
|
||||||
|
on_step(PushStepStatus::Failed(step, e.to_string()), cx)?;
|
||||||
|
log::warn!("failed to broadcast repository state: {e}");
|
||||||
|
} else {
|
||||||
|
on_step(PushStepStatus::Done(step), cx)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Announce the repository and push the state and objects to the grasp servers.
|
||||||
|
///
|
||||||
|
/// Both events are signed by the caller, so a failure at the call site can retract the announcement by id.
|
||||||
|
pub async fn announce_and_push<P>(
|
||||||
|
&self,
|
||||||
|
client: &Client,
|
||||||
|
announcement: Event,
|
||||||
|
state_event: Event,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
on_step: &mut P,
|
||||||
|
) -> Result<Event, Error>
|
||||||
|
where
|
||||||
|
P: FnMut(PushStepStatus, &mut AsyncApp) -> Result<(), Error>,
|
||||||
|
{
|
||||||
|
let step = PushStep::AnnounceRepository;
|
||||||
|
on_step(PushStepStatus::Running(step.clone()), cx)?;
|
||||||
|
|
||||||
|
let published = Announcement::from_event(&announcement)
|
||||||
|
.ok_or_else(|| anyhow!("failed to parse announcement"))?;
|
||||||
|
|
||||||
|
// Ensure the relays are connected before sending the announcement event.
|
||||||
|
for url in &published.relays {
|
||||||
|
client.add_relay(url).and_connect().await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = client.send_event(&announcement).broadcast().await?;
|
||||||
|
|
||||||
|
// Handle the case where no relay accepted the announcement event.
|
||||||
|
if output.success.is_empty() {
|
||||||
|
let reasons = output
|
||||||
|
.failed
|
||||||
|
.values()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
// Run the callback to report the failure
|
||||||
|
let step = PushStepStatus::Failed(step, format!("event not accepted: {reasons}"));
|
||||||
|
on_step(step, cx)?;
|
||||||
|
|
||||||
|
bail!("event not accepted: {reasons}");
|
||||||
|
}
|
||||||
|
|
||||||
|
on_step(PushStepStatus::Done(step), cx)?;
|
||||||
|
|
||||||
|
let outcome = if self.refs.is_empty() {
|
||||||
|
PushOutcome::default()
|
||||||
|
} else {
|
||||||
|
self.push_to_grasps(client, state_event, &published, cx, on_step)
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
if outcome.accepted() == 0 {
|
||||||
|
bail!(
|
||||||
|
"Repository was announced, but the push to grasp servers failed: {}",
|
||||||
|
outcome.failure_summary()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(announcement)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retracts a live announcement; call when `announce_and_push` fails.
|
||||||
|
pub async fn retract_announcement<S>(
|
||||||
|
client: &Client,
|
||||||
|
signer: &S,
|
||||||
|
id: EventId,
|
||||||
|
) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
|
||||||
|
{
|
||||||
|
let builder = EventDeletionRequest::new().id(id);
|
||||||
|
let deletion = builder.finalize_async(signer).await?;
|
||||||
|
let output = client.send_event(&deletion).broadcast().await?;
|
||||||
|
|
||||||
|
// Without relay acceptance the deletion request stays live.
|
||||||
|
if output.success.is_empty() {
|
||||||
|
bail!("deletion event not accepted by any relay");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ Mechanical, no behavior change.
|
|||||||
Replace `push_staged_to_grasps` with methods on the type that owns the refs:
|
Replace `push_staged_to_grasps` with methods on the type that owns the refs:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
/// How much to push to the grasp servers.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
pub enum GitPushMode {
|
pub enum GitPushMode {
|
||||||
/// Mirror all branches and tags.
|
/// Mirror all branches and tags.
|
||||||
#[default]
|
#[default]
|
||||||
@@ -106,7 +107,7 @@ impl SyncState {
|
|||||||
// via cx.background_spawn (blocking gix work). Reports every transition
|
// via cx.background_spawn (blocking gix work). Reports every transition
|
||||||
// through on_step. Ends with the relay fan-out broadcast. Errors once
|
// 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).
|
// up front when self.path is None (state not built from a local repo).
|
||||||
pub(crate) async fn push_to_grasps<P>(
|
pub async fn push_to_grasps<P>(
|
||||||
&self,
|
&self,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
@@ -118,11 +119,10 @@ impl SyncState {
|
|||||||
P: FnMut(PushStepStatus, &mut AsyncApp) -> Result<(), Error>,
|
P: FnMut(PushStepStatus, &mut AsyncApp) -> Result<(), Error>,
|
||||||
{ ... }
|
{ ... }
|
||||||
|
|
||||||
// announce_repository_and_push moved as-is: sign + broadcast the
|
// Announces the pre-signed announcement (AnnounceRepository step), then
|
||||||
// announcement (AnnounceRepository step), parse it into an
|
// delegates to push_to_grasps with the pre-signed state event. No signer:
|
||||||
// `Announcement`, sign the state event, delegate to push_to_grasps,
|
// on failure the call site retracts the announcement by id.
|
||||||
// retract on total failure. This one keeps the signer.
|
pub async fn announce_and_push<P>(...) -> Result<Event, Error> { ... }
|
||||||
pub(crate) async fn announce_and_push<S, P>(...) -> Result<Event, Error> { ... }
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ The struct carries the job description — what (`refs`, `head`), from where (`p
|
|||||||
|
|
||||||
- `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`.
|
- `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.
|
- `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 with the callers (`push_repo_from`, `announce_and_push`, which also signs the announcement and the retraction).
|
- 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.
|
- `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.
|
- `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.
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ The struct carries the job description — what (`refs`, `head`), from where (`p
|
|||||||
|
|
||||||
- Add `BackendEvent::PushProgress { addr: RepoAddr, status: PushStepStatus }`.
|
- 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.
|
- `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`), then call `announce_and_push(cx, client, signer, announcement, on_step)`.
|
- `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.
|
- `push.rs` keeps `GraspServer`, `PushOutcome` and the URL helpers.
|
||||||
|
|
||||||
## Step 4 — UI checklist
|
## Step 4 — UI checklist
|
||||||
|
|||||||
Reference in New Issue
Block a user