strip redundant doc comments from signed_core and signed_git

This commit is contained in:
2026-10-03 16:31:37 +07:00
parent 6706e328ec
commit a56b0b8881
31 changed files with 265 additions and 1031 deletions
+1 -15
View File
@@ -4,9 +4,6 @@ use std::str::FromStr;
use nostr::prelude::*;
use serde::{Deserialize, Serialize};
/// Address of a NIP-34 repository announcement, `30617:<owner-pubkey>:<repo-id>`.
///
/// Wraps the Rust Nostr SDK's [`Coordinate`], which parses, formats and hashes this.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RepoAddr(Coordinate);
@@ -16,7 +13,6 @@ impl RepoAddr {
Self(Coordinate::new(Kind::GitRepoAnnouncement, owner).identifier(identifier))
}
/// NIP-34 `d` tag identifier for a user-visible repository name.
pub fn identifier_from_name(name: &str) -> String {
name.chars()
.map(|c| {
@@ -45,7 +41,6 @@ impl RepoAddr {
&self.0
}
/// Latest announcement event for this repository.
pub fn announcement_filter(&self) -> Filter {
Filter::new()
.kind(Kind::GitRepoAnnouncement)
@@ -53,7 +48,6 @@ impl RepoAddr {
.identifier(self.identifier())
}
/// Latest state event for this repository, carrying refs and HEAD.
pub fn state_filter(&self) -> Filter {
Filter::new()
.kind(Kind::RepoState)
@@ -61,21 +55,13 @@ impl RepoAddr {
.identifier(self.identifier())
}
/// All NIP-34 activity addressed to this 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.
// Statuses may omit the `a` tag per NIP-34; those are not matched here.
pub fn activity_filter(&self) -> Filter {
Filter::new()
.kinds(crate::filters::ACTIVITY_KINDS)
.coordinate(&self.0)
}
/// Deletion events relevant to this repository.
///
/// Requests authored by the repository owner.
///
/// Requests addressed to the repository coordinate via its `#a` tag.
pub fn deletion_filters(&self) -> Vec<Filter> {
vec![
Filter::new()
+5 -18
View File
@@ -2,25 +2,14 @@ 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.
///
/// 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.
// All versions of the addressable event up to `cutoff` are deleted.
coords: Vec<(Coordinate, PublicKey, Timestamp)>,
/// `(author, cutoff)` from kind-62 vanish requests.
vanished: Vec<(PublicKey, Timestamp)>,
}
impl Deletions {
/// Build the deletion index from raw kind-5 and kind-62 events.
pub fn from_events(events: impl IntoIterator<Item = Event>) -> Self {
let mut ids = HashSet::new();
let mut coords = Vec::new();
@@ -36,8 +25,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.
// Any vanish request is then honored for the author's events.
// We can't verify which relay the request targeted, so any
// vanish request is honored for the author's events.
vanished.push((event.pubkey, event.created_at));
}
}
@@ -49,10 +38,8 @@ 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`.
// 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
+6 -33
View File
@@ -2,7 +2,6 @@ use std::time::Duration;
use nostr::prelude::*;
/// Kinds that make up the activity of a repository.
pub const ACTIVITY_KINDS: [Kind; 9] = [
Kind::Comment,
Kind::GitPatch,
@@ -15,7 +14,6 @@ pub const ACTIVITY_KINDS: [Kind; 9] = [
Kind::GitStatusDraft,
];
/// Kinds that notify a user when they tag them via their `p` tag.
const NOTIFICATION_KINDS: [Kind; 8] = [
Kind::GitIssue,
Kind::GitPullRequest,
@@ -27,7 +25,6 @@ const NOTIFICATION_KINDS: [Kind; 8] = [
Kind::GitStatusDraft,
];
/// Git root kinds that make a comment count as git activity.
const GIT_ROOT_KINDS: [Kind; 4] = [
Kind::GitIssue,
Kind::GitPatch,
@@ -35,7 +32,6 @@ const GIT_ROOT_KINDS: [Kind; 4] = [
Kind::GitRepoAnnouncement,
];
/// Whether `kind` carries repository data: announcements, states, activity and deletions.
pub fn is_repo_kind(kind: Kind) -> bool {
kind == Kind::GitRepoAnnouncement
|| kind == Kind::RepoState
@@ -44,7 +40,6 @@ pub fn is_repo_kind(kind: Kind) -> bool {
|| ACTIVITY_KINDS.contains(&kind)
}
/// Value of the first tag named `name` on `event`.
fn tag_value<'a>(event: &'a Event, name: &str) -> Option<&'a str> {
event
.tags
@@ -53,19 +48,15 @@ fn tag_value<'a>(event: &'a Event, name: &str) -> Option<&'a str> {
.and_then(|tag| tag.content())
}
/// Kind named by the first tag `name` on `event`.
fn tag_kind(event: &Event, name: &str) -> Option<Kind> {
tag_value(event, name)?.parse::<Kind>().ok()
}
/// Namespace for the repository-agnostic filter constructors.
pub struct Filters;
impl Filters {
/// How far back deletion requests are fetched and stored.
const DELETIONS_LOOKBACK: Duration = Duration::from_secs(3 * 365 * 86_400);
/// Status events, kinds `1630..=1633`, referencing any of the given root events.
pub fn statuses_for(roots: impl IntoIterator<Item = EventId>) -> Filter {
Filter::new()
.kinds([
@@ -77,17 +68,13 @@ impl Filters {
.events(roots)
}
/// 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.
/// The roots are issues, patches and PRs.
///
/// Returns two filters, since combining `#E` and `#e` would AND the conditions.
// Two filters: 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() {
@@ -103,7 +90,6 @@ impl Filters {
]
}
/// NIP-22 comments on our issues, patches and pull requests.
fn notification_comments(me: PublicKey) -> Filter {
Filter::new()
.kind(Kind::Comment)
@@ -111,8 +97,7 @@ impl Filters {
.custom_tags(SingleLetterTag::UPPERCASE_K, ["1621", "1617", "1618"])
}
/// Activity directed at us: comments on our roots, and git events tagging us
/// via their lowercase `p` tag. `Filter::pubkey` sets that `p` tag.
// `Filter::pubkey` matches the git events' lowercase `p` tag.
pub fn notifications(me: PublicKey) -> Vec<Filter> {
vec![
Self::notification_comments(me),
@@ -120,37 +105,27 @@ impl Filters {
]
}
/// Git activity authored by `me`, for "Continue where you left off".
///
/// A comment on an unrelated kind matches too, so results must be filtered
/// through `GitEvent::is_git_activity` before display.
// A comment on an unrelated kind matches too, so results must be filtered
// through `GitEvent::is_git_activity` before display.
pub fn authored_activity(me: PublicKey) -> Filter {
Filter::new().kinds(ACTIVITY_KINDS).author(me)
}
/// All repository announcements, for global discovery.
pub fn all_announcements() -> Filter {
Filter::new().kind(Kind::GitRepoAnnouncement)
}
/// All repository state events, carrying each repository's refs and last push time.
pub fn all_states() -> Filter {
Filter::new().kind(Kind::RepoState)
}
/// `now` minus [`Self::DELETIONS_LOOKBACK`].
/// Quantized to whole days so identical filters hash the same.
///
/// This lets the backend's sync dedup match identical filters.
// Quantized to whole days so identical filters hash the same.
fn deletions_since() -> Timestamp {
let now = Timestamp::now().as_secs();
Timestamp::from_secs(now - now % 86_400) - Self::DELETIONS_LOOKBACK
}
/// All deletion-related events within [`Self::DELETIONS_LOOKBACK`].
/// These are NIP-09 kind `5` and NIP-62 kind `62`.
///
/// Deletion requests must be known before any other event is shown.
// Deletion requests must be known before any other event is shown.
pub fn deletions() -> Filter {
Filter::new()
.kinds([Kind::EventDeletion, Kind::RequestToVanish])
@@ -158,13 +133,11 @@ impl Filters {
}
}
/// Whether a kind-1111 comment targets a git root, checked via its `K` tag.
pub(crate) fn is_git_comment(event: &Event) -> bool {
event.kind == Kind::Comment
&& tag_kind(event, "K").is_some_and(|kind| GIT_ROOT_KINDS.contains(&kind))
}
/// Whether a status event references a git root, checked via its `k` tag.
pub(crate) fn is_git_status(event: &Event) -> bool {
tag_kind(event, "k").is_some_and(|kind| GIT_ROOT_KINDS.contains(&kind))
}
+14 -45
View File
@@ -6,33 +6,21 @@ use serde::{Deserialize, Serialize};
use crate::{GitEvent, RepoAddr};
/// Window before `now` that an advanced cutoff retreats to.
const ADVANCE_WINDOW: Duration = Duration::from_secs(3 * 24 * 60 * 60);
/// Window before `now` that a mark-all cutoff retreats to.
const MARK_ALL_WINDOW: Duration = Duration::from_secs(10 * 24 * 60 * 60);
/// A thread of notification and own-activity events sharing one root.
#[derive(Debug, Clone)]
pub struct InboxItem {
/// The root issue, patch or pull request the events belong to.
pub root: EventId,
/// The root event itself, when it is known locally.
pub root_event: Option<Event>,
/// Repository the root belongs to, from the root's `a` tag.
pub address: Option<RepoAddr>,
/// Notification events directed at the user, newest first.
pub events: Vec<Event>,
/// The user's own events in the thread, newest first.
pub own_events: Vec<Event>,
/// Unread event ids, oldest first.
pub unread_ids: Vec<EventId>,
/// Whether every notification event in the thread is archived.
pub archived: bool,
}
impl InboxItem {
/// Title of the thread, read from its root issue/patch/PR when known.
pub fn title(&self) -> String {
self.root_event
.as_ref()
@@ -50,7 +38,6 @@ impl InboxItem {
.map(|event| event.kind)
}
/// Timestamp of the newest event in the thread.
pub fn latest_activity(&self) -> Timestamp {
self.root_event
.as_ref()
@@ -62,7 +49,6 @@ impl InboxItem {
.unwrap_or_default()
}
/// Up to `limit` events of the thread, oldest first.
pub fn timeline(&self, limit: usize) -> Vec<Event> {
let mut seen: HashSet<EventId> = HashSet::new();
let mut events: Vec<Event> = Vec::new();
@@ -92,7 +78,6 @@ impl InboxItem {
events
}
/// Whether the thread has an unread event still visible in the inbox.
pub fn is_unread(&self) -> bool {
!self.archived && !self.unread_ids.is_empty()
}
@@ -112,7 +97,6 @@ impl InboxItem {
}
}
/// Resolves thread roots by following parent pointers through known events.
pub struct ThreadResolver<'a, L: ?Sized> {
lookup: &'a L,
}
@@ -125,16 +109,14 @@ where
Self { lookup }
}
/// Root issue, patch or pull request of a notification event.
///
/// Returns `None` when the event is not git-related, or when its root is a
/// coordinate rather than an event.
///
/// - issue (1621) / PR (1618): itself
/// - patch (1617): its `e` parent patch, else itself
/// - NIP-22 comment (1111): uppercase `E` root pointer
/// - PR update (1619): uppercase `E`
/// - statuses (1630-1633): NIP-10 root `e`
// Kind → root mapping:
// - issue (1621) / PR (1618): itself
// - patch (1617): its `e` parent patch, else itself
// - NIP-22 comment (1111): uppercase `E` root pointer
// - PR update (1619): uppercase `E`
// - statuses (1630-1633): NIP-10 root `e`
// Returns `None` when the event is not git-related, or when its root is a
// coordinate rather than an event.
pub fn notification_root(&self, event: &Event) -> Option<EventId> {
match event.kind {
Kind::GitIssue | Kind::GitPullRequest => Some(event.id),
@@ -159,7 +141,7 @@ where
}
}
/// Follow NIP-10/NIP-22 parent pointers until a root item is reached.
// Follow NIP-10/NIP-22 parent pointers until a root item is reached.
pub fn resolve_thread_root(&self, id: EventId) -> EventId {
let mut seen = HashSet::new();
let mut root = id;
@@ -184,7 +166,7 @@ where
}
}
/// Parent of a thread event, mirroring gitworkshop's `getParentId`.
// Mirrors gitworkshop's `getParentId`.
fn parent_id(&self, event: &Event) -> Option<EventId> {
for marker in ["reply", "root"] {
if let Some(id) = event
@@ -217,7 +199,6 @@ where
self.first_uppercase_e_id(event)
}
/// NIP-10 root of an event: the `e` tag marked `root`, else the first `e` tag.
fn nip10_root_id(&self, event: &Event) -> Option<EventId> {
event
.tags
@@ -226,12 +207,10 @@ where
.or_else(|| self.first_e_id(event))
}
/// First `e` tag id, in document order.
fn first_e_id(&self, event: &Event) -> Option<EventId> {
self.first_tag_id(event, "e")
}
/// First uppercase `E` tag id, in document order.
fn first_uppercase_e_id(&self, event: &Event) -> Option<EventId> {
self.first_tag_id(event, "E")
}
@@ -246,7 +225,6 @@ where
})
}
/// Event id from a four-element `e` tag carrying `marker`.
fn e_tag_with_marker(&self, tag: &Tag, marker: &str) -> Option<EventId> {
let slice = tag.as_slice();
if tag.kind() != "e" || slice.len() != 4 || slice[3] != marker {
@@ -257,7 +235,6 @@ where
}
}
/// Group notification events and the user's own events into one item per thread.
pub fn group<E, O, L>(
events: E,
own: O,
@@ -328,7 +305,8 @@ where
items
}
/// Read and archive state of the inbox, a high-water-mark model.
// High-water-mark model: events at or before the cutoff
// are covered without an entry in the id set.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct InboxReadState {
#[serde(default)]
@@ -342,31 +320,26 @@ pub struct InboxReadState {
}
impl InboxReadState {
/// Whether `event` is at or before the read cutoff, or marked read.
pub fn is_read(&self, event: &Event) -> bool {
event.created_at <= self.read_before || self.read_ids.contains(&event.id)
}
/// Whether `event` is at or before the archived cutoff, or marked archived.
pub fn is_archived(&self, event: &Event) -> bool {
event.created_at <= self.archived_before || self.archived_ids.contains(&event.id)
}
/// Mark one event read. Events at or before the cutoff are already read.
pub fn mark_read(&mut self, event: &Event) {
if event.created_at > self.read_before {
self.read_ids.insert(event.id);
}
}
/// Mark one event archived. Events at or before the cutoff are already archived.
pub fn mark_archived(&mut self, event: &Event) {
if event.created_at > self.archived_before {
self.archived_ids.insert(event.id);
}
}
/// Mark every non-self event read, anchoring the cutoff ten days back.
pub fn mark_all_read(&mut self, all: &[Event], me: PublicKey, now: Timestamp) {
let cutoff = now - MARK_ALL_WINDOW;
self.read_before = cutoff;
@@ -377,15 +350,14 @@ impl InboxReadState {
.collect();
}
/// Advance the read cutoff to the newest point that keeps unread events
/// unread, then prune the id set.
// Advance the cutoff to the newest point that keeps unread events unread,
// then prune the id set.
pub fn advance_read(&mut self, all: &[Event], me: PublicKey, now: Timestamp) {
let cutoff = advance_cutoff(all, me, now, self.read_before, |event| self.is_read(event));
self.read_before = cutoff;
prune_ids(&mut self.read_ids, all, cutoff);
}
/// Advance the archived cutoff, mirroring [`Self::advance_read`].
pub fn advance_archived(&mut self, all: &[Event], me: PublicKey, now: Timestamp) {
let cutoff = advance_cutoff(all, me, now, self.archived_before, |event| {
self.is_archived(event)
@@ -395,7 +367,6 @@ impl InboxReadState {
}
}
/// Newest cutoff that keeps unread events unread, never earlier than `current`.
fn advance_cutoff<M>(
all: &[Event],
me: PublicKey,
@@ -422,7 +393,6 @@ where
candidate.max(current)
}
/// Drop ids whose event is unknown or now covered by the cutoff.
fn prune_ids(ids: &mut HashSet<EventId>, all: &[Event], cutoff: Timestamp) {
let created_at: HashMap<EventId, Timestamp> = all
.iter()
@@ -578,7 +548,6 @@ mod tests {
assert_eq!(items[0].kind(), Some(Kind::GitIssue));
assert_eq!(items[0].title(), "Add retry logic");
assert_eq!(items[0].events, vec![reply.clone()]);
// The own events are kept apart from the notifications, newest first.
assert_eq!(items[0].own_events, vec![mine.clone(), issue.clone()]);
assert_eq!(
items[0]
+12 -83
View File
@@ -4,42 +4,28 @@ use nostr::prelude::*;
use crate::RepoAddr;
/// 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, the `d` tag.
pub id: String,
/// Author of the announcement event.
pub owner: PublicKey,
/// When the announcement was published, used for latest-wins resolution.
pub created_at: Timestamp,
pub name: Option<String>,
pub description: Option<String>,
/// Webpage URLs for browsing.
pub web: Vec<Url>,
/// URLs for `git clone`.
pub clone: Vec<Url>,
/// Relays the repository monitors for patches and issues.
pub relays: Vec<RelayUrl>,
/// Earliest unique commit ID, the `r` tag with `euc` marker.
pub euc: Option<String>,
/// Other recognized maintainers.
pub maintainers: Vec<PublicKey>,
/// Marks the repository as a subordinate fork of the upstream, per NIP-34.
pub upstream: Option<Upstream>,
/// Hashtags labelling the repository, the `t` tags.
pub hashtags: Vec<String>,
}
/// The `u` tag of a fork announcement, per NIP-34.
// The `u` tag of a fork announcement, per NIP-34.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Upstream {
/// Raw first value of the `u` tag, a coordinate or git URL.
pub raw: String,
/// Upstream repository coordinate when the `u` tag names a NIP-34 repository.
/// `None` for the git-URL form.
// `None` for the git-URL form.
pub addr: Option<RepoAddr>,
}
@@ -65,27 +51,20 @@ impl Upstream {
}
}
/// Tag accessors for NIP-34 git collaboration events.
pub trait GitEvent {
/// Subject of an issue or pull request event.
fn activity_subject(&self) -> String;
/// The `c` tag, the tip of the proposed branch, as hex.
fn current_commit(&self) -> Option<String>;
/// The `merge-base` tag, the base commit a pull request diffs against.
fn merge_base(&self) -> Option<String>;
/// The `clone` tag, URLs the tip commit can be fetched from.
fn clone_urls(&self) -> Option<Vec<Url>>;
/// The `branch-name` tag, the proposed branch's name.
fn branch_name(&self) -> Option<String>;
/// Whether the event is git activity worth showing in the activity list.
fn is_git_activity(&self) -> bool;
/// Whether the event carries an `e`/`E` tag pointing at `root`.
// Matches both NIP-10 lowercase `e` and NIP-22 uppercase `E` root pointers.
fn references_root(&self, root: &EventId) -> bool;
}
@@ -196,7 +175,6 @@ impl<T: GitEvent + ?Sized> GitEvent for &T {
}
}
/// A NIP-34 pull request root event, with its patch set and update history.
pub struct PullRequest<'a>(pub &'a Event);
impl<'a> PullRequest<'a> {
@@ -204,22 +182,17 @@ impl<'a> PullRequest<'a> {
Self(event)
}
/// The patch set of the pull request.
///
/// Returns an empty list when no patch event can be linked to the PR.
// The PR references its root patch via an `e` tag.
pub fn patches(&self, patches: impl IntoIterator<Item = &'a Event>) -> Vec<&'a Event> {
let pr = self.0;
let patches: Vec<&'a Event> = patches.into_iter().collect();
// The PR references its root patch via an `e` tag.
if let Some(root_id) = pr.tags.event_ids().next()
&& let Some(root) = patches.iter().find(|patch| patch.id == root_id)
{
return Self::forward_series(root, &patches);
}
// The PR has no `e` tag.
// The last patch of the set carries the tip commit in its `commit` or `r` tag.
let Some(tip) = pr.current_commit() else {
return Vec::new();
};
@@ -250,9 +223,7 @@ impl<'a> PullRequest<'a> {
series
}
/// The patch set of the pull request joined into one diff text.
///
/// Falls back to the root event's content when no patch set is found.
// Falls back to the root event's content when no patch set is found.
pub fn patch(&self, patches: impl IntoIterator<Item = &'a Event>) -> String {
let pr = self.0;
let patches: Vec<&'a Event> = patches.into_iter().collect();
@@ -267,10 +238,8 @@ impl<'a> PullRequest<'a> {
.join("\n")
}
/// The newest `GitPullRequestUpdate` revising `root`, from the root's own author.
///
/// A pull request's tip is only mutable by its author per NIP-34, updates
/// from anyone else are ignored even if they are newer.
// A pull request's tip is only mutable by its author per NIP-34,
// updates from anyone else are ignored even if they are newer.
pub fn latest_update(
events: impl Iterator<Item = &'a Event>,
root: &Event,
@@ -287,7 +256,6 @@ impl<'a> PullRequest<'a> {
.max_by_key(|e| e.created_at)
}
/// The chain of patches replying to `root` via NIP-10 `e` tags, oldest first.
fn forward_series(root: &'a Event, patches: &[&'a Event]) -> Vec<&'a Event> {
let mut series = vec![root];
loop {
@@ -309,9 +277,7 @@ impl<'a> PullRequest<'a> {
series
}
/// Whether `patch` produces `commit`, found via its `commit` or `r` tag.
///
/// It lets clients find existing patches for a specific commit.
// Lets clients find existing patches for a specific commit.
fn patch_produces_commit(patch: &Event, commit: &str) -> bool {
patch
.tags
@@ -324,9 +290,7 @@ impl<'a> PullRequest<'a> {
}
impl Announcement {
/// The announced forks of `base` a new pull request compare can be built from.
///
/// The user's own forks are listed first.
// The user's own forks are listed first.
pub fn forks_in<'a>(
announcements: &'a [Announcement],
base: &RepoAddr,
@@ -413,15 +377,11 @@ impl Announcement {
RepoAddr::new(self.owner, self.id.clone())
}
/// The name of the repository, or a default if none is provided.
pub fn name(&self) -> String {
self.name.clone().unwrap_or("Untitled".into())
}
/// 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.
// The `u` tag pointing at `base` also covers permanent forks whose EUC diverged.
pub fn is_fork_of(&self, base: &RepoAddr, base_euc: Option<&str>) -> bool {
if self.addr() == *base {
return false;
@@ -432,17 +392,14 @@ impl Announcement {
base_euc.is_some_and(|euc| self.euc.as_deref() == Some(euc))
}
/// The description of the repository, or a default if none is provided.
pub fn description(&self) -> String {
self.description
.clone()
.unwrap_or("No description".to_string())
}
/// 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.
// A `u` tag marking the repository as a subordinate fork excludes the
// announcement author from the maintainers, 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) {
@@ -451,7 +408,6 @@ impl Announcement {
maintainers
}
/// The `git clone` URLs for this repository, deduplicated.
pub fn clone_urls(&self) -> Vec<String> {
let mut seen = HashSet::new();
self.clone
@@ -545,7 +501,6 @@ mod tests {
let announcement = Announcement::from_event(&event).expect("parses");
// An invalid URL keeps the whole clone tag from being parsed.
assert!(announcement.clone.is_empty());
assert_eq!(
announcement.relays,
@@ -568,8 +523,6 @@ 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.
assert_eq!(
upstream.addr,
Some(RepoAddr::new(
@@ -589,7 +542,6 @@ mod tests {
#[test]
fn is_fork_of_matches_the_u_tag_coordinate() {
// The base repository, announced by the `u` tag's owner.
let base = RepoAddr::new(
PublicKey::from_hex(MAINTAINER_HEX).expect("valid pubkey"),
"upstream",
@@ -597,26 +549,20 @@ 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.
// 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.
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 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()));
// An unrelated repository with a different EUC is not a fork.
let other_event = announcement_event(&[
&["d", "other"],
&["r", "bb231c4c6a5777dc89b42207b499891a344add5c", "euc"],
@@ -624,14 +570,11 @@ mod tests {
let other = Announcement::from_event(&other_event).expect("parses");
assert!(!other.is_fork_of(&base_addr, base.euc.as_deref()));
// Without a base EUC there is nothing to compare against.
assert!(!fork.is_fork_of(&base_addr, None));
}
#[test]
fn is_fork_of_matches_permanent_forks_with_a_diverged_euc() {
// 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 = RepoAddr::new(
PublicKey::from_hex(MAINTAINER_HEX).expect("valid pubkey"),
"upstream",
@@ -654,8 +597,6 @@ 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, 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")));
@@ -672,8 +613,6 @@ 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 then not a maintainer of the primary project, per NIP-34.
assert!(!maintainers.contains(&announcement.owner));
assert_eq!(
maintainers,
@@ -698,8 +637,6 @@ mod tests {
#[test]
fn pull_request_patch_joins_the_whole_patch_set() {
// 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)]);
@@ -733,8 +670,6 @@ mod tests {
#[test]
fn pull_request_patches_finds_the_set_via_the_tip_commit() {
// 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(
@@ -794,7 +729,6 @@ mod tests {
created_at,
)
};
// An update revising a different PR must be ignored even though it is newer.
let unrelated = signed_at(
Kind::GitPullRequestUpdate,
vec![Tag::parse(["E", OTHER_ROOT_HEX]).expect("valid tag")],
@@ -822,8 +756,6 @@ mod tests {
.finalize(&other)
.expect("signed event");
// The tip of a PR is only mutable by its author.
// A newer update from anyone else must not win.
assert!(PullRequest::latest_update([&stranger, &root].into_iter(), &root).is_none());
}
@@ -861,8 +793,6 @@ mod tests {
PublicKey::from_hex(OWNER_KEYS[0]).expect("pubkey"),
"upstream",
);
// Newest first, as RepoListStore keeps them.
// Unrelated repo, the user's fork with the shared EUC, another fork with a `u` tag.
let all = vec![
owned_announcements(
2,
@@ -936,7 +866,6 @@ mod tests {
assert_eq!(forks.len(), 1);
assert_eq!(forks[0].id, "mirror");
// Without a base EUC only `u`-tag forks match.
all.push(
owned_announcements(
2,
+1 -8
View File
@@ -1,19 +1,13 @@
use nostr::prelude::*;
/// Refs and HEAD parsed from a kind `30618` repository state event.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepoState {
/// `(refname, commit-id)` pairs.
pub refs: Vec<(String, String)>,
/// Branch pointed to by the `HEAD` tag, if any.
pub head: Option<String>,
}
impl RepoState {
/// 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.
// The `d` tag matches the repository id.
pub fn build(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 {
@@ -27,7 +21,6 @@ impl RepoState {
EventBuilder::new(Kind::RepoState, "").tags(tags)
}
/// Parse a kind `30618` repository state event into refs and HEAD.
pub fn parse(event: &Event) -> Self {
let mut refs = Vec::new();
let mut head = None;
+3 -7
View File
@@ -1,6 +1,5 @@
use nostr::prelude::*;
/// Status of a root patch, pull request or issue, kinds `1630..=1633`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RepoStatus {
Open,
@@ -29,9 +28,7 @@ impl RepoStatus {
}
}
/// Resolve the status of a root event per NIP-34.
///
/// Defaults to [`RepoStatus::Open`].
// Defaults to Open when no authorized status event exists.
pub fn resolve<'a, I>(
status_events: I,
root_author: &PublicKey,
@@ -50,9 +47,8 @@ impl RepoStatus {
}
}
/// NIP-10 and NIP-34 use the lowercase `e` tag.
///
/// NIP-22 comments, kind `1111`, use the uppercase `E` tag for the thread root.
// 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