update
This commit is contained in:
@@ -10,7 +10,7 @@ use nostr::event::IntoEventBuilder;
|
||||
use nostr_connect::prelude::*;
|
||||
use nostr_sdk::client::SyncSummary;
|
||||
use nostr_sdk::prelude::*;
|
||||
use signed_core::{Announcement, build_state, filters, identifier_from_name, repo_addr};
|
||||
use signed_core::{Announcement, RepoAddr, build_state, filters, identifier_from_name, repo_addr};
|
||||
use signed_nostr::{SignedAuthUrlHandler, UniversalSigner, Update};
|
||||
|
||||
use crate::git_store::GitStore;
|
||||
@@ -497,15 +497,30 @@ impl Backend {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let builder = build_state(
|
||||
&repo_id,
|
||||
&[("refs/heads/main".to_owned(), commit)],
|
||||
Some("main"),
|
||||
);
|
||||
this.send(builder, cx)
|
||||
})?
|
||||
.await?;
|
||||
let state_event = match this
|
||||
.update(cx, |this, cx| {
|
||||
let builder = build_state(
|
||||
&repo_id,
|
||||
&[("refs/heads/main".to_owned(), commit)],
|
||||
Some("main"),
|
||||
);
|
||||
this.send(builder, cx)
|
||||
})?
|
||||
.await
|
||||
{
|
||||
Ok(state_event) => state_event,
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.retract_events(std::slice::from_ref(&event), cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
return Err(e.context(
|
||||
"The repository was announced, but its state could not be published. \
|
||||
The announcement has been retracted",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Push the initial commit to every grasp server. A server
|
||||
// that fails to accept the push is logged, but the creation
|
||||
@@ -517,7 +532,19 @@ impl Backend {
|
||||
let servers = servers.clone();
|
||||
push_to_grasp_servers(path, owner, repo_id, servers, signed_git::push_main)
|
||||
});
|
||||
push.await?;
|
||||
if let Err(e) = push.await {
|
||||
// The events are already published; retract them so the
|
||||
// repository doesn't remain announced without content.
|
||||
this.update(cx, |this, cx| {
|
||||
this.retract_events(&[event.clone(), state_event.clone()], cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
return Err(e.context(
|
||||
"The repository was announced, but the push to every grasp server failed. \
|
||||
The announcement has been retracted",
|
||||
));
|
||||
}
|
||||
|
||||
Announcement::from_event(&event).ok_or_else(|| anyhow!("failed to parse announcement"))
|
||||
})
|
||||
@@ -624,11 +651,26 @@ impl Backend {
|
||||
|
||||
let refs = state.refs.clone();
|
||||
let head = state.head.clone();
|
||||
this.update(cx, |this, cx| {
|
||||
let builder = build_state(&repo_id, &refs, head.as_deref());
|
||||
this.send(builder, cx)
|
||||
})?
|
||||
.await?;
|
||||
let state_event = match this
|
||||
.update(cx, |this, cx| {
|
||||
let builder = build_state(&repo_id, &refs, head.as_deref());
|
||||
this.send(builder, cx)
|
||||
})?
|
||||
.await
|
||||
{
|
||||
Ok(state_event) => state_event,
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.retract_events(std::slice::from_ref(&event), cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
return Err(e.context(
|
||||
"The repository was announced, but its state could not be published. \
|
||||
The announcement has been retracted",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Push every branch and tag to each grasp server. A server
|
||||
// that fails to accept the push is logged, but the init only
|
||||
@@ -642,7 +684,17 @@ impl Backend {
|
||||
let servers = servers.clone();
|
||||
push_to_grasp_servers(path, owner, repo_id, servers, signed_git::push_all)
|
||||
});
|
||||
push.await?;
|
||||
if let Err(e) = push.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.retract_events(&[event.clone(), state_event.clone()], cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
return Err(e.context(
|
||||
"The repository was announced, but the push to every grasp server failed. \
|
||||
The announcement has been retracted",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Point `origin` at the first grasp server so later pushes
|
||||
@@ -660,6 +712,106 @@ impl Backend {
|
||||
})
|
||||
}
|
||||
|
||||
/// Re-push the repository's current refs to the grasp servers announced
|
||||
/// in its `relays` tag: publishes a fresh state event (the push
|
||||
/// authorization), then pushes every branch and tag, like the init
|
||||
/// flow. The repository must have a local clone in the cache.
|
||||
pub fn push_repository(
|
||||
&mut self,
|
||||
announcement: Announcement,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<(), Error>> {
|
||||
let addr = announcement.addr();
|
||||
let cache = GitStore::global(cx).cache().clone();
|
||||
let path = cache.repo_path(&addr);
|
||||
let owner = announcement
|
||||
.owner
|
||||
.to_bech32()
|
||||
.unwrap_or_else(|_| announcement.owner.to_hex());
|
||||
let repo_id = announcement.id.clone();
|
||||
let relays = announcement.relays.clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
// 1. Read the current refs of the local clone.
|
||||
let work = cx.background_spawn({
|
||||
let path = path.clone();
|
||||
async move { signed_git::worktree_ref_state(&path) }
|
||||
});
|
||||
let state = work.await?;
|
||||
|
||||
// 2. Publish a fresh state event; grasp servers authorize a
|
||||
// push by the state they have seen.
|
||||
let refs = state.refs.clone();
|
||||
let head = state.head.clone();
|
||||
this.update(cx, |this, cx| {
|
||||
let builder = build_state(&repo_id, &refs, head.as_deref());
|
||||
this.send(builder, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
// 3. Push every branch and tag to the announced grasp servers.
|
||||
if !refs.is_empty() {
|
||||
let push = cx.background_spawn({
|
||||
let path = path.clone();
|
||||
let owner = owner.clone();
|
||||
let repo_id = repo_id.clone();
|
||||
let relays = relays.clone();
|
||||
async move {
|
||||
push_to_grasp_servers(path, owner, repo_id, relays, signed_git::push_all)
|
||||
.await
|
||||
}
|
||||
});
|
||||
push.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete the repository from nostr: publish NIP-09 deletions for its
|
||||
/// announcement, state and activity events (issues, pull requests,
|
||||
/// patches, statuses, comments). Only the repository owner may delete
|
||||
/// it.
|
||||
pub fn delete_repository(
|
||||
&mut self,
|
||||
addr: RepoAddr,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<(), Error>> {
|
||||
let Some(public_key) = self.current_user else {
|
||||
return Task::ready(Err(anyhow!("Sign in to delete a repository")));
|
||||
};
|
||||
if public_key != addr.public_key {
|
||||
return Task::ready(Err(anyhow!("Only the repository owner can delete it")));
|
||||
}
|
||||
|
||||
let client = self.client.clone();
|
||||
let addr = addr.clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
// Collect every event of the repository from the local database.
|
||||
let events = cx.background_spawn(async move {
|
||||
let db = client.database();
|
||||
let mut events = Vec::new();
|
||||
for filter in [
|
||||
filters::announcement(&addr),
|
||||
filters::state(&addr),
|
||||
filters::activity(&addr),
|
||||
] {
|
||||
events.extend(db.query(filter).await?);
|
||||
}
|
||||
Ok::<_, Error>(events)
|
||||
});
|
||||
let events = events.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.retract_events(&events, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Login with an `nsec1...` key or a `bunker://...` URI, dispatching on
|
||||
/// the credential's prefix.
|
||||
pub fn login(&mut self, credential: &str, cx: &mut Context<Self>) {
|
||||
@@ -1149,6 +1301,32 @@ impl Backend {
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Publish a NIP-09 deletion event for `events` (best-effort), so a
|
||||
/// publish that fails midway can retract the events that were already
|
||||
/// broadcast to relays. Failures are logged, not surfaced: the caller's
|
||||
/// error already told the user what happened.
|
||||
fn retract_events(&mut self, events: &[Event], cx: &mut Context<Self>) {
|
||||
if events.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut tags: Vec<Tag> = Vec::with_capacity(events.len() * 2);
|
||||
|
||||
for event in events {
|
||||
tags.push(Tag::event(event.id));
|
||||
tags.push(Tag::parse(["k", &event.kind.to_string()]).expect("valid kind tag"));
|
||||
}
|
||||
|
||||
let task = self.send(EventBuilder::new(Kind::EventDeletion, "").tags(tags), cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, _cx| {
|
||||
if let Err(e) = task.await {
|
||||
log::warn!("failed to retract repository events: {e}");
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the given relays, connect to them, and fetch the filters: a one-shot
|
||||
|
||||
@@ -86,8 +86,13 @@ impl RepoStore {
|
||||
let kind = event.kind == Kind::GitRepoAnnouncement;
|
||||
let author = event.pubkey == this.addr.public_key;
|
||||
let coordinate = event.tags.coordinates().into_iter().any(|c| c == this.addr);
|
||||
// Locally published deletions may target any event of
|
||||
// this repository; refresh so they take effect
|
||||
// immediately, like relay deletions.
|
||||
let deletion =
|
||||
event.kind == Kind::EventDeletion || event.kind == Kind::RequestToVanish;
|
||||
|
||||
coordinate || (kind && author)
|
||||
coordinate || (kind && author) || deletion
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -98,8 +98,14 @@ impl RepoListStore {
|
||||
}
|
||||
}
|
||||
BackendEvent::Published(event) => {
|
||||
event.kind == Kind::GitRepoAnnouncement
|
||||
&& this.author.is_none_or(|a| a == event.pubkey)
|
||||
let announcement = event.kind == Kind::GitRepoAnnouncement
|
||||
&& this.author.is_none_or(|a| a == event.pubkey);
|
||||
// Locally published deletions (e.g. deleting a repo)
|
||||
// are already in the local database; refresh so they
|
||||
// take effect immediately, like relay deletions.
|
||||
let deletion =
|
||||
event.kind == Kind::EventDeletion || event.kind == Kind::RequestToVanish;
|
||||
announcement || deletion
|
||||
}
|
||||
BackendEvent::Synced | BackendEvent::SyncProgress { .. } => true,
|
||||
_ => false,
|
||||
|
||||
Reference in New Issue
Block a user