This commit is contained in:
2026-09-03 16:38:52 +07:00
parent 018395d0c5
commit 212f35d6bb
69 changed files with 2130 additions and 2373 deletions
+3 -4
View File
@@ -1,9 +1,8 @@
use nostr::prelude::*;
/// Address of a NIP-34 repository announcement: `30617:<owner-pubkey>:<repo-id>`.
///
/// The Rust Nostr SDK's [`Coordinate`] already provides parsing, formatting and hashing for this.
/// The alias keeps the repository-specific vocabulary while reusing the SDK type.
/// 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.
pub type RepoAddr = Coordinate;
/// Build the address of a NIP-34 repository announcement.
+24 -21
View File
@@ -1,13 +1,13 @@
use nostr::prelude::*;
/// ngit / GitWorkshop cover-note extension (kind 1624): a markdown note
/// attached to an issue, patch or PR by its author or a repository
/// maintainer. Not part of the NIP-34 draft; read support for interop.
/// 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.
pub const COVER_NOTE_KIND: Kind = Kind::Custom(1624);
/// Whether a kind-1985 label event is a valid annotation of `root`: it
/// references the root via a lowercase `e` tag and was authored by the root
/// author or a maintainer.
/// 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.
fn label_targets_root(event: &Event, root: &Event, maintainers: &[PublicKey]) -> bool {
if event.kind != Kind::Label {
return false;
@@ -22,8 +22,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 and carries 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,10 +32,11 @@ fn has_hashtag_labels(event: &Event) -> bool {
})
}
/// The effective hashtag labels of `root`: the `t` tags on the event itself
/// (self-reported by its author) plus all labels attached via authorized
/// NIP-32 kind-1985 events in the `#t` namespace. Labels are additive — all
/// valid label events contribute (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
@@ -61,10 +62,11 @@ pub fn labels(root: &Event, label_events: &[Event], maintainers: &[PublicKey]) -
labels
}
/// The effective subject/title override of `root`, from authorized kind-1985
/// label events in the `#subject` namespace. Only the latest event wins
/// (tiebreak: lexicographically larger event id, per NIP-01 replaceable
/// semantics). Returns `None` when no valid override exists.
/// 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.
/// Returns `None` when no valid override exists.
pub fn subject_override(
root: &Event,
label_events: &[Event],
@@ -103,8 +105,8 @@ pub fn subject_override(
})
}
/// The 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],
@@ -116,9 +118,10 @@ pub fn labels_and_subject(
)
}
/// The effective cover note of `root`: the latest authorized kind-1624 event
/// (tiebreak: lexicographically larger event id, per NIP-01 replaceable
/// semantics). Returns `None` when no valid cover note exists.
/// 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,
cover_notes: &'a [Event],
+2 -2
View File
@@ -2,10 +2,10 @@ use nostr::prelude::*;
use crate::RepoAddr;
/// Target of a `nostr://` clone URL (NIP-34 "Nostr Clone URL format").
/// Target of a `nostr://` clone URL, as defined by NIP-34.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CloneTarget {
/// `nostr://<naddr1...>` direct repository address.
/// `nostr://<naddr1...>` encodes a direct repository address.
Addr(RepoAddr),
/// `nostr://<npub|nip05>/[relay-hint/]<identifier>`
UserRepo {
+15 -14
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 and its
/// nested replies (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.
@@ -12,8 +12,8 @@ pub struct CommentThread {
pub replies: Vec<CommentThread>,
}
/// The direct parent of a comment (NIP-22 lowercase `e` tag), or `None` for
/// comments without one.
/// The direct parent id of a comment, from its NIP-22 lowercase `e` tag.
/// `None` when no `e` tag is present.
fn comment_parent(event: &Event) -> Option<EventId> {
event
.tags
@@ -23,14 +23,14 @@ fn comment_parent(event: &Event) -> Option<EventId> {
.and_then(|id| EventId::parse(id).ok())
}
/// Group the comments on a root event (issue / patch / PR) into NIP-22
/// threads. A comment whose parent is the root itself starts a thread; other
/// comments nest under their parent comment. Threads and replies are ordered
/// oldest-first. Replies whose parent comment is missing (e.g. not fetched)
/// are placed as top-level threads so they are not dropped.
/// 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.
pub fn comment_threads(root: &Event, comments: &[Event]) -> Vec<CommentThread> {
// Index comments by their parent id. Comments without a parent tag are
// treated as replying to the root event itself.
// Index comments by their parent id.
// Comments without a parent tag reply to the root event itself.
let mut children: HashMap<EventId, Vec<&Event>> = HashMap::new();
for comment in comments {
let parent = comment_parent(comment).unwrap_or(root.id);
@@ -65,8 +65,8 @@ pub fn comment_threads(root: &Event, comments: &[Event]) -> Vec<CommentThread> {
let mut threads = build(root.id, &children, &mut visited);
// Orphan replies: their parent comment is unknown, so they never appear
// in the tree rooted at the root event; surface them as top-level threads.
// Orphan replies have an unknown parent comment, so they never reach the root tree.
// Surface them as top-level threads so they are not dropped.
let mut orphans: Vec<&Event> = comments
.iter()
.filter(|event| !visited.contains(&event.id))
@@ -148,7 +148,8 @@ mod tests {
.expect("signed event");
let a = comment(&keys, Some(&root), "a", 100);
// `missing` is not in the comment set; its reply should still show up.
// `missing` is not in the comment set.
// Its reply should still show up.
let missing = EventBuilder::new(Kind::Comment, "missing")
.finalize(&keys)
.expect("signed event");
+8 -11
View File
@@ -2,11 +2,10 @@ use std::collections::HashSet;
use nostr::prelude::*;
/// NIP-09 deletion requests and NIP-62 vanish requests, used to hide
/// deleted events before they reach the UI.
///
/// Built from the kind-5 / kind-62 events stored in the local database;
/// pass any event through [`Deletions::is_deleted`] before displaying it.
/// 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)>,
@@ -34,8 +33,8 @@ impl Deletions {
.map(|c| (c, event.pubkey, event.created_at)),
);
} else if event.kind == Kind::RequestToVanish {
// Client-side we can't verify which relay the request targeted,
// so any vanish request is honored for the author's events.
// Client-side we can't verify which relay the request targeted.
// Any vanish request is then honored for the author's events.
vanished.push((event.pubkey, event.created_at));
}
}
@@ -48,10 +47,8 @@ impl Deletions {
}
/// Whether the event is covered by a valid deletion or vanish request.
///
/// A request is only valid when its author matches the deleted event's
/// author (NIP-09); addressable events are deleted up to the request's
/// `created_at`.
/// 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
.vanished
+37 -42
View File
@@ -25,7 +25,7 @@ pub fn announcement(addr: &RepoAddr) -> Filter {
.identifier(addr.identifier.clone())
}
/// Latest state event (refs / HEAD) for a repository.
/// Latest state event for a repository, carrying refs and HEAD.
pub fn state(addr: &RepoAddr) -> Filter {
Filter::new()
.kind(Kind::RepoState)
@@ -33,18 +33,18 @@ pub fn state(addr: &RepoAddr) -> Filter {
.identifier(addr.identifier.clone())
}
/// All NIP-34 activity addressed to a repository (`#a` tag): issues, PRs,
/// patches, statuses and comments (kind 1111).
///
/// Note: the `a` tag on status events is optional per NIP-34, so statuses
/// published without it won't be matched here.
/// All NIP-34 activity addressed to a repository via its `#a` tag.
/// Covers issues, PRs, patches, statuses and kind-1111 comments.
/// The `a` tag is optional on status events per NIP-34.
/// Statuses published without it are not matched here.
pub fn activity(addr: &RepoAddr) -> Filter {
Filter::new().kinds(ACTIVITY_KINDS).coordinate(addr)
}
/// Status events (`1630..=1633`) referencing any of the given root events
/// (`#e` tag). Batched: one filter covers all roots, so a negentropy sync
/// reconciles them in a single session instead of one per root.
/// Status events, kinds `1630..=1633`, referencing any of the given root events.
/// 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()
.kinds([
@@ -56,33 +56,29 @@ pub fn statuses_for(roots: impl IntoIterator<Item = EventId>) -> Filter {
.events(roots)
}
/// Cover notes (kind 1624) and NIP-32 label events (kind 1985) referencing
/// any of the given root events (`#e` tag), fetched per root like comments
/// and statuses because they carry no repository `a` tag. Batched, like
/// [`statuses_for`].
/// 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()
.kinds([crate::COVER_NOTE_KIND, Kind::Label])
.events(roots)
}
/// A user's grasp list (kind `10317`).
/// A user's grasp list, kind `10317`.
pub fn grasp_list(public_key: PublicKey) -> Filter {
Filter::new()
.kind(Kind::GitUserGraspList)
.author(public_key)
}
/// NIP-22 comments (kind `1111`) referencing any of the given root events
/// (issues, patches, PRs).
///
/// Comments carry no repository `a` tag, so they must be fetched by their
/// root reference. NIP-22 defines the uppercase `E` tag as the thread root
/// (used by ngit), but some clients (including Signed) use a lowercase `e`
/// tag, so both are matched.
///
/// Returns two filters because `#E` and `#e` conditions would be ANDed if
/// combined into one.
/// 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();
if roots.is_empty() {
@@ -105,42 +101,41 @@ pub fn announcements_by(public_key: PublicKey) -> Filter {
.author(public_key)
}
/// All repository announcements (for global discovery).
///
/// Unbounded: intended for negentropy sync, which reconciles sets
/// efficiently regardless of size. Local database queries with this
/// filter are served by LMDB, so they stay fast as the database grows.
/// 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, and NIP-34
/// events are all far younger than this window, so older requests can never
/// match anything shown. Bounding the window keeps the kind-5/62 set (one of
/// the largest on public relays) from being fully reconciled on every sync.
/// 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 and the backend's sync dedup can match them.
/// `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();
Timestamp::from_secs(now - now % 86_400) - DELETIONS_LOOKBACK
}
/// All deletion-related events (NIP-09 kind `5`, NIP-62 kind `62`) within
/// [`DELETIONS_LOOKBACK`]. Deletion requests must be known before any other
/// event can be shown.
/// 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()
.kinds([Kind::EventDeletion, Kind::RequestToVanish])
.since(deletions_since())
}
/// Deletion events relevant to a single repository: requests authored by
/// the repository owner and requests addressed to the repository
/// coordinate (`#a` tag).
/// 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![
Filter::new()
+79 -78
View File
@@ -5,16 +5,16 @@ use nostr::prelude::*;
use crate::RepoAddr;
/// Parsed NIP-34 repository announcement (plain data, ready for the UI).
/// Parsed NIP-34 repository announcement, plain data ready for the UI.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Announcement {
/// ID of the announcement event itself.
pub event_id: EventId,
/// Repository ID (`d` tag).
/// Repository ID, the `d` tag.
pub id: String,
/// Author of the announcement event.
pub owner: PublicKey,
/// When the announcement was published (for latest-wins resolution).
/// When the announcement was published, used for latest-wins resolution.
pub created_at: Timestamp,
pub name: Option<SharedString>,
pub description: Option<SharedString>,
@@ -24,36 +24,38 @@ pub struct Announcement {
pub clone: Vec<Url>,
/// Relays the repository monitors for patches and issues.
pub relays: Vec<RelayUrl>,
/// Earliest unique commit ID (`r` tag with `euc` marker).
/// Earliest unique commit ID, the `r` tag with `euc` marker.
pub euc: Option<String>,
/// Other recognized maintainers.
pub maintainers: Vec<PublicKey>,
/// Value of a `u` tag, if any: this repository is a subordinate fork of
/// the referenced upstream (NIP-34).
/// 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 (`t` tags).
/// Hashtags labelling the repository, the `t` tags.
pub hashtags: Vec<String>,
}
/// The `u` tag of a fork announcement (NIP-34)
/// the repository this one is a subordinate fork of. The first value is
/// the upstream coordinate (`30617:<pubkey>:<id>`) or a git URL.
/// The second is an optional relay hint for the upstream.
/// 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 (coordinate or git URL).
/// Raw first value of the `u` tag, a coordinate or git URL.
pub raw: String,
/// The upstream `30617:<pubkey>:<id>` coordinate, when the `u` tag
/// references a NIP-34 repository; `None` for the git-URL form.
/// Upstream repository coordinate when the `u` tag names a NIP-34 repository.
/// `None` for the git-URL form.
pub addr: Option<RepoAddr>,
/// Relay hint for the upstream, if the `u` tag carries one.
pub relay_hint: Option<RelayUrl>,
}
impl Upstream {
/// Parse the `u` tag values. The first 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 an optional relay hint.
/// 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
@@ -67,8 +69,8 @@ impl Upstream {
}
}
/// Text for display: the upstream coordinate when it is a NIP-34
/// repository, otherwise the raw `u` value (git-URL form).
/// 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()),
@@ -77,8 +79,8 @@ impl Upstream {
}
}
/// Subject of a NIP-34 issue or pull request event: the `subject` tag,
/// falling back to the first non-empty line of the content.
/// Subject of a NIP-34 issue or pull request event.
/// Taken from the `subject` tag, else the first non-empty line of the content.
pub fn activity_subject(event: &Event) -> SharedString {
let subject = event
.tags
@@ -101,13 +103,13 @@ pub fn activity_subject(event: &Event) -> SharedString {
.unwrap_or(SharedString::from("Untitled"))
}
/// The patch set of a pull request: the root patch event (kind `1617`) the
/// PR references via its `e` tag, plus every patch of the set chained to it
/// with NIP-10 `e` reply tags, in series order (oldest first). When the PR
/// has no `e` tag, falls back to the patch producing the PR's tip commit
/// (its `commit`/`r` tag, per NIP-34) and walks the reply chain backward to
/// the root.
///
/// 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,
@@ -115,17 +117,18 @@ pub fn pull_request_patches<'a>(
) -> Vec<&'a Event> {
let patches: Vec<&'a Event> = patches.into_iter().collect();
// The PR references its root patch via an `e` tag; follow the NIP-10
// reply chain forward from there (each patch of the set replies to the
// previous one). Among several replies (a revision), the newest wins.
// The PR references its root patch via an `e` tag.
// Follow the NIP-10 reply chain forward from there.
// Each patch replies to the previous one, and among several replies the newest wins.
if let Some(root_id) = pr.tags.event_ids().next()
&& let Some(root) = patches.iter().find(|patch| patch.id == root_id)
{
return forward_series(root, &patches);
}
// No `e` tag: the last patch of the set carries the PR's tip commit in
// its `commit`/`r` tag; walk the reply chain backward to the root.
// The PR has no `e` tag.
// The last patch of the set carries the tip commit in its `commit` or `r` tag.
// Walk the reply chain backward to the root.
let Some(tip) = current_commit_of(pr) else {
return Vec::new();
};
@@ -156,10 +159,9 @@ pub fn pull_request_patches<'a>(
series
}
/// The patch content of a pull request: the contents of every patch event of
/// its patch set (see [`pull_request_patches`]) joined in series order,
/// falling back to the PR's own content for older PRs that carried the
/// patch inline.
/// 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());
@@ -173,7 +175,7 @@ pub fn pull_request_patch<'a>(pr: &Event, patches: impl IntoIterator<Item = &'a
.join("\n")
}
/// The chain of patches replying to `root` (NIP-10 `e` tags), oldest first.
/// The chain of patches replying to `root` via NIP-10 `e` tags, oldest first.
fn forward_series<'a>(root: &'a Event, patches: &[&'a Event]) -> Vec<&'a Event> {
let mut series = vec![root];
loop {
@@ -195,7 +197,7 @@ fn forward_series<'a>(root: &'a Event, patches: &[&'a Event]) -> Vec<&'a Event>
series
}
/// The `c` tag of an event (tip of the proposed branch), as hex.
/// The `c` tag of an event, the tip of the proposed branch, as hex.
fn current_commit_of(event: &Event) -> Option<String> {
event
.tags
@@ -206,8 +208,8 @@ fn current_commit_of(event: &Event) -> Option<String> {
})
}
/// Whether `patch` produces `commit` (its `commit` or `r` tag), so clients
/// can find existing patches for a specific commit.
/// 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
.tags
@@ -219,7 +221,8 @@ fn patch_produces_commit(patch: &Event, commit: &str) -> bool {
}
impl Announcement {
/// Parse a kind `30617` event. Returns `None` if the kind is wrong or the `d` tag is missing.
/// 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 {
return None;
@@ -251,8 +254,8 @@ impl Announcement {
_ => {}
}
// The `u` tag is not modelled by the SDK's `Nip34Tag`; parse it
// manually (first wins).
// The SDK's `Nip34Tag` does not model the `u` tag, so parse it manually.
// Only the first `u` tag is used.
if upstream.is_none() && tag.kind() == "u" {
let values = tag.as_slice();
let raw = values.get(1).map(String::as_str).unwrap_or_default();
@@ -284,11 +287,10 @@ impl Announcement {
crate::repo_addr(self.owner, self.id.clone())
}
/// Whether this announcement is a fork of the repository at `base`:
/// its `u` tag points at `base` (also covers permanent forks whose EUC
/// diverged), or it shares `base`'s earliest unique commit (EUC) and is
/// not the base repository itself. Read-only discovery input: nothing
/// here is published back to nostr.
/// 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;
@@ -306,10 +308,10 @@ impl Announcement {
.unwrap_or(SharedString::from("No description"))
}
/// The effective maintainers of this repository: the announced
/// `maintainers` plus the announcement author, who asserts themselves as
/// a maintainer of the primary project unless a `u` tag marks this
/// repository as a subordinate fork (NIP-34).
/// The effective maintainers of this repository.
/// The announced `maintainers` plus the announcement author.
/// The author asserts themselves as a maintainer of the primary project.
/// 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();
if self.upstream.is_none() && !maintainers.contains(&self.owner) {
@@ -318,8 +320,8 @@ impl Announcement {
maintainers
}
/// The `git clone` URLs for this repository, deduplicated while
/// preserving the announced order (deterministic across calls).
/// 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
@@ -464,8 +466,8 @@ mod tests {
let announcement = Announcement::from_event(&event).expect("parses");
let upstream = announcement.upstream.expect("parses the u tag");
// The coordinate part resolves to a repository address; the raw
// value keeps the `|git-url` suffix.
// The coordinate part resolves to a repository address.
// The raw value keeps the `|git-url` suffix.
assert_eq!(
upstream.addr,
Some(crate::repo_addr(
@@ -489,8 +491,8 @@ mod tests {
#[test]
fn parses_git_url_upstream() {
// The `u` tag may reference a non-nostr upstream by git URL only;
// there is no repository address to navigate to.
// The `u` tag may reference a non-nostr upstream by git URL only.
// There is no repository address to navigate to.
let event = announcement_event(&[
&["d", "my-fork"],
&["u", "https://example.com/upstream.git"],
@@ -508,7 +510,7 @@ mod tests {
#[test]
fn is_fork_of_matches_the_u_tag_coordinate() {
// The base repository (announced by the `u`-tag's owner).
// The base repository, announced by the `u` tag's owner.
let base = crate::repo_addr(
PublicKey::from_hex(MAINTAINER_HEX).expect("valid pubkey"),
"upstream",
@@ -516,21 +518,21 @@ mod tests {
let event = announcement_event(&[&["d", "my-fork"], &["u", &base.to_string()]]);
let fork = Announcement::from_event(&event).expect("parses");
// A `u` tag pointing at the base address marks a fork even when
// neither side announces an EUC.
// A `u` tag pointing at the base address marks a fork.
// This holds even when neither side announces an EUC.
assert!(fork.is_fork_of(&base, None));
}
#[test]
fn is_fork_of_matches_a_shared_euc() {
let euc = "aa231c4c6a5777dc89b42207b499891a344add5c";
// The base repo has no `u` tag; it announces the family EUC.
// The base repo has no `u` tag. It announces the family EUC.
let base_event = announcement_event(&[&["d", "upstream"], &["r", euc, "euc"]]);
let base = Announcement::from_event(&base_event).expect("parses");
let base_addr = base.addr();
// A fork (no `u` tag; a pure mirror or cross-hosted clone) shares
// the EUC, so clients of the family can find it.
// A fork with no `u` tag, a pure mirror or cross-hosted clone, shares the EUC.
// Clients of the family can then find it.
let fork_event = announcement_event(&[&["d", "mirror"], &["r", euc, "euc"]]);
let fork = Announcement::from_event(&fork_event).expect("parses");
assert!(fork.is_fork_of(&base_addr, base.euc.as_deref()));
@@ -549,8 +551,8 @@ mod tests {
#[test]
fn is_fork_of_matches_permanent_forks_with_a_diverged_euc() {
// A permanent fork re-announces its EUC (first commit after the
// fork); only the `u` tag still relates it to the base.
// A permanent fork re-announces its EUC, the first commit after the fork.
// Only the `u` tag still relates it to the base.
let base = crate::repo_addr(
PublicKey::from_hex(MAINTAINER_HEX).expect("valid pubkey"),
"upstream",
@@ -573,8 +575,7 @@ mod tests {
let base = Announcement::from_event(&event).expect("parses");
let base_addr = base.addr();
// The base announcement matches its own EUC, but is not a fork of
// itself.
// The base announcement matches its own EUC but is not a fork of itself.
assert!(!base.is_fork_of(&base_addr, base.euc.as_deref()));
}
@@ -585,8 +586,8 @@ mod tests {
let announcement = Announcement::from_event(&event).expect("parses");
let maintainers = announcement.effective_maintainers();
// The owner asserts themselves as a maintainer of the primary
// project (NIP-34), alongside the announced co-maintainers.
// The owner asserts themselves as a maintainer of the primary project, per NIP-34.
// Announced co-maintainers are included too.
assert_eq!(maintainers.len(), 2);
assert!(maintainers.contains(&announcement.owner));
assert!(maintainers.contains(&PublicKey::from_hex(MAINTAINER_HEX).expect("valid pubkey")));
@@ -603,8 +604,8 @@ mod tests {
let announcement = Announcement::from_event(&event).expect("parses");
let maintainers = announcement.effective_maintainers();
// A `u` tag marks the repository as a subordinate fork: the author
// is not a maintainer of the primary project (NIP-34).
// A `u` tag marks the repository as a subordinate fork.
// The author is then not a maintainer of the primary project, per NIP-34.
assert!(!maintainers.contains(&announcement.owner));
assert_eq!(
maintainers,
@@ -632,7 +633,7 @@ mod tests {
#[test]
fn pull_request_patch_falls_back_to_inline_content() {
// Older PRs carried the patch in the content; no linked patch event.
// Older PRs carried the patch in the content and link no patch event.
let pr = pr_event("patch-inline", vec![]);
assert_eq!(pull_request_patch(&pr, [] as [&Event; 0]), "patch-inline");
@@ -659,8 +660,8 @@ mod tests {
#[test]
fn pull_request_patch_joins_the_whole_patch_set() {
// NIP-34: a PR references the root patch; later patches of the set
// reply to the previous one (NIP-10 `e` tags).
// A PR references the root patch, per NIP-34.
// Later patches of the set reply to the previous one via NIP-10 `e` tags.
let root = patch_event("patch-one", vec![], 100);
let second = patch_event("patch-two", vec![Tag::event(root.id)], 200);
let pr = pr_event("description", vec![Tag::event(root.id)]);
@@ -712,8 +713,8 @@ mod tests {
#[test]
fn pull_request_patches_finds_the_set_via_the_tip_commit() {
// PRs without an `e` tag: the last patch of the set carries the tip
// commit in its `r` tag; walk the reply chain backward to the root.
// PRs without an `e` tag fall back to the patch producing the tip commit.
// Walk the reply chain backward to the root.
let root = patch_event("patch-one", vec![], 100);
let tip = "1111111111111111111111111111111111111111";
let last = patch_event(
+6 -7
View File
@@ -1,10 +1,10 @@
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, published as
/// `ref: refs/heads/<branch>`. The `d` tag matches the repository id.
/// `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>`.
/// 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())];
for (name, commit) in refs {
@@ -19,9 +19,8 @@ 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.
/// `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>) {
let mut refs = Vec::new();
let mut head = None;
+6 -6
View File
@@ -1,6 +1,6 @@
use nostr::prelude::*;
/// Status of a root patch, pull request or issue (kinds `1630..=1633`).
/// Status of a root patch, pull request or issue, kinds `1630..=1633`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RepoStatus {
Open,
@@ -30,9 +30,9 @@ impl RepoStatus {
}
}
/// Check whether an event references the given root event via an `e` or `E`
/// tag. NIP-10 / NIP-34 use the lowercase `e` tag; NIP-22 comments (kind
/// `1111`) use the uppercase `E` tag for the root of the thread.
/// 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();
event
@@ -41,8 +41,8 @@ pub fn references_root(event: &Event, root: &EventId) -> bool {
.any(|tag| matches!(tag.kind(), "e" | "E") && tag.content() == Some(root.as_str()))
}
/// Resolve the status of a root event per NIP-34:
/// the most recent status event from the root author or a maintainer wins.
/// 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,