This commit is contained in:
2026-09-02 09:14:04 +07:00
parent 6f1256757f
commit c054f61593
10 changed files with 373 additions and 41 deletions
+157 -11
View File
@@ -3,12 +3,14 @@ use std::collections::{HashMap, HashSet};
use std::time::Duration;
use anyhow::Error;
use bitcoin_hashes::sha1::Hash as Sha1Hash;
use gpui::{AppContext, Context, Subscription, Task};
use nostr::event::IntoEventBuilder;
use nostr_sdk::prelude::*;
use signed_core::{
Announcement, COVER_NOTE_KIND, Deletions, RepoAddr, RepoStatus, build_state, cover_note,
filters, labels_and_subject, parse_state, pull_request_patch, subject_override,
filters, labels_and_subject, parse_state, pull_request_patch, pull_request_patches,
subject_override,
};
use crate::backend::{Backend, BackendEvent};
@@ -235,7 +237,8 @@ impl RepoStore {
fn run_refresh(&mut self, cx: &mut Context<Self>) {
self.refreshing = true;
let client = Backend::global(cx).read(cx).client();
let backend = Backend::global(cx);
let client = backend.read(cx).client();
let addr = self.addr.clone();
let work = cx.background_spawn(async move {
@@ -623,17 +626,21 @@ impl RepoStore {
/// carry a real commit id for other NIP-34 clients to verify and apply
/// the proposal. The `clone` tag carries the announced mirror URLs; the
/// linked patch is the source of truth until the commit is pushed there.
/// `branch_name` lands in the PR's `branch-name` tag (NIP-34); `draft`
/// publishes a kind-1633 status right after the PR event.
pub fn open_pull_request(
&mut self,
subject: Option<String>,
description: String,
branch_name: Option<String>,
patch: String,
draft: bool,
cx: &mut Context<Self>,
) {
self.last_error = None;
let Some(current_commit) =
patch_current_commit(&patch).and_then(|hex| hex.parse::<bitcoin_hashes::Sha1>().ok())
patch_current_commit(&patch).and_then(|hex| hex.parse::<Sha1Hash>().ok())
else {
self.last_error = Some(
"Patch must be `git format-patch` output with a `From <commit-id>` header".into(),
@@ -666,8 +673,8 @@ impl RepoStore {
}
let patch_builder = EventBuilder::new(Kind::GitPatch, patch).tags(patch_tags);
let patch_task =
Backend::global(cx).update(cx, |backend, cx| backend.send(patch_builder, cx));
let backend = Backend::global(cx);
let patch_task = backend.update(cx, |backend, cx| backend.send(patch_builder, cx));
self.tasks.push(cx.spawn(async move |this, cx| {
let patch_event = match patch_task.await {
@@ -688,7 +695,7 @@ impl RepoStore {
content: description,
subject,
labels: Vec::new(),
branch_name: None,
branch_name,
// NIP-34: PRs carry at least one clone URL where the
// tip commit can be downloaded; use the repository's
// announced mirrors until a push backend exists.
@@ -703,10 +710,146 @@ impl RepoStore {
}
.into_event_builder();
Backend::global(cx).update(cx, |backend, cx| backend.send(builder, cx))
// NIP-34: the `r` EUC tag lets clients subscribe to all PRs of this repository.
let builder = 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,
};
let backend = Backend::global(cx);
backend.update(cx, |backend, cx| backend.send(builder, cx))
})?;
if let Err(e) = pr_task.await {
let pr_event = match pr_task.await {
Ok(event) => event,
Err(e) => {
return this.update(cx, |this, cx| {
this.last_error = Some(e.to_string());
cx.notify();
});
}
};
// 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);
})?;
}
Ok(())
}));
}
/// Update a pull request: publish a revision patch event chained to the
/// original root patch (`t root-revision` and a NIP-10 `e` reply, 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>) {
self.last_error = None;
let backend = Backend::global(cx);
let Some(user) = backend.read(cx).current_user() else {
self.last_error = Some("Sign in to update the pull request".into());
cx.notify();
return;
};
if user != root.pubkey {
self.last_error = Some("Only the pull request author can update it".into());
cx.notify();
return;
}
let Some(current_commit) =
patch_current_commit(&patch).and_then(|hex| hex.parse::<Sha1Hash>().ok())
else {
self.last_error = Some(
"Patch must be `git format-patch` output with a `From <commit-id>` header".into(),
);
cx.notify();
return;
};
// NIP-34: the first patch of a revision replies to the original
// root patch (the PR's `e` tag; fall back to the oldest patch of
// the linked set for PRs without one).
let root_patch_id = root.tags.event_ids().next().or_else(|| {
pull_request_patches(root, self.patches.iter())
.first()
.map(|p| p.id)
});
let commit_hex = current_commit.to_string();
let mut patch_tags = vec![
Tag::coordinate(self.addr.clone(), None),
Tag::public_key(self.addr.public_key),
Tag::parse(["t", "root-revision"]).expect("valid root-revision tag"),
];
if let Some(root_patch_id) = root_patch_id
&& let Ok(tag) = Tag::parse(["e", &root_patch_id.to_hex(), "", "reply"])
{
patch_tags.push(tag);
}
// NIP-34: the `r` EUC tag lets clients subscribe to all patches of
// this repository; `commit`/`r` tags reference the new tip.
if let Some(euc) = self.announcement.as_ref().and_then(|a| a.euc.clone())
&& let Ok(tag) = Tag::parse(["r", &euc])
{
patch_tags.push(tag);
}
if let Ok(tag) = Tag::parse(["commit", &commit_hex]) {
patch_tags.push(tag);
}
if let Ok(tag) = Tag::parse(["r", &commit_hex]) {
patch_tags.push(tag);
}
let patch_builder = EventBuilder::new(Kind::GitPatch, patch).tags(patch_tags);
let backend = Backend::global(cx);
let patch_task = backend.update(cx, |backend, cx| backend.send(patch_builder, cx));
let root = root.clone();
let clone: Vec<Url> = self
.announcement
.as_ref()
.map(|a| a.clone.clone())
.unwrap_or_default();
let euc = self.announcement.as_ref().and_then(|a| a.euc.clone());
self.tasks.push(cx.spawn(async move |this, cx| {
if let Err(e) = patch_task.await {
return this.update(cx, |this, cx| {
this.last_error = Some(e.to_string());
cx.notify();
});
}
let update_task = this.update(cx, |this, cx| {
let builder = GitPullRequestUpdate {
repository: this.addr.clone(),
pull_request_event: root.id,
pull_request_author: root.pubkey,
current_commit,
clone: clone.clone(),
merge_base: None,
}
.into_event_builder();
// NIP-34: the `r` EUC tag lets clients subscribe to all PR
// updates of this repository; the SDK builder omits it.
let builder = match euc.as_deref() {
Some(euc) => builder.tag(Tag::parse(["r", euc]).expect("valid r tag")),
None => builder,
};
backend.update(cx, |backend, cx| backend.send(builder, cx))
})?;
if let Err(e) = update_task.await {
return this.update(cx, |this, cx| {
this.last_error = Some(e.to_string());
cx.notify();
@@ -729,7 +872,8 @@ impl RepoStore {
.map(Announcement::effective_maintainers)
.unwrap_or_default();
let Some(user) = Backend::global(cx).read(cx).current_user() else {
let backend = Backend::global(cx);
let Some(user) = backend.read(cx).current_user() else {
self.last_error = Some("Sign in to change the status".into());
cx.notify();
return;
@@ -761,7 +905,8 @@ impl RepoStore {
pub fn publish_state(&mut self, cx: &mut Context<Self>) {
self.last_error = None;
let Some(user) = Backend::global(cx).read(cx).current_user() else {
let backend = Backend::global(cx);
let Some(user) = backend.read(cx).current_user() else {
self.last_error = Some("Sign in to publish repository state".into());
cx.notify();
return;
@@ -924,7 +1069,8 @@ fn resolve_statuses(
.chain(pull_requests)
.map(|root| {
let events = by_root.get(&root.id).map(Vec::as_slice).unwrap_or(&[]);
let status = signed_core::resolve_status(events.iter().copied(), &root.pubkey, maintainers);
let status =
signed_core::resolve_status(events.iter().copied(), &root.pubkey, maintainers);
(root.id, status)
})
.collect()