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
+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(