add send patch panel
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, 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>) {
|
||||
|
||||
Reference in New Issue
Block a user