add pull request viewer

This commit is contained in:
2026-08-22 20:38:22 +07:00
parent 385718d74d
commit 7fb7275233
9 changed files with 1737 additions and 187 deletions
+68 -35
View File
@@ -3,7 +3,9 @@ use std::time::Duration;
use anyhow::Error;
use gpui::{AppContext, Context, Subscription, Task};
use nostr_sdk::prelude::*;
use signed_core::{Announcement, Deletions, RepoAddr, RepoStatus, filters, parse_state};
use signed_core::{
Announcement, Deletions, RepoAddr, RepoStatus, filters, parse_state, pull_request_patch,
};
use crate::backend::{Backend, BackendEvent};
use crate::git_store::GitStore;
@@ -211,6 +213,7 @@ impl RepoStore {
});
self.tasks.retain(|task| !task.is_ready());
self.tasks.push(cx.spawn(async move |this, cx| {
let (announcement, state, issues, patches, pull_requests, statuses, comments) =
match work.await {
@@ -277,7 +280,7 @@ impl RepoStore {
/// Number of open issues: issues whose resolved status is
/// [`RepoStatus::Open`] (issues without status events default to open).
pub fn open_issue_count(&self) -> usize {
pub fn issue_count(&self) -> usize {
self.issues
.iter()
.filter(|issue| self.status_of(issue) == RepoStatus::Open)
@@ -287,7 +290,7 @@ impl RepoStore {
/// 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`].
pub fn open_pull_request_count(&self) -> usize {
pub fn pull_request_count(&self) -> usize {
self.pull_requests
.iter()
.filter(|pr| pr.kind == Kind::GitPullRequest && self.status_of(pr) == RepoStatus::Open)
@@ -336,52 +339,81 @@ impl RepoStore {
self.send(builder, cx);
}
/// Open a pull request on this repository: a root PR event whose content
/// is the `git format-patch` output of the proposed changes.
/// Open a pull request on this repository: a root PR event (kind 1618)
/// whose content is the markdown description, plus a root patch event
/// (kind 1617) carrying the `git format-patch` output, which the PR
/// references via an `e` tag (NIP-34).
///
/// The branch metadata (branch name, clone URL, merge base, root patch)
/// isn't known to the UI yet and is left empty; the proposed commit is
/// parsed from the patch's `From <commit>` header, falling back to an
/// empty hash for hand-written content.
/// The patch is published first and the PR is sent once the patch
/// event's id is known, so the two always arrive together. The branch
/// metadata (branch name, clone URL, merge base) isn't known to the UI
/// yet and is left empty; the proposed commit is parsed from the patch's
/// `From <commit>` header, falling back to an empty hash for hand-written
/// content.
pub fn open_pull_request(
&mut self,
subject: Option<String>,
content: String,
description: String,
patch: String,
cx: &mut Context<Self>,
) {
let current_commit = patch_current_commit(&content)
self.last_error = None;
let current_commit = patch_current_commit(&patch)
.and_then(|hex| hex.parse().ok())
.unwrap_or_else(|| bitcoin_hashes::Sha1::from_byte_array([0u8; 20]));
let builder = GitPullRequest {
repository: self.addr.clone(),
content,
subject,
labels: Vec::new(),
branch_name: None,
clone: Vec::new(),
current_commit,
root_patch_event: None,
merge_base: None,
}
.into_event_builder();
self.send(builder, cx);
}
/// Send a root patch (`git format-patch` output) to this repository.
pub fn send_root_patch(&mut self, patch: String, cx: &mut Context<Self>) {
let Ok(root_marker) = Tag::parse(["t", "root"]) else {
return;
};
let builder = EventBuilder::new(Kind::GitPatch, patch).tags([
let patch_builder = EventBuilder::new(Kind::GitPatch, patch).tags([
Tag::coordinate(self.addr.clone(), None),
Tag::public_key(self.addr.public_key),
root_marker,
]);
self.send(builder, cx);
let patch_task =
Backend::global(cx).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 {
Ok(event) => event,
Err(e) => {
return this.update(cx, |this, cx| {
this.last_error = Some(e.to_string());
cx.notify();
});
}
};
// The PR references the patch event so viewers can find the
// patch without carrying it inline.
let pr_task = this.update(cx, |this, cx| {
let builder = GitPullRequest {
repository: this.addr.clone(),
content: description,
subject,
labels: Vec::new(),
branch_name: None,
clone: Vec::new(),
current_commit,
root_patch_event: Some(patch_event.id),
merge_base: None,
}
.into_event_builder();
Backend::global(cx).update(cx, |backend, cx| backend.send(builder, cx))
})?;
if let Err(e) = pr_task.await {
return this.update(cx, |this, cx| {
this.last_error = Some(e.to_string());
cx.notify();
});
}
Ok(())
}));
}
/// Set the status of a root event (requires being the root author or a maintainer).
@@ -400,8 +432,9 @@ impl RepoStore {
self.send(builder, cx);
}
/// Merge a pull request: apply its patch (`git format-patch` output) to
/// the local clone of this repository, then publish the merged status.
/// Merge a pull request: apply its patch (the content of the linked
/// root patch event) to the local clone of this repository, then publish
/// the merged status.
///
/// Only the repository author may merge. The clone is created on demand
/// from the announcement's clone URLs when the repository hasn't been
@@ -427,7 +460,7 @@ impl RepoStore {
.as_ref()
.map(|a| a.clone.iter().map(ToString::to_string).collect())
.unwrap_or_default();
let patch = root.content.clone();
let patch = pull_request_patch(root, self.patches.iter());
let root = root.clone();
let apply = cx.background_spawn(async move {