add send patch panel

This commit is contained in:
2026-09-02 15:26:50 +07:00
parent c03ce50c82
commit b9054346f7
9 changed files with 422 additions and 94 deletions
+60 -49
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, Subscription, Task, WeakEntity};
use gpui::{AppContext, AsyncApp, Context, SharedString, Subscription, Task, WeakEntity};
use nostr::event::IntoEventBuilder;
use nostr_sdk::prelude::*;
use signed_core::{
@@ -157,12 +157,22 @@ impl RepoStore {
store
}
/// Returns the repository's address.
pub fn addr(&self) -> &RepoAddr {
&self.addr
}
/// Filters that make up a repository: announcement, state, activity and
/// deletions targeting it.
/// Returns the repository's name, or "Unknown" if not known.
pub fn name(&self) -> SharedString {
self.announcement
.as_ref()
.map_or(SharedString::default(), |a| {
a.name.clone().unwrap_or(SharedString::from("Unknown"))
})
}
/// Filters that make up a repository: announcement, state,
/// activity and deletions targeting it.
fn repo_filters(addr: &RepoAddr) -> Vec<Filter> {
let mut filters = vec![
// Announcement and state share author and identifier, so they
@@ -202,8 +212,7 @@ impl RepoStore {
});
}
/// Fetch this repository's events from the bootstrap relays (one-shot,
/// auto-closing subscription).
/// Fetch this repository's events from the bootstrap relays
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
let backend = Backend::global(cx);
let addr = self.addr.clone();
@@ -215,10 +224,8 @@ impl RepoStore {
/// Re-query the local database and update all fields.
///
/// Debounced: a short delay collapses bursts of requests (e.g. per-event
/// `NostrUpdate`s), and requests that arrive while a query is running are
/// folded into one follow-up query. The query and processing run on a
/// background thread; only the results are applied on the main thread.
/// The query and processing run on a background thread,
/// only the results are applied on the main thread.
pub fn refresh(&mut self, cx: &mut Context<Self>) {
if self.refreshing {
self.refresh_dirty = true;
@@ -242,7 +249,6 @@ impl RepoStore {
self.tasks.push(task);
}
/// One query + apply cycle (debounced entry point).
fn run_refresh(&mut self, cx: &mut Context<Self>) {
self.refreshing = true;
@@ -297,13 +303,15 @@ impl RepoStore {
// NIP-22 comments reference their root via an `E`/`e` tag rather
// than the repository's `a` tag, so query them by the root events
// of this repository.
let db = client.database();
let mut seen_comments: HashSet<EventId> = comments.iter().map(|e| e.id).collect();
let db = client.database();
let roots = issues
.iter()
.chain(&patches)
.chain(&pull_requests)
.map(|e| e.id);
for filter in filters::comments_for(roots) {
for event in db.query(filter).await? {
if seen_comments.insert(event.id) {
@@ -312,16 +320,17 @@ impl RepoStore {
}
}
// Status events may omit their `a` tag (NIP-34 makes it
// optional), so also query them by the root events they
// reference.
let db = client.database();
// Status events may omit their `a` tag,
// so also query them by the root events they reference.
let mut seen_statuses: HashSet<EventId> = statuses.iter().map(|e| e.id).collect();
let db = client.database();
let roots = issues
.iter()
.chain(&patches)
.chain(&pull_requests)
.map(|e| e.id);
for root in roots {
for event in db.query(filters::statuses_for([root])).await? {
if seen_statuses.insert(event.id) {
@@ -330,17 +339,18 @@ impl RepoStore {
}
}
// Cover notes (1624) and label events (1985) reference their
// target via an `e` tag, so query them per root like comments
// and statuses.
let db = client.database();
// Cover notes (1624) and label events (1985) reference
// so query them per root like comments and statuses.
let mut seen_cover_notes: HashSet<EventId> = cover_notes.iter().map(|e| e.id).collect();
let mut seen_labels: HashSet<EventId> = labels.iter().map(|e| e.id).collect();
let db = client.database();
let roots = issues
.iter()
.chain(&patches)
.chain(&pull_requests)
.map(|e| e.id);
for root in roots {
for event in db.query(filters::annotations_for([root])).await? {
if deletions.is_deleted(&event) {
@@ -368,12 +378,15 @@ impl RepoStore {
.as_ref()
.map(Announcement::effective_maintainers)
.unwrap_or_default();
let status_by_root =
resolve_statuses(&issues, &patches, &pull_requests, &statuses, &maintainers);
let open_issue_count = issues
.iter()
.filter(|issue| status_of(&status_by_root, issue) == RepoStatus::Open)
.count();
let open_pr_count = pull_requests
.iter()
.filter(|pr| {
@@ -426,8 +439,8 @@ impl RepoStore {
let again = this.update(cx, |this, cx| {
this.announcement = announcement;
// The announcement may list relays for this repository's
// activity; connect to any we haven't fetched from yet.
// The announcement may list relays for this repository's activity,
// connect to any we haven't fetched from yet.
let relays = this
.announcement
.as_ref()
@@ -462,11 +475,13 @@ impl RepoStore {
.chain(&this.pull_requests)
.map(|e| e.id)
.collect::<HashSet<EventId>>();
let new_roots: Vec<EventId> = roots
.iter()
.filter(|id| !this.root_fetches.contains(id))
.copied()
.collect();
if !new_roots.is_empty() {
this.root_fetches.extend(new_roots.iter().copied());
// Batch the per-root filters: one statuses filter and one
@@ -476,6 +491,7 @@ impl RepoStore {
let mut root_filters = filters::comments_for(new_roots.clone());
root_filters.push(filters::statuses_for(new_roots.iter().copied()));
root_filters.push(filters::annotations_for(new_roots));
let announced: Vec<RelayUrl> = this.repo_relays.iter().cloned().collect();
let backend = Backend::global(cx);
backend.update(cx, |backend, cx| {
@@ -495,8 +511,7 @@ impl RepoStore {
}
})?;
// Requests that arrived while the refresh was running are
// coalesced into one follow-up refresh.
// Requests that arrived while the refresh was running are coalesced into one follow-up refresh.
if again {
this.update(cx, |this, cx| this.refresh(cx))?;
}
@@ -505,21 +520,19 @@ impl RepoStore {
}));
}
/// Resolve the status of a root event (issue / patch / PR) per NIP-34:
/// a lookup into the map built on the last refresh.
/// Resolve the status of a root event (issue / patch / PR) per NIP-34
pub fn status_of(&self, root: &Event) -> RepoStatus {
status_of(&self.status_by_root, root)
}
/// Refresh generation, incremented on every applied refresh. Views use
/// it to key their derived-data caches (filtered lists, counts) so
/// renders that change nothing stay O(1).
/// Refresh generation, incremented on every applied refresh.
/// Views use it to key their derived-data caches.
pub fn version(&self) -> u64 {
self.version
}
/// The effective cover note of `root` (kind 1624), if any: the latest
/// note authored by the root author or a maintainer.
/// The effective cover note of `root` (kind 1624), if any:
/// the latest note authored by the root author or a maintainer.
pub fn cover_note_of(&self, root: &Event) -> Option<&Event> {
let maintainers = self
.announcement
@@ -557,14 +570,13 @@ impl RepoStore {
/// Number of open issues: issues whose resolved status is
/// [`RepoStatus::Open`] (issues without status events default to open).
/// Cached on the last refresh.
pub fn issue_count(&self) -> usize {
self.open_issue_count
}
/// Number of open pull requests: root PR events (not PR updates, whose
/// status is carried by the root) with a resolved status of
/// [`RepoStatus::Open`]. Cached on the last refresh.
/// [`RepoStatus::Open`].
pub fn pull_request_count(&self) -> usize {
self.open_pr_count
}
@@ -596,15 +608,13 @@ impl RepoStore {
.filter(move |e| signed_core::references_root(e, root))
}
/// Comment on a root event (issue / PR) per NIP-34 (kind 1111), using
/// the SDK's NIP-22 `CommentBuilder` so other NIP-34 clients (ngit,
/// GitWorkshop) can thread the comment.
/// Comment on a root event (issue / PR) per NIP-34 (kind 1111)
pub fn comment(&mut self, root: &Event, content: String, cx: &mut Context<Self>) {
self.reply(root, None, content, cx);
}
/// Reply to `parent` (a comment on `root`) with a NIP-22 threaded
/// comment; `None` publishes a top-level comment on the root itself.
/// Reply to `parent` (a comment on `root`) with a NIP-22 threaded comment,
/// `None` publishes a top-level comment on the root itself.
pub fn reply(
&mut self,
root: &Event,
@@ -664,6 +674,7 @@ impl RepoStore {
.into_iter()
.map(str::to_owned)
.collect();
if let Some(oversized) = series
.iter()
.find(|part| part.len() > MAX_PATCH_EVENT_BYTES)
@@ -677,8 +688,7 @@ impl RepoStore {
return;
}
// The tip of the series is its last commit; `git format-patch`
// orders patches oldest first.
// The tip of the series is its last commit; `git format-patch` orders patches oldest first.
let Some(current_commit) = series
.last()
.and_then(|part| patch_current_commit(part))
@@ -692,16 +702,18 @@ impl RepoStore {
};
let backend = Backend::global(cx);
let signer = backend.read(cx).signer();
if backend.read(cx).current_user().is_none() {
self.last_error = Some("Sign in to open a pull request".into());
cx.notify();
return;
}
let signer = backend.read(cx).signer();
let addr = self.addr.clone();
let owner = self.addr.public_key;
let euc = self.announcement.as_ref().and_then(|a| a.euc.clone());
let (push_owner, push_repo_id, push_relays) = self
.announcement
.as_ref()
@@ -742,9 +754,8 @@ impl RepoStore {
subject,
labels: Vec::new(),
branch_name,
// NIP-34: PRs carry at least one clone URL where the
// tip commit can be downloaded; the announced mirrors
// are also the servers the tip is pushed to below.
// NIP-34: PRs carry at least one clone URL where the tip commit can be downloaded,
// the announced mirrors are also the servers the tip is pushed to below.
clone: this
.announcement
.as_ref()
@@ -757,8 +768,7 @@ impl RepoStore {
}
.into_event_builder();
// NIP-34: the `r` EUC tag lets clients subscribe to all
// PRs of this repository; the SDK builder omits it.
// NIP-34: the `r` EUC tag lets clients subscribe to all PRs of this repository
match this.announcement.as_ref().and_then(|a| a.euc.clone()) {
Some(euc) => builder.tag(Tag::parse(["r", &euc]).expect("valid r tag")),
None => builder,
@@ -803,6 +813,7 @@ impl RepoStore {
}
})
.await;
if pushed == 0 {
this.update(cx, |this, cx| {
this.last_warning = Some(format!(
@@ -818,6 +829,7 @@ impl RepoStore {
let backend = Backend::global(cx);
backend.update(cx, |backend, cx| backend.publish_event(event, cx))
})?;
let pr_event = match publish_task.await {
Ok(event) => event,
Err(e) => {
@@ -828,8 +840,8 @@ impl RepoStore {
}
};
// NIP-34: a draft PR carries a kind-1633 status event; publish
// it right after the PR event so viewers never show it open.
// NIP-34: a draft PR carries a kind-1633 status event,
// publish it right after the PR event so viewers never show it open.
if draft {
this.update(cx, |this, cx| {
this.set_status(&pr_event, RepoStatus::Draft, cx);
@@ -842,8 +854,7 @@ impl RepoStore {
/// Update a pull request: publish revision patch events chained to the
/// original root patch (`t root-revision` and a NIP-10 `e` reply on the
/// first, per NIP-34), then a kind-1619 PR update event carrying the
/// new tip.
/// first, per NIP-34), then a kind-1619 PR update event carrying the new tip.
///
/// Only the PR author may update it; other authors must open a new PR.
pub fn update_pull_request(&mut self, root: &Event, patch: String, cx: &mut Context<Self>) {