This commit is contained in:
2026-09-04 08:16:05 +07:00
parent 212f35d6bb
commit 17de4f6376
43 changed files with 376 additions and 721 deletions
+3 -2
View File
@@ -1,8 +1,9 @@
use nostr::prelude::*;
/// Address of a NIP-34 repository announcement, `30617:<owner-pubkey>:<repo-id>`.
/// The Rust Nostr SDK's [`Coordinate`] parses, formats and hashes this.
/// The alias reuses the SDK type while keeping repository-specific vocabulary.
///
/// The Rust Nostr SDK's [`Coordinate`] parses, formats and hashes this,
/// the alias reuses the SDK type while keeping repository-specific vocabulary.
pub type RepoAddr = Coordinate;
/// Build the address of a NIP-34 repository announcement.
+21 -20
View File
@@ -1,13 +1,15 @@
use nostr::prelude::*;
/// ngit and GitWorkshop cover-note extension, kind 1624.
/// A markdown note attached to an issue, patch or PR by its author or a maintainer.
/// Not part of the NIP-34 draft, read support for interop.
/// GitWorkshop and `ngit` cover-note extension, kind 1624.
///
/// A markdown note attached to an issue, patch or PR by its author or a maintainer,
/// not part of the NIP-34 draft, read support for interop.
pub const COVER_NOTE_KIND: Kind = Kind::Custom(1624);
/// Whether a kind-1985 label event is a valid annotation of `root`.
/// The event references the root with a lowercase `e` tag.
/// Its author must be the root author or a maintainer.
///
/// The event references the root with a lowercase `e` tag,
/// its author must be the root author or a maintainer.
fn label_targets_root(event: &Event, root: &Event, maintainers: &[PublicKey]) -> bool {
if event.kind != Kind::Label {
return false;
@@ -22,8 +24,8 @@ fn label_targets_root(event: &Event, root: &Event, maintainers: &[PublicKey]) ->
.any(|tag| tag.kind() == "e" && tag.content().is_some_and(|content| content == root_id))
}
/// Whether a kind-1985 label event declares the `#t` namespace.
/// It must also carry at least one `["l", "<value>", "#t"]` label.
/// Whether a kind-1985 label event declares the `#t` namespace,
/// it must also carry at least one `["l", "<value>", "#t"]` label.
fn has_hashtag_labels(event: &Event) -> bool {
event.tags.iter().any(|tag| tag.as_slice() == ["L", "#t"])
&& event.tags.iter().any(|tag| {
@@ -32,11 +34,12 @@ fn has_hashtag_labels(event: &Event) -> bool {
})
}
/// Effective hashtag labels of `root`.
/// The `t` tags on the event itself, self-reported by its author.
/// Authorized NIP-32 kind-1985 events in the `#t` namespace add more.
/// Labels are additive, so all valid label events contribute.
/// There is no latest-wins semantics.
/// Effective hashtag labels of `root`,
/// the `t` tags on the event itself, self-reported by its author,
/// authorized NIP-32 kind-1985 events in the `#t` namespace add more.
///
/// Labels are additive, so all valid label events contribute,
/// there is no latest-wins semantics.
pub fn labels(root: &Event, label_events: &[Event], maintainers: &[PublicKey]) -> Vec<String> {
let mut labels: Vec<String> = root
.tags
@@ -62,10 +65,9 @@ pub fn labels(root: &Event, label_events: &[Event], maintainers: &[PublicKey]) -
labels
}
/// Subject or title override of `root` from authorized kind-1985 label events.
/// Only label events in the `#subject` namespace count.
/// The latest event wins, per NIP-01 replaceable semantics.
/// The tiebreak is the lexicographically larger event id.
/// Subject or title override of `root` from authorized kind-1985 label events,
/// only label events in the `#subject` namespace count.
///
/// Returns `None` when no valid override exists.
pub fn subject_override(
root: &Event,
@@ -105,8 +107,8 @@ pub fn subject_override(
})
}
/// Effective hashtag labels and subject override of `root` in one pass.
/// Mirrors ngit's `get_labels_and_subject`.
/// Effective hashtag labels and subject override of `root` in one pass,
/// mirrors ngit's `get_labels_and_subject`.
pub fn labels_and_subject(
root: &Event,
label_events: &[Event],
@@ -119,8 +121,7 @@ pub fn labels_and_subject(
}
/// Effective cover note of `root`.
/// The latest authorized kind-1624 event wins.
/// The tiebreak is the lexicographically larger event id, per NIP-01 replaceable semantics.
///
/// Returns `None` when no valid cover note exists.
pub fn cover_note<'a>(
root: &Event,
+8 -6
View File
@@ -2,8 +2,8 @@ use std::collections::{HashMap, HashSet};
use nostr::prelude::*;
/// A NIP-22 comment thread, a top-level comment on the root event.
/// Nested replies are ordered oldest first at every level.
/// A NIP-22 comment thread, a top-level comment on the root event,
/// nested replies are ordered oldest first at every level.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommentThread {
/// The thread's top-level comment.
@@ -23,11 +23,13 @@ fn comment_parent(event: &Event) -> Option<EventId> {
.and_then(|id| EventId::parse(id).ok())
}
/// Group the comments on a root issue, patch or PR into NIP-22 threads.
/// A comment whose parent is the root starts a thread.
/// Group the comments on a root issue, patch or PR into NIP-22 threads,
/// a comment whose parent is the root starts a thread.
///
/// Other comments nest under their parent comment.
/// Threads and replies are ordered oldest first.
/// Replies with a missing parent are made top-level threads, so none are dropped.
///
/// Threads and replies are ordered oldest first,
/// replies with a missing parent are made top-level threads, so none are dropped.
pub fn comment_threads(root: &Event, comments: &[Event]) -> Vec<CommentThread> {
// Index comments by their parent id.
// Comments without a parent tag reply to the root event itself.
+6 -2
View File
@@ -2,14 +2,17 @@ use std::collections::HashSet;
use nostr::prelude::*;
/// NIP-09 deletion requests and NIP-62 vanish requests.
/// Built from the kind-5 and kind-62 events in the local database.
/// NIP-09 deletion requests and NIP-62 vanish requests,
/// built from the kind-5 and kind-62 events in the local database.
///
/// Deleted events are hidden before they reach the UI.
///
/// Pass any event through [`Deletions::is_deleted`] before showing it.
pub struct Deletions {
/// `(deleted event id, expected author)` from `e` tags of kind-5 events.
ids: HashSet<(EventId, PublicKey)>,
/// `(coordinate, expected author, cutoff)` from `a` tags of kind-5 events.
///
/// All versions of the addressable event up to `cutoff` are deleted.
coords: Vec<(Coordinate, PublicKey, Timestamp)>,
/// `(author, cutoff)` from kind-62 vanish requests.
@@ -48,6 +51,7 @@ impl Deletions {
/// Whether the event is covered by a valid deletion or vanish request.
/// A request is valid when its author matches the deleted event's author, per NIP-09.
///
/// Addressable events are deleted up to the request's `created_at`.
pub fn is_deleted(&self, event: &Event) -> bool {
if self
+9 -12
View File
@@ -42,8 +42,8 @@ pub fn activity(addr: &RepoAddr) -> Filter {
}
/// Status events, kinds `1630..=1633`, referencing any of the given root events.
/// They are matched via the `#e` tag.
/// One filter covers all roots.
/// They are matched via the `#e` tag. One filter covers all roots.
///
/// A negentropy sync reconciles them in a single session, not one per root.
pub fn statuses_for(roots: impl IntoIterator<Item = EventId>) -> Filter {
Filter::new()
@@ -58,7 +58,9 @@ pub fn statuses_for(roots: impl IntoIterator<Item = EventId>) -> Filter {
/// Cover notes and NIP-32 label events referencing any of the given root events.
/// These are kinds 1624 and 1985, matched via the `#e` tag.
///
/// Because they carry no repository `a` tag, they are fetched by root like comments.
///
/// Batched, like [`statuses_for`].
pub fn annotations_for(roots: impl IntoIterator<Item = EventId>) -> Filter {
Filter::new()
@@ -75,9 +77,7 @@ pub fn grasp_list(public_key: PublicKey) -> Filter {
/// NIP-22 comments, kind `1111`, referencing any of the given root events.
/// The roots are issues, patches and PRs.
/// Comments carry no repository `a` tag, so they are fetched by root reference.
/// NIP-22 names the uppercase `E` tag as the thread root, used by ngit.
/// Some clients, including Signed, use a lowercase `e` tag, so both are matched.
///
/// Returns two filters, since combining `#E` and `#e` would AND the conditions.
pub fn comments_for(roots: impl IntoIterator<Item = EventId>) -> Vec<Filter> {
let roots: Vec<String> = roots.into_iter().map(|id| id.to_hex()).collect();
@@ -102,22 +102,16 @@ pub fn announcements_by(public_key: PublicKey) -> Filter {
}
/// All repository announcements, for global discovery.
/// Unbounded, intended for negentropy sync, which reconciles sets regardless of size.
/// Local queries with this filter are served by LMDB, staying fast as the database grows.
pub fn all_announcements() -> Filter {
Filter::new().kind(Kind::GitRepoAnnouncement)
}
/// How far back deletion requests are fetched and stored.
/// A deletion request can only target events created before it.
/// NIP-34 events are far younger than this window.
/// Older requests can never match anything shown.
/// Bounding the window keeps the kind-5 and kind-62 set from a full sync reconciliation.
/// That set is one of the largest on public relays.
const DELETIONS_LOOKBACK: Duration = Duration::from_secs(3 * 365 * 86_400);
/// `now` minus [`DELETIONS_LOOKBACK`].
/// Quantized to whole days so identical filters hash the same.
///
/// This lets the backend's sync dedup match identical filters.
fn deletions_since() -> Timestamp {
let now = Timestamp::now().as_secs();
@@ -126,6 +120,7 @@ fn deletions_since() -> Timestamp {
/// All deletion-related events within [`DELETIONS_LOOKBACK`].
/// These are NIP-09 kind `5` and NIP-62 kind `62`.
///
/// Deletion requests must be known before any other event is shown.
pub fn deletions() -> Filter {
Filter::new()
@@ -134,7 +129,9 @@ pub fn deletions() -> Filter {
}
/// Deletion events relevant to a single repository.
///
/// Requests authored by the repository owner.
///
/// Requests addressed to the repository coordinate via its `#a` tag.
pub fn deletions_for_repo(addr: &RepoAddr) -> Vec<Filter> {
vec![
+7 -22
View File
@@ -28,7 +28,6 @@ pub struct Announcement {
pub euc: Option<String>,
/// Other recognized maintainers.
pub maintainers: Vec<PublicKey>,
/// Value of a `u` tag, if any.
/// Marks the repository as a subordinate fork of the upstream, per NIP-34.
pub upstream: Option<Upstream>,
/// Hashtags labelling the repository, the `t` tags.
@@ -36,9 +35,6 @@ pub struct Announcement {
}
/// The `u` tag of a fork announcement, per NIP-34.
/// The first value is the upstream coordinate or a git URL.
/// The coordinate form is `30617:<pubkey>:<id>`.
/// The second value is an optional relay hint for the upstream.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Upstream {
/// Raw first value of the `u` tag, a coordinate or git URL.
@@ -52,10 +48,6 @@ pub struct Upstream {
impl Upstream {
/// Parse the `u` tag values.
/// The first value is the upstream coordinate or a git URL.
/// The coordinate form may append `|git-url`.
/// The coordinate is the part before the first `|`.
/// The second value is an optional relay hint.
fn parse(raw: &str, relay_hint: Option<&str>) -> Self {
let coordinate = raw.split('|').next().unwrap_or(raw);
let addr = coordinate
@@ -70,7 +62,6 @@ impl Upstream {
}
/// Text for display.
/// The upstream coordinate for a NIP-34 repository, else the raw `u` value.
pub fn display(&self) -> SharedString {
match &self.addr {
Some(addr) => SharedString::from(addr.to_string()),
@@ -104,12 +95,7 @@ pub fn activity_subject(event: &Event) -> SharedString {
}
/// The patch set of a pull request.
/// The PR references the root patch event, kind `1617`, via its `e` tag.
/// Every patch of the set is chained to the previous one with NIP-10 `e` reply tags.
/// They are returned in series order, oldest first.
/// A PR without an `e` tag falls back to the patch producing its tip commit.
/// The tip commit is the PR's `commit` or `r` tag, per NIP-34.
/// The reply chain is then walked backward to the root.
///
/// Returns an empty list when no patch event can be linked to the PR.
pub fn pull_request_patches<'a>(
pr: &Event,
@@ -160,8 +146,6 @@ pub fn pull_request_patches<'a>(
}
/// The patch content of a pull request.
/// The contents of its patch set, see [`pull_request_patches`], joined in series order.
/// Older PRs that carried the patch inline fall back to their own content.
pub fn pull_request_patch<'a>(pr: &Event, patches: impl IntoIterator<Item = &'a Event>) -> String {
let patches: Vec<&'a Event> = patches.into_iter().collect();
let series = pull_request_patches(pr, patches.iter().copied());
@@ -209,6 +193,7 @@ fn current_commit_of(event: &Event) -> Option<String> {
}
/// Whether `patch` produces `commit`, found via its `commit` or `r` tag.
///
/// It lets clients find existing patches for a specific commit.
fn patch_produces_commit(patch: &Event, commit: &str) -> bool {
patch
@@ -222,6 +207,7 @@ fn patch_produces_commit(patch: &Event, commit: &str) -> bool {
impl Announcement {
/// Parse a kind `30617` event.
///
/// Returns `None` when the kind is wrong or the `d` tag is missing.
pub fn from_event(event: &Event) -> Option<Self> {
if event.kind != Kind::GitRepoAnnouncement {
@@ -289,8 +275,8 @@ impl Announcement {
/// Whether this announcement is a fork of the repository at `base`.
/// Its `u` tag points at `base`, which also covers permanent forks whose EUC diverged.
///
/// Or it shares `base`'s earliest unique commit and is not the base itself.
/// Read-only discovery input, nothing here is published back to nostr.
pub fn is_fork_of(&self, base: &RepoAddr, base_euc: Option<&str>) -> bool {
if self.addr() == *base {
return false;
@@ -308,9 +294,9 @@ impl Announcement {
.unwrap_or(SharedString::from("No description"))
}
/// The effective maintainers of this repository.
/// The announced `maintainers` plus the announcement author.
/// The author asserts themselves as a maintainer of the primary project.
/// The effective maintainers of this repository,
/// the announced `maintainers` plus the announcement author.
///
/// A `u` tag that marks the repository as a subordinate fork excludes them, per NIP-34.
pub fn effective_maintainers(&self) -> Vec<PublicKey> {
let mut maintainers = self.maintainers.clone();
@@ -321,7 +307,6 @@ impl Announcement {
}
/// The `git clone` URLs for this repository, deduplicated.
/// The announced order is preserved, making output deterministic across calls.
pub fn clone_urls(&self) -> Vec<SharedString> {
let mut seen = HashSet::new();
self.clone
+4 -4
View File
@@ -1,9 +1,8 @@
use nostr::prelude::*;
/// Build a kind `30618` repository state event from refs and HEAD.
/// `refs` are `(refname, commit-id)` pairs, e.g. `refs/heads/main`.
/// `head` is the short branch name HEAD points to.
/// It is published as `ref: refs/heads/<branch>`.
/// Build a kind `30618` repository state event from refs and HEAD,
/// it is published as `ref: refs/heads/<branch>`.
///
/// The `d` tag matches the repository id.
pub fn build_state(id: &str, refs: &[(String, String)], head: Option<&str>) -> EventBuilder {
let mut tags: Vec<Tag> = vec![Tag::identifier(id.to_owned())];
@@ -19,6 +18,7 @@ pub fn build_state(id: &str, refs: &[(String, String)], head: Option<&str>) -> E
}
/// Parse a kind `30618` repository state event into refs and HEAD.
///
/// `refs` are `(refname, commit-id)` pairs.
/// `head` is the branch pointed to by the `HEAD` tag, if any.
pub fn parse_state(event: &Event) -> (Vec<(String, String)>, Option<String>) {
+2 -2
View File
@@ -30,8 +30,8 @@ impl RepoStatus {
}
}
/// Whether an event references the given root event via an `e` or `E` tag.
/// NIP-10 and NIP-34 use the lowercase `e` tag.
///
/// NIP-22 comments, kind `1111`, use the uppercase `E` tag for the thread root.
pub fn references_root(event: &Event, root: &EventId) -> bool {
let root = root.to_hex();
@@ -42,7 +42,7 @@ pub fn references_root(event: &Event, root: &EventId) -> bool {
}
/// Resolve the status of a root event per NIP-34.
/// The most recent status event from the root author or a maintainer wins.
///
/// Defaults to [`RepoStatus::Open`].
pub fn resolve_status<'a, I>(
status_events: I,