review
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user