upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-03-05 14:19:49 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-03-05 14:23:07 +0000
commit37244449d6d0d58bb639f181bd15092de1acaaee (patch)
tree7de03867a1a9578e32fdbdbb2be63e863cea57a4 /src/lib
parent609f3c3db02d437222e2c8e171189179d06c3e9c (diff)
feat(cover-note): add kind-1624 cover notes for PRs, patches, and issues
Implements experimental kind-1624 cover note events: - KIND_COVER_NOTE constant and process_cover_note() in git_events.rs; replaceable semantics (latest created_at, hex-id tiebreak), author or maintainer only - kind-1624 events fetched alongside labels in the fetch pipeline; cover_notes count added to FetchReport display - ngit pr/issue view: cover note displayed in place of description with a clear 'Cover Note:' header; maintainer-authored notes identify the author; original description shown only with --comments; cover_note object included in --json output - ngit pr set-cover-note / ngit issue set-cover-note: publish a kind-1624 event; nostr: mentions in --body converted to q/p tags via tags_from_content (same rules as issue --body) - Fix pre-existing clippy::too_many_lines on repo/mod.rs show_info
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/client.rs34
-rw-r--r--src/lib/git_events.rs67
2 files changed, 89 insertions, 12 deletions
diff --git a/src/lib/client.rs b/src/lib/client.rs
index 94a173f..d5597fa 100644
--- a/src/lib/client.rs
+++ b/src/lib/client.rs
@@ -56,7 +56,7 @@ use crate::{
56 get_dirs, 56 get_dirs,
57 git::{Repo, RepoActions, get_git_config_item}, 57 git::{Repo, RepoActions, get_git_config_item},
58 git_events::{ 58 git_events::{
59 KIND_COMMENT, KIND_LABEL, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, 59 KIND_COMMENT, KIND_COVER_NOTE, KIND_LABEL, KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE,
60 KIND_USER_GRASP_LIST, event_is_cover_letter, event_is_patch_set_root, 60 KIND_USER_GRASP_LIST, event_is_cover_letter, event_is_patch_set_root,
61 event_is_revision_root, event_is_valid_pr_or_pr_update, status_kinds, 61 event_is_revision_root, event_is_valid_pr_or_pr_update, status_kinds,
62 }, 62 },
@@ -2000,6 +2000,8 @@ async fn process_fetched_events(
2000 report.comments.insert(event.id); 2000 report.comments.insert(event.id);
2001 } else if event.kind.eq(&KIND_LABEL) { 2001 } else if event.kind.eq(&KIND_LABEL) {
2002 report.labels.insert(event.id); 2002 report.labels.insert(event.id);
2003 } else if event.kind.eq(&KIND_COVER_NOTE) {
2004 report.cover_notes.insert(event.id);
2003 } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind) 2005 } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind)
2004 { 2006 {
2005 if request.missing_contributor_profiles.contains(&event.pubkey) { 2007 if request.missing_contributor_profiles.contains(&event.pubkey) {
@@ -2126,6 +2128,9 @@ pub fn consolidate_fetch_reports(reports: Vec<Result<FetchReport>>) -> FetchRepo
2126 for c in relay_report.labels { 2128 for c in relay_report.labels {
2127 report.labels.insert(c); 2129 report.labels.insert(c);
2128 } 2130 }
2131 for c in relay_report.cover_notes {
2132 report.cover_notes.insert(c);
2133 }
2129 report.deletions += relay_report.deletions; 2134 report.deletions += relay_report.deletions;
2130 for c in relay_report.contributor_profiles { 2135 for c in relay_report.contributor_profiles {
2131 report.contributor_profiles.insert(c); 2136 report.contributor_profiles.insert(c);
@@ -2268,6 +2273,24 @@ pub fn get_fetch_filters(
2268 ] 2273 ]
2269 } 2274 }
2270 }, 2275 },
2276 // Fetch kind-1624 cover note events for issues and proposals.
2277 // Cover notes reference the target via a lowercase `e` tag.
2278 {
2279 let all_root_ids: HashSet<EventId> = issue_ids
2280 .iter()
2281 .chain(proposal_ids.iter())
2282 .copied()
2283 .collect();
2284 if all_root_ids.is_empty() {
2285 vec![]
2286 } else {
2287 vec![
2288 nostr::Filter::default()
2289 .events(all_root_ids)
2290 .kind(KIND_COVER_NOTE),
2291 ]
2292 }
2293 },
2271 // Request kind-5 deletions for state events and repo announcements by 2294 // Request kind-5 deletions for state events and repo announcements by
2272 // their event ID (#e tag), as per NIP-09. The #a-tagged filter above 2295 // their event ID (#e tag), as per NIP-09. The #a-tagged filter above
2273 // covers addressable-event deletions; this covers the specific event IDs 2296 // covers addressable-event deletions; this covers the specific event IDs
@@ -2358,6 +2381,8 @@ pub struct FetchReport {
2358 comments: HashSet<EventId>, 2381 comments: HashSet<EventId>,
2359 /// NIP-32 kind-1985 label events for issues and proposals. 2382 /// NIP-32 kind-1985 label events for issues and proposals.
2360 labels: HashSet<EventId>, 2383 labels: HashSet<EventId>,
2384 /// Kind-1624 cover note events for issues, patches, and PRs.
2385 cover_notes: HashSet<EventId>,
2361 /// Count of kind-5 deletion events received (for display purposes). 2386 /// Count of kind-5 deletion events received (for display purposes).
2362 deletions: u32, 2387 deletions: u32,
2363 contributor_profiles: HashSet<PublicKey>, 2388 contributor_profiles: HashSet<PublicKey>,
@@ -2453,6 +2478,13 @@ impl Display for FetchReport {
2453 if self.labels.len() > 1 { "s" } else { "" }, 2478 if self.labels.len() > 1 { "s" } else { "" },
2454 )); 2479 ));
2455 } 2480 }
2481 if !self.cover_notes.is_empty() {
2482 display_items.push(format!(
2483 "{} cover note{}",
2484 self.cover_notes.len(),
2485 if self.cover_notes.len() > 1 { "s" } else { "" },
2486 ));
2487 }
2456 if self.deletions > 0 { 2488 if self.deletions > 0 {
2457 display_items.push(format!( 2489 display_items.push(format!(
2458 "{} deletion{}", 2490 "{} deletion{}",
diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs
index b512e44..a5793a5 100644
--- a/src/lib/git_events.rs
+++ b/src/lib/git_events.rs
@@ -94,6 +94,10 @@ pub const KIND_COMMENT: Kind = Kind::Custom(1111);
94/// NIP-32 label event (kind 1985) — applies hashtag labels to an existing 94/// NIP-32 label event (kind 1985) — applies hashtag labels to an existing
95/// event. Used to add labels to issues, patches and PRs after the fact. 95/// event. Used to add labels to issues, patches and PRs after the fact.
96pub const KIND_LABEL: Kind = Kind::Custom(1985); 96pub const KIND_LABEL: Kind = Kind::Custom(1985);
97/// Cover note event (kind 1624) — a markdown note attached to a PR, patch or
98/// issue by its author or a repository maintainer. Only the latest authorised
99/// event is displayed (replaceable semantics with hex-id tiebreak).
100pub const KIND_COVER_NOTE: Kind = Kind::Custom(1624);
97 101
98pub fn event_is_patch_set_root(event: &Event) -> bool { 102pub fn event_is_patch_set_root(event: &Event) -> bool {
99 event.kind.eq(&Kind::GitPatch) 103 event.kind.eq(&Kind::GitPatch)
@@ -985,19 +989,15 @@ pub fn is_event_proposal_root_for_branch(
985/// 989///
986/// 1. `t` tags on the event itself (self-reported by the event author). 990/// 1. `t` tags on the event itself (self-reported by the event author).
987/// 2. NIP-32 kind-1985 label events in `label_events` that reference `event` 991/// 2. NIP-32 kind-1985 label events in `label_events` that reference `event`
988/// via a lowercase `e` tag and carry `["L", "#t"]` + 992/// via a lowercase `e` tag and carry `["L", "#t"]` + `["l", "<value>",
989/// `["l", "<value>", "#t"]` tags. 993/// "#t"]` tags.
990/// 994///
991/// A label is only applied when the author of the source event is either the 995/// A label is only applied when the author of the source event is either the
992/// author of `event` itself or one of the repository maintainers. 996/// author of `event` itself or one of the repository maintainers.
993/// 997///
994/// Labels are additive — all valid label events contribute; there is no 998/// Labels are additive — all valid label events contribute; there is no
995/// "latest wins" replacement semantics. 999/// "latest wins" replacement semantics.
996pub fn process_labels( 1000pub fn process_labels(event: &Event, repo_ref: &RepoRef, label_events: &[Event]) -> Vec<String> {
997 event: &Event,
998 repo_ref: &RepoRef,
999 label_events: &[Event],
1000) -> Vec<String> {
1001 let is_permitted = |pubkey: &PublicKey| -> bool { 1001 let is_permitted = |pubkey: &PublicKey| -> bool {
1002 pubkey.eq(&event.pubkey) || repo_ref.maintainers.contains(pubkey) 1002 pubkey.eq(&event.pubkey) || repo_ref.maintainers.contains(pubkey)
1003 }; 1003 };
@@ -1162,12 +1162,57 @@ pub fn get_labels_and_subject(
1162/// Compatibility wrapper — returns only the hashtag labels. 1162/// Compatibility wrapper — returns only the hashtag labels.
1163/// 1163///
1164/// Prefer [`get_labels_and_subject`] when the subject override is also needed. 1164/// Prefer [`get_labels_and_subject`] when the subject override is also needed.
1165pub fn get_labels( 1165pub fn get_labels(event: &Event, repo_ref: &RepoRef, label_events: &[Event]) -> Vec<String> {
1166 process_labels(event, repo_ref, label_events)
1167}
1168
1169/// The effective cover note for `event`, selected from a pre-fetched set of
1170/// kind-1624 events.
1171///
1172/// A cover note is a markdown body attached to a PR, patch or issue by its
1173/// author or a repository maintainer. Only the latest authorised event wins
1174/// (replaceable semantics: newest `created_at`, tiebreak by lexicographically
1175/// larger event ID). Events authored by other pubkeys are ignored.
1176///
1177/// Returns `None` when no valid cover note exists.
1178pub fn process_cover_note(
1166 event: &Event, 1179 event: &Event,
1167 repo_ref: &RepoRef, 1180 repo_ref: &RepoRef,
1168 label_events: &[Event], 1181 cover_note_events: &[Event],
1169) -> Vec<String> { 1182) -> Option<(Event, bool)> {
1170 process_labels(event, repo_ref, label_events) 1183 let is_permitted = |pubkey: &PublicKey| -> bool {
1184 pubkey.eq(&event.pubkey) || repo_ref.maintainers.contains(pubkey)
1185 };
1186
1187 let event_id_str = event.id.to_string();
1188
1189 // Find the winning cover note: latest created_at, tiebreak by
1190 // lexicographically larger event ID (NIP-1 replaceable event semantics).
1191 let winner = cover_note_events
1192 .iter()
1193 .filter(|cn| {
1194 if !cn.kind.eq(&KIND_COVER_NOTE) {
1195 return false;
1196 }
1197 if !is_permitted(&cn.pubkey) {
1198 return false;
1199 }
1200 // Must reference our event via a lowercase `e` tag.
1201 cn.tags.iter().any(|t| {
1202 let s = t.as_slice();
1203 s.len() >= 2 && s[0].eq("e") && s[1].eq(&event_id_str)
1204 })
1205 })
1206 .max_by(|a, b| {
1207 a.created_at
1208 .cmp(&b.created_at)
1209 .then_with(|| a.id.to_string().cmp(&b.id.to_string()))
1210 })?;
1211
1212 // True when the cover note author differs from the original event author
1213 // (i.e. a maintainer wrote it, not the PR/issue author).
1214 let by_different_author = winner.pubkey != event.pubkey;
1215 Some((winner.clone(), by_different_author))
1171} 1216}
1172 1217
1173pub fn get_status( 1218pub fn get_status(