upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/lib/client.rs118
1 files changed, 101 insertions, 17 deletions
diff --git a/src/lib/client.rs b/src/lib/client.rs
index 41e5379..62db8d2 100644
--- a/src/lib/client.rs
+++ b/src/lib/client.rs
@@ -787,6 +787,7 @@ impl Connect for Client {
787 fresh_coordinates.insert(c); 787 fresh_coordinates.insert(c);
788 } 788 }
789 let mut fresh_proposal_roots = request.proposals.clone(); 789 let mut fresh_proposal_roots = request.proposals.clone();
790 let mut fresh_issue_roots = request.issue_ids.clone();
790 let mut fresh_profiles: HashSet<PublicKey> = request 791 let mut fresh_profiles: HashSet<PublicKey> = request
791 .missing_contributor_profiles 792 .missing_contributor_profiles
792 .union( 793 .union(
@@ -819,6 +820,7 @@ impl Connect for Client {
819 let filters = get_fetch_filters( 820 let filters = get_fetch_filters(
820 &fresh_coordinates, 821 &fresh_coordinates,
821 &fresh_proposal_roots, 822 &fresh_proposal_roots,
823 &fresh_issue_roots,
822 &fresh_non_proposal_event_ids, 824 &fresh_non_proposal_event_ids,
823 &fresh_profiles, 825 &fresh_profiles,
824 ); 826 );
@@ -842,6 +844,7 @@ impl Connect for Client {
842 844
843 fresh_coordinates = HashSet::new(); 845 fresh_coordinates = HashSet::new();
844 fresh_proposal_roots = HashSet::new(); 846 fresh_proposal_roots = HashSet::new();
847 fresh_issue_roots = HashSet::new();
845 fresh_profiles = HashSet::new(); 848 fresh_profiles = HashSet::new();
846 849
847 let relay = self.client.relay(&relay_url).await?; 850 let relay = self.client.relay(&relay_url).await?;
@@ -881,6 +884,7 @@ impl Connect for Client {
881 git_repo_path, 884 git_repo_path,
882 &mut fresh_coordinates, 885 &mut fresh_coordinates,
883 &mut fresh_proposal_roots, 886 &mut fresh_proposal_roots,
887 &mut fresh_issue_roots,
884 &mut fresh_profiles, 888 &mut fresh_profiles,
885 &mut report, 889 &mut report,
886 ) 890 )
@@ -888,6 +892,7 @@ impl Connect for Client {
888 892
889 if fresh_coordinates.is_empty() 893 if fresh_coordinates.is_empty()
890 && fresh_proposal_roots.is_empty() 894 && fresh_proposal_roots.is_empty()
895 && fresh_issue_roots.is_empty()
891 && fresh_profiles.is_empty() 896 && fresh_profiles.is_empty()
892 { 897 {
893 break; 898 break;
@@ -1630,6 +1635,7 @@ async fn create_relays_request(
1630 }; 1635 };
1631 1636
1632 let mut proposals: HashSet<EventId> = HashSet::new(); 1637 let mut proposals: HashSet<EventId> = HashSet::new();
1638 let mut issue_ids: HashSet<EventId> = HashSet::new();
1633 let mut missing_contributor_profiles: HashSet<PublicKey> = HashSet::new(); 1639 let mut missing_contributor_profiles: HashSet<PublicKey> = HashSet::new();
1634 let mut contributors: HashSet<PublicKey> = HashSet::new(); 1640 let mut contributors: HashSet<PublicKey> = HashSet::new();
1635 1641
@@ -1645,7 +1651,7 @@ async fn create_relays_request(
1645 git_repo_path, 1651 git_repo_path,
1646 vec![ 1652 vec![
1647 nostr::Filter::default() 1653 nostr::Filter::default()
1648 .kinds(vec![Kind::GitPatch, KIND_PULL_REQUEST]) 1654 .kinds(vec![Kind::GitPatch, KIND_PULL_REQUEST, Kind::GitIssue])
1649 .custom_tags( 1655 .custom_tags(
1650 SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), 1656 SingleLetterTag::lowercase(nostr_sdk::Alphabet::A),
1651 repo_coordinates_without_relays 1657 repo_coordinates_without_relays
@@ -1663,6 +1669,9 @@ async fn create_relays_request(
1663 { 1669 {
1664 proposals.insert(event.id); 1670 proposals.insert(event.id);
1665 contributors.insert(event.pubkey); 1671 contributors.insert(event.pubkey);
1672 } else if event.kind.eq(&Kind::GitIssue) {
1673 issue_ids.insert(event.id);
1674 contributors.insert(event.pubkey);
1666 } 1675 }
1667 } 1676 }
1668 } 1677 }
@@ -1739,6 +1748,7 @@ async fn create_relays_request(
1739 for filter in get_fetch_filters( 1748 for filter in get_fetch_filters(
1740 &repo_coordinates_without_relays, 1749 &repo_coordinates_without_relays,
1741 &proposals, 1750 &proposals,
1751 &issue_ids,
1742 &HashSet::new(), /* non_proposal_event_ids not yet computed; deletion events are not 1752 &HashSet::new(), /* non_proposal_event_ids not yet computed; deletion events are not
1743 * cached locally */ 1753 * cached locally */
1744 &missing_contributor_profiles 1754 &missing_contributor_profiles
@@ -1746,7 +1756,7 @@ async fn create_relays_request(
1746 &profiles_to_fetch_from_user_relays 1756 &profiles_to_fetch_from_user_relays
1747 .clone() 1757 .clone()
1748 .into_keys() 1758 .into_keys()
1749 .collect(), 1759 .collect::<HashSet<PublicKey>>(),
1750 ) 1760 )
1751 .copied() 1761 .copied()
1752 .collect(), 1762 .collect(),
@@ -1858,6 +1868,7 @@ async fn create_relays_request(
1858 ids 1868 ids
1859 }, 1869 },
1860 proposals, 1870 proposals,
1871 issue_ids,
1861 contributors, 1872 contributors,
1862 missing_contributor_profiles, 1873 missing_contributor_profiles,
1863 existing_events, 1874 existing_events,
@@ -1873,6 +1884,7 @@ async fn process_fetched_events(
1873 git_repo_path: Option<&Path>, 1884 git_repo_path: Option<&Path>,
1874 fresh_coordinates: &mut HashSet<Nip19Coordinate>, 1885 fresh_coordinates: &mut HashSet<Nip19Coordinate>,
1875 fresh_proposal_roots: &mut HashSet<EventId>, 1886 fresh_proposal_roots: &mut HashSet<EventId>,
1887 fresh_issue_roots: &mut HashSet<EventId>,
1876 fresh_profiles: &mut HashSet<PublicKey>, 1888 fresh_profiles: &mut HashSet<PublicKey>,
1877 report: &mut FetchReport, 1889 report: &mut FetchReport,
1878) -> Result<()> { 1890) -> Result<()> {
@@ -1976,6 +1988,14 @@ async fn process_fetched_events(
1976 { 1988 {
1977 fresh_profiles.insert(event.pubkey); 1989 fresh_profiles.insert(event.pubkey);
1978 } 1990 }
1991 } else if event.kind.eq(&Kind::GitIssue) {
1992 fresh_issue_roots.insert(event.id);
1993 report.issues.insert(event.id);
1994 if !request.contributors.contains(&event.pubkey)
1995 && !fresh_profiles.contains(&event.pubkey)
1996 {
1997 fresh_profiles.insert(event.pubkey);
1998 }
1979 } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind) 1999 } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind)
1980 { 2000 {
1981 if request.missing_contributor_profiles.contains(&event.pubkey) { 2001 if request.missing_contributor_profiles.contains(&event.pubkey) {
@@ -2001,23 +2021,44 @@ async fn process_fetched_events(
2001 } 2021 }
2002 } 2022 }
2003 for event in &events { 2023 for event in &events {
2004 if !request.existing_events.contains(&event.id) 2024 if !request.existing_events.contains(&event.id) {
2005 && !event.tags.iter().any(|t| { 2025 let tagged_root_id = event.tags.iter().find_map(|t| {
2006 t.as_slice().len() > 1 2026 if t.as_slice().len() > 1
2007 && (t.as_slice()[0].eq("E") || t.as_slice()[0].eq("e")) 2027 && (t.as_slice()[0].eq("E") || t.as_slice()[0].eq("e"))
2008 && if let Ok(id) = EventId::parse(&t.as_slice()[1]) { 2028 {
2009 report.proposals.contains(&id) 2029 EventId::parse(&t.as_slice()[1]).ok()
2030 } else {
2031 None
2032 }
2033 });
2034 if status_kinds().contains(&event.kind) {
2035 // Route status events to the correct counter based on whether
2036 // the root event is a known issue or a proposal (patch/PR).
2037 // Don't double-count statuses that arrived in the same batch
2038 // as their parent (new issues/proposals already inflate the count).
2039 if let Some(root_id) = &tagged_root_id {
2040 if report.issues.contains(root_id) {
2041 // status for a new issue in this batch — skip (counted via issues)
2042 } else if report.proposals.contains(root_id) {
2043 // status for a new proposal in this batch — skip (counted via proposals)
2044 } else if request.issue_ids.contains(root_id) {
2045 report.issue_statuses.insert(event.id);
2010 } else { 2046 } else {
2011 false 2047 report.statuses.insert(event.id);
2012 } 2048 }
2013 }) 2049 }
2014 { 2050 } else {
2015 if (event.kind.eq(&Kind::GitPatch) && !event_is_patch_set_root(event)) 2051 // Non-status events: commits/PR-updates for proposals only.
2016 || event.kind.eq(&KIND_PULL_REQUEST_UPDATE) 2052 let not_tagged_with_new_proposal = tagged_root_id
2017 { 2053 .as_ref()
2018 report.commits.insert(event.id); 2054 .is_none_or(|id| !report.proposals.contains(id));
2019 } else if status_kinds().contains(&event.kind) { 2055 if not_tagged_with_new_proposal {
2020 report.statuses.insert(event.id); 2056 if (event.kind.eq(&Kind::GitPatch) && !event_is_patch_set_root(event))
2057 || event.kind.eq(&KIND_PULL_REQUEST_UPDATE)
2058 {
2059 report.commits.insert(event.id);
2060 }
2061 }
2021 } 2062 }
2022 } 2063 }
2023 } 2064 }
@@ -2070,6 +2111,12 @@ pub fn consolidate_fetch_reports(reports: Vec<Result<FetchReport>>) -> FetchRepo
2070 for c in relay_report.statuses { 2111 for c in relay_report.statuses {
2071 report.statuses.insert(c); 2112 report.statuses.insert(c);
2072 } 2113 }
2114 for c in relay_report.issues {
2115 report.issues.insert(c);
2116 }
2117 for c in relay_report.issue_statuses {
2118 report.issue_statuses.insert(c);
2119 }
2073 report.deletions += relay_report.deletions; 2120 report.deletions += relay_report.deletions;
2074 for c in relay_report.contributor_profiles { 2121 for c in relay_report.contributor_profiles {
2075 report.contributor_profiles.insert(c); 2122 report.contributor_profiles.insert(c);
@@ -2107,6 +2154,7 @@ pub fn consolidate_fetch_reports(reports: Vec<Result<FetchReport>>) -> FetchRepo
2107pub fn get_fetch_filters( 2154pub fn get_fetch_filters(
2108 repo_coordinates: &HashSet<Nip19Coordinate>, 2155 repo_coordinates: &HashSet<Nip19Coordinate>,
2109 proposal_ids: &HashSet<EventId>, 2156 proposal_ids: &HashSet<EventId>,
2157 issue_ids: &HashSet<EventId>,
2110 non_proposal_event_ids: &HashSet<EventId>, 2158 non_proposal_event_ids: &HashSet<EventId>,
2111 required_profiles: &HashSet<PublicKey>, 2159 required_profiles: &HashSet<PublicKey>,
2112) -> Vec<nostr::Filter> { 2160) -> Vec<nostr::Filter> {
@@ -2118,7 +2166,12 @@ pub fn get_fetch_filters(
2118 get_filter_state_events(repo_coordinates, false), 2166 get_filter_state_events(repo_coordinates, false),
2119 get_filter_repo_ann_events(repo_coordinates, false), 2167 get_filter_repo_ann_events(repo_coordinates, false),
2120 nostr::Filter::default() 2168 nostr::Filter::default()
2121 .kinds(vec![Kind::GitPatch, Kind::EventDeletion, KIND_PULL_REQUEST]) 2169 .kinds(vec![
2170 Kind::GitPatch,
2171 Kind::EventDeletion,
2172 KIND_PULL_REQUEST,
2173 Kind::GitIssue,
2174 ])
2122 .custom_tags( 2175 .custom_tags(
2123 SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), 2176 SingleLetterTag::lowercase(nostr_sdk::Alphabet::A),
2124 repo_coordinates 2177 repo_coordinates
@@ -2157,6 +2210,19 @@ pub fn get_fetch_filters(
2157 ), 2210 ),
2158 ] 2211 ]
2159 }, 2212 },
2213 // Fetch status events for known issues.
2214 if issue_ids.is_empty() {
2215 vec![]
2216 } else {
2217 vec![
2218 nostr::Filter::default()
2219 .events(issue_ids.clone())
2220 .kinds(status_kinds()),
2221 nostr::Filter::default()
2222 .custom_tags(SingleLetterTag::uppercase(Alphabet::E), issue_ids.clone())
2223 .kinds(status_kinds()),
2224 ]
2225 },
2160 // Request kind-5 deletions for state events and repo announcements by 2226 // Request kind-5 deletions for state events and repo announcements by
2161 // their event ID (#e tag), as per NIP-09. The #a-tagged filter above 2227 // their event ID (#e tag), as per NIP-09. The #a-tagged filter above
2162 // covers addressable-event deletions; this covers the specific event IDs 2228 // covers addressable-event deletions; this covers the specific event IDs
@@ -2241,6 +2307,8 @@ pub struct FetchReport {
2241 /// commits against existing propoals 2307 /// commits against existing propoals
2242 commits: HashSet<EventId>, 2308 commits: HashSet<EventId>,
2243 statuses: HashSet<EventId>, 2309 statuses: HashSet<EventId>,
2310 issues: HashSet<EventId>,
2311 issue_statuses: HashSet<EventId>,
2244 /// Count of kind-5 deletion events received (for display purposes). 2312 /// Count of kind-5 deletion events received (for display purposes).
2245 deletions: u32, 2313 deletions: u32,
2246 contributor_profiles: HashSet<PublicKey>, 2314 contributor_profiles: HashSet<PublicKey>,
@@ -2304,6 +2372,20 @@ impl Display for FetchReport {
2304 if self.statuses.len() > 1 { "es" } else { "" }, 2372 if self.statuses.len() > 1 { "es" } else { "" },
2305 )); 2373 ));
2306 } 2374 }
2375 if !self.issues.is_empty() {
2376 display_items.push(format!(
2377 "{} issue{}",
2378 self.issues.len(),
2379 if self.issues.len() > 1 { "s" } else { "" },
2380 ));
2381 }
2382 if !self.issue_statuses.is_empty() {
2383 display_items.push(format!(
2384 "{} issue status{}",
2385 self.issue_statuses.len(),
2386 if self.issue_statuses.len() > 1 { "es" } else { "" },
2387 ));
2388 }
2307 if self.deletions > 0 { 2389 if self.deletions > 0 {
2308 display_items.push(format!( 2390 display_items.push(format!(
2309 "{} deletion{}", 2391 "{} deletion{}",
@@ -2345,6 +2427,8 @@ pub struct FetchRequest {
2345 repo_coordinates_without_relays: Vec<(Nip19Coordinate, Option<Timestamp>)>, 2427 repo_coordinates_without_relays: Vec<(Nip19Coordinate, Option<Timestamp>)>,
2346 state: Option<(Timestamp, EventId)>, 2428 state: Option<(Timestamp, EventId)>,
2347 proposals: HashSet<EventId>, 2429 proposals: HashSet<EventId>,
2430 /// Known issue event IDs, used to fetch their status events.
2431 issue_ids: HashSet<EventId>,
2348 /// Event IDs of non-proposal events (state events, repo announcements) for 2432 /// Event IDs of non-proposal events (state events, repo announcements) for
2349 /// which we should also request kind-5 deletion events by `#e` tag. 2433 /// which we should also request kind-5 deletion events by `#e` tag.
2350 non_proposal_event_ids: HashSet<EventId>, 2434 non_proposal_event_ids: HashSet<EventId>,