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:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/client.rs164
-rw-r--r--src/lib/git/mod.rs20
-rw-r--r--src/lib/git_events.rs321
-rw-r--r--src/lib/login/fresh.rs4
-rw-r--r--src/lib/repo_ref.rs23
5 files changed, 441 insertions, 91 deletions
diff --git a/src/lib/client.rs b/src/lib/client.rs
index e808bea..6f28cff 100644
--- a/src/lib/client.rs
+++ b/src/lib/client.rs
@@ -31,6 +31,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressState, P
31use mockall::*; 31use mockall::*;
32use nostr::{ 32use nostr::{
33 Event, 33 Event,
34 event::{TagKind, TagStandard, UnsignedEvent},
34 filter::Alphabet, 35 filter::Alphabet,
35 nips::{nip01::Coordinate, nip19::Nip19Coordinate}, 36 nips::{nip01::Coordinate, nip19::Nip19Coordinate},
36 signer::SignerBackend, 37 signer::SignerBackend,
@@ -47,20 +48,23 @@ use crate::{
47 get_dirs, 48 get_dirs,
48 git::{Repo, RepoActions, get_git_config_item}, 49 git::{Repo, RepoActions, get_git_config_item},
49 git_events::{ 50 git_events::{
50 event_is_cover_letter, event_is_patch_set_root, event_is_revision_root, status_kinds, 51 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_is_cover_letter,
52 event_is_patch_set_root, event_is_revision_root, event_is_valid_pr_or_pr_update,
53 status_kinds,
51 }, 54 },
52 login::{get_likely_logged_in_user, user::get_user_ref_from_cache}, 55 login::{get_likely_logged_in_user, user::get_user_ref_from_cache},
53 repo_ref::RepoRef, 56 repo_ref::{RepoRef, normalize_grasp_server_url},
54 repo_state::RepoState, 57 repo_state::RepoState,
55}; 58};
56 59
57#[allow(clippy::struct_field_names)] 60#[allow(clippy::struct_field_names)]
58pub struct Client { 61pub struct Client {
59 client: nostr_sdk::Client, 62 client: nostr_sdk::Client,
60 fallback_relays: Vec<String>, 63 relay_default_set: Vec<String>,
61 more_fallback_relays: Vec<String>, 64 more_fallback_relays: Vec<String>,
62 blaster_relays: Vec<String>, 65 blaster_relays: Vec<String>,
63 fallback_signer_relays: Vec<String>, 66 fallback_signer_relays: Vec<String>,
67 grasp_default_set: Vec<String>,
64 relays_not_to_retry: Arc<RwLock<HashMap<RelayUrl, String>>>, 68 relays_not_to_retry: Arc<RwLock<HashMap<RelayUrl, String>>>,
65} 69}
66 70
@@ -94,10 +98,11 @@ pub trait Connect {
94 async fn set_signer(&mut self, signer: Arc<dyn NostrSigner>); 98 async fn set_signer(&mut self, signer: Arc<dyn NostrSigner>);
95 async fn connect(&self, relay_url: &RelayUrl) -> Result<()>; 99 async fn connect(&self, relay_url: &RelayUrl) -> Result<()>;
96 async fn disconnect(&self) -> Result<()>; 100 async fn disconnect(&self) -> Result<()>;
97 fn get_fallback_relays(&self) -> &Vec<String>; 101 fn get_relay_default_set(&self) -> &Vec<String>;
98 fn get_more_fallback_relays(&self) -> &Vec<String>; 102 fn get_more_fallback_relays(&self) -> &Vec<String>;
99 fn get_blaster_relays(&self) -> &Vec<String>; 103 fn get_blaster_relays(&self) -> &Vec<String>;
100 fn get_fallback_signer_relays(&self) -> &Vec<String>; 104 fn get_fallback_signer_relays(&self) -> &Vec<String>;
105 fn get_grasp_default_set(&self) -> &Vec<String>;
101 async fn send_event_to<'a>( 106 async fn send_event_to<'a>(
102 &self, 107 &self,
103 git_repo_path: Option<&'a Path>, 108 git_repo_path: Option<&'a Path>,
@@ -147,10 +152,11 @@ impl Connect for Client {
147 .opts(Options::new().relay_limits(RelayLimits::disable())) 152 .opts(Options::new().relay_limits(RelayLimits::disable()))
148 .build() 153 .build()
149 }, 154 },
150 fallback_relays: opts.fallback_relays, 155 relay_default_set: opts.relay_default_set,
151 more_fallback_relays: opts.more_fallback_relays, 156 more_fallback_relays: opts.more_fallback_relays,
152 blaster_relays: opts.blaster_relays, 157 blaster_relays: opts.blaster_relays,
153 fallback_signer_relays: opts.fallback_signer_relays, 158 fallback_signer_relays: opts.fallback_signer_relays,
159 grasp_default_set: opts.grasp_default_set,
154 relays_not_to_retry: Arc::new(RwLock::new(HashMap::new())), 160 relays_not_to_retry: Arc::new(RwLock::new(HashMap::new())),
155 } 161 }
156 } 162 }
@@ -189,8 +195,8 @@ impl Connect for Client {
189 Ok(()) 195 Ok(())
190 } 196 }
191 197
192 fn get_fallback_relays(&self) -> &Vec<String> { 198 fn get_relay_default_set(&self) -> &Vec<String> {
193 &self.fallback_relays 199 &self.relay_default_set
194 } 200 }
195 201
196 fn get_more_fallback_relays(&self) -> &Vec<String> { 202 fn get_more_fallback_relays(&self) -> &Vec<String> {
@@ -205,6 +211,10 @@ impl Connect for Client {
205 &self.fallback_signer_relays 211 &self.fallback_signer_relays
206 } 212 }
207 213
214 fn get_grasp_default_set(&self) -> &Vec<String> {
215 &self.grasp_default_set
216 }
217
208 async fn send_event_to<'a>( 218 async fn send_event_to<'a>(
209 &self, 219 &self,
210 git_repo_path: Option<&'a Path>, 220 git_repo_path: Option<&'a Path>,
@@ -335,8 +345,8 @@ impl Connect for Client {
335 trusted_maintainer_coordinate: Option<&'a Nip19Coordinate>, 345 trusted_maintainer_coordinate: Option<&'a Nip19Coordinate>,
336 user_profiles: &HashSet<PublicKey>, 346 user_profiles: &HashSet<PublicKey>,
337 ) -> Result<(Vec<Result<FetchReport>>, MultiProgress)> { 347 ) -> Result<(Vec<Result<FetchReport>>, MultiProgress)> {
338 let fallback_relays = &self 348 let relay_default_set = &self
339 .fallback_relays 349 .relay_default_set
340 .iter() 350 .iter()
341 .filter_map(|r| RelayUrl::parse(r).ok()) 351 .filter_map(|r| RelayUrl::parse(r).ok())
342 .collect::<HashSet<RelayUrl>>(); 352 .collect::<HashSet<RelayUrl>>();
@@ -345,7 +355,7 @@ impl Connect for Client {
345 git_repo_path, 355 git_repo_path,
346 trusted_maintainer_coordinate, 356 trusted_maintainer_coordinate,
347 user_profiles, 357 user_profiles,
348 fallback_relays.clone(), 358 relay_default_set.clone(),
349 ) 359 )
350 .await?; 360 .await?;
351 361
@@ -685,17 +695,18 @@ async fn get_events_of(
685 695
686pub struct Params { 696pub struct Params {
687 pub keys: Option<nostr::Keys>, 697 pub keys: Option<nostr::Keys>,
688 pub fallback_relays: Vec<String>, 698 pub relay_default_set: Vec<String>,
689 pub more_fallback_relays: Vec<String>, 699 pub more_fallback_relays: Vec<String>,
690 pub blaster_relays: Vec<String>, 700 pub blaster_relays: Vec<String>,
691 pub fallback_signer_relays: Vec<String>, 701 pub fallback_signer_relays: Vec<String>,
702 pub grasp_default_set: Vec<String>,
692} 703}
693 704
694impl Default for Params { 705impl Default for Params {
695 fn default() -> Self { 706 fn default() -> Self {
696 Params { 707 Params {
697 keys: None, 708 keys: None,
698 fallback_relays: if std::env::var("NGITTEST").is_ok() { 709 relay_default_set: if std::env::var("NGITTEST").is_ok() {
699 vec![ 710 vec![
700 "ws://localhost:8051".to_string(), 711 "ws://localhost:8051".to_string(),
701 "ws://localhost:8052".to_string(), 712 "ws://localhost:8052".to_string(),
@@ -731,6 +742,11 @@ impl Default for Params {
731 } else { 742 } else {
732 vec!["wss://relay.nsec.app".to_string()] 743 vec!["wss://relay.nsec.app".to_string()]
733 }, 744 },
745 grasp_default_set: if std::env::var("NGITTEST").is_ok() {
746 vec![]
747 } else {
748 vec!["relay.ngit.dev".to_string(), "gitnostr.com".to_string()]
749 },
734 } 750 }
735 } 751 }
736} 752}
@@ -749,7 +765,7 @@ impl Params {
749 .collect(); 765 .collect();
750 // elsewhere it is assumed this isn't empty 766 // elsewhere it is assumed this isn't empty
751 if !new_default_relays.is_empty() { 767 if !new_default_relays.is_empty() {
752 params.fallback_relays = new_default_relays; 768 params.relay_default_set = new_default_relays;
753 } 769 }
754 } 770 }
755 if let Ok(Some(relay_blasters)) = 771 if let Ok(Some(relay_blasters)) =
@@ -770,6 +786,17 @@ impl Params {
770 .map(|relay_url| relay_url.to_string()) // Convert RelayUrl back to String 786 .map(|relay_url| relay_url.to_string()) // Convert RelayUrl back to String
771 .collect(); 787 .collect();
772 } 788 }
789 if let Ok(Some(grasp_default_servers)) =
790 get_git_config_item(git_repo, "nostr.grasp-default-set")
791 {
792 let new_default_grasp_servers: Vec<String> = grasp_default_servers
793 .split(';')
794 .filter_map(|url| normalize_grasp_server_url(url).ok()) // Attempt to parse and filter out errors
795 .collect();
796 if !new_default_grasp_servers.is_empty() {
797 params.grasp_default_set = new_default_grasp_servers;
798 }
799 }
773 } 800 }
774 params 801 params
775 } 802 }
@@ -811,6 +838,30 @@ pub async fn sign_event(
811 } 838 }
812} 839}
813 840
841pub async fn sign_draft_event(
842 draft_event: UnsignedEvent,
843 signer: &Arc<dyn NostrSigner>,
844 description: String,
845) -> Result<nostr::Event> {
846 if signer.backend() == SignerBackend::NostrConnect {
847 let term = console::Term::stderr();
848 term.write_line(&format!(
849 "signing event ({description}) with remote signer..."
850 ))?;
851 let event = signer
852 .sign_event(draft_event)
853 .await
854 .context("failed to sign event")?;
855 term.clear_last_lines(1)?;
856 Ok(event)
857 } else {
858 signer
859 .sign_event(draft_event)
860 .await
861 .context("failed to sign event")
862 }
863}
864
814pub async fn fetch_public_key(signer: &Arc<dyn NostrSigner>) -> Result<nostr::PublicKey> { 865pub async fn fetch_public_key(signer: &Arc<dyn NostrSigner>) -> Result<nostr::PublicKey> {
815 if signer.backend() == SignerBackend::NostrConnect { 866 if signer.backend() == SignerBackend::NostrConnect {
816 let term = console::Term::stderr(); 867 let term = console::Term::stderr();
@@ -1459,7 +1510,7 @@ async fn process_fetched_events(
1459 report.updated_state = Some((event.created_at, event.id)); 1510 report.updated_state = Some((event.created_at, event.id));
1460 } 1511 }
1461 } 1512 }
1462 } else if event_is_patch_set_root(event) { 1513 } else if event_is_patch_set_root(event) || event.kind.eq(&KIND_PULL_REQUEST) {
1463 fresh_proposal_roots.insert(event.id); 1514 fresh_proposal_roots.insert(event.id);
1464 report.proposals.insert(event.id); 1515 report.proposals.insert(event.id);
1465 if !request.contributors.contains(&event.pubkey) 1516 if !request.contributors.contains(&event.pubkey)
@@ -1487,12 +1538,23 @@ async fn process_fetched_events(
1487 } 1538 }
1488 for event in &events { 1539 for event in &events {
1489 if !request.existing_events.contains(&event.id) 1540 if !request.existing_events.contains(&event.id)
1490 && !event 1541 && (!event
1491 .tags 1542 .tags
1492 .event_ids() 1543 .event_ids()
1493 .any(|id| report.proposals.contains(id)) 1544 .any(|id| report.proposals.contains(id))
1545 || event
1546 .tags
1547 .filter_standardized(TagKind::Custom(std::borrow::Cow::Borrowed("E")))
1548 .filter_map(|t| match t {
1549 TagStandard::Event { event_id, .. } => Some(event_id),
1550 TagStandard::EventReport(event_id, ..) => Some(event_id),
1551 _ => None,
1552 })
1553 .any(|id| report.proposals.contains(id)))
1494 { 1554 {
1495 if event.kind.eq(&Kind::GitPatch) && !event_is_patch_set_root(event) { 1555 if (event.kind.eq(&Kind::GitPatch) && !event_is_patch_set_root(event))
1556 || event.kind.eq(&KIND_PULL_REQUEST_UPDATE)
1557 {
1496 report.commits.insert(event.id); 1558 report.commits.insert(event.id);
1497 } else if status_kinds().contains(&event.kind) { 1559 } else if status_kinds().contains(&event.kind) {
1498 report.statuses.insert(event.id); 1560 report.statuses.insert(event.id);
@@ -1570,7 +1632,7 @@ pub fn get_fetch_filters(
1570 get_filter_state_events(repo_coordinates), 1632 get_filter_state_events(repo_coordinates),
1571 get_filter_repo_events(repo_coordinates), 1633 get_filter_repo_events(repo_coordinates),
1572 nostr::Filter::default() 1634 nostr::Filter::default()
1573 .kinds(vec![Kind::GitPatch, Kind::EventDeletion]) 1635 .kinds(vec![Kind::GitPatch, Kind::EventDeletion, KIND_PULL_REQUEST])
1574 .custom_tags( 1636 .custom_tags(
1575 SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), 1637 SingleLetterTag::lowercase(nostr_sdk::Alphabet::A),
1576 repo_coordinates 1638 repo_coordinates
@@ -1584,15 +1646,29 @@ pub fn get_fetch_filters(
1584 vec![] 1646 vec![]
1585 } else { 1647 } else {
1586 vec![ 1648 vec![
1587 nostr::Filter::default() 1649 nostr::Filter::default().events(proposal_ids.clone()).kinds(
1588 .events(proposal_ids.clone()) 1650 [
1589 .kinds([vec![Kind::GitPatch, Kind::EventDeletion], status_kinds()].concat()), 1651 vec![
1652 Kind::GitPatch,
1653 Kind::EventDeletion,
1654 KIND_PULL_REQUEST_UPDATE,
1655 ],
1656 status_kinds(),
1657 ]
1658 .concat(),
1659 ),
1590 nostr::Filter::default() 1660 nostr::Filter::default()
1591 .custom_tags( 1661 .custom_tags(
1592 SingleLetterTag::uppercase(Alphabet::E), 1662 SingleLetterTag::uppercase(Alphabet::E),
1593 proposal_ids.clone(), 1663 proposal_ids.clone(),
1594 ) 1664 )
1595 .kinds([vec![Kind::GitPatch, Kind::EventDeletion], status_kinds()].concat()), 1665 .kinds(
1666 [
1667 vec![Kind::EventDeletion, KIND_PULL_REQUEST_UPDATE],
1668 status_kinds(),
1669 ]
1670 .concat(),
1671 ),
1596 ] 1672 ]
1597 }, 1673 },
1598 if required_profiles.is_empty() { 1674 if required_profiles.is_empty() {
@@ -1784,7 +1860,7 @@ pub async fn get_proposals_and_revisions_from_cache(
1784 git_repo_path, 1860 git_repo_path,
1785 vec![ 1861 vec![
1786 nostr::Filter::default() 1862 nostr::Filter::default()
1787 .kind(nostr::Kind::GitPatch) 1863 .kinds([nostr::Kind::GitPatch, KIND_PULL_REQUEST])
1788 .custom_tags( 1864 .custom_tags(
1789 nostr::SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), 1865 nostr::SingleLetterTag::lowercase(nostr_sdk::Alphabet::A),
1790 repo_coordinates 1866 repo_coordinates
@@ -1796,7 +1872,8 @@ pub async fn get_proposals_and_revisions_from_cache(
1796 ) 1872 )
1797 .await? 1873 .await?
1798 .iter() 1874 .iter()
1799 .filter(|e| event_is_patch_set_root(e)) 1875 .filter(|e| event_is_patch_set_root(e) || e.kind.eq(&KIND_PULL_REQUEST))
1876 .filter(|e| e.kind.eq(&Kind::GitPatch) || event_is_valid_pr_or_pr_update(e))
1800 .cloned() 1877 .cloned()
1801 .collect::<Vec<nostr::Event>>(); 1878 .collect::<Vec<nostr::Event>>();
1802 proposals.sort_by_key(|e| e.created_at); 1879 proposals.sort_by_key(|e| e.created_at);
@@ -1804,7 +1881,7 @@ pub async fn get_proposals_and_revisions_from_cache(
1804 Ok(proposals) 1881 Ok(proposals)
1805} 1882}
1806 1883
1807pub async fn get_all_proposal_patch_events_from_cache( 1884pub async fn get_all_proposal_patch_pr_pr_update_events_from_cache(
1808 git_repo_path: &Path, 1885 git_repo_path: &Path,
1809 repo_ref: &RepoRef, 1886 repo_ref: &RepoRef,
1810 proposal_id: &nostr::EventId, 1887 proposal_id: &nostr::EventId,
@@ -1813,10 +1890,21 @@ pub async fn get_all_proposal_patch_events_from_cache(
1813 git_repo_path, 1890 git_repo_path,
1814 vec![ 1891 vec![
1815 nostr::Filter::default() 1892 nostr::Filter::default()
1816 .kind(nostr::Kind::GitPatch) 1893 .kinds([
1894 nostr::Kind::GitPatch,
1895 KIND_PULL_REQUEST,
1896 KIND_PULL_REQUEST_UPDATE,
1897 ])
1817 .event(*proposal_id), 1898 .event(*proposal_id),
1818 nostr::Filter::default() 1899 nostr::Filter::default()
1819 .kind(nostr::Kind::GitPatch) 1900 .kinds([
1901 nostr::Kind::GitPatch,
1902 KIND_PULL_REQUEST,
1903 KIND_PULL_REQUEST_UPDATE,
1904 ])
1905 .custom_tag(SingleLetterTag::uppercase(Alphabet::E), *proposal_id),
1906 nostr::Filter::default()
1907 .kinds([nostr::Kind::GitPatch, KIND_PULL_REQUEST])
1820 .id(*proposal_id), 1908 .id(*proposal_id),
1821 ], 1909 ],
1822 ) 1910 )
@@ -1836,7 +1924,11 @@ pub async fn get_all_proposal_patch_events_from_cache(
1836 .iter() 1924 .iter()
1837 .copied() 1925 .copied()
1838 .collect(); 1926 .collect();
1839 commit_events.retain(|e| permissioned_users.contains(&e.pubkey)); 1927
1928 commit_events.retain(|e| {
1929 permissioned_users.contains(&e.pubkey)
1930 && (e.kind.eq(&Kind::GitPatch) || event_is_valid_pr_or_pr_update(e))
1931 });
1840 1932
1841 let revision_roots: HashSet<nostr::EventId> = commit_events 1933 let revision_roots: HashSet<nostr::EventId> = commit_events
1842 .iter() 1934 .iter()
@@ -1849,8 +1941,20 @@ pub async fn get_all_proposal_patch_events_from_cache(
1849 git_repo_path, 1941 git_repo_path,
1850 vec![ 1942 vec![
1851 nostr::Filter::default() 1943 nostr::Filter::default()
1852 .kind(nostr::Kind::GitPatch) 1944 .kinds([
1853 .events(revision_roots) 1945 nostr::Kind::GitPatch,
1946 KIND_PULL_REQUEST,
1947 KIND_PULL_REQUEST_UPDATE,
1948 ])
1949 .events(revision_roots.clone())
1950 .authors(permissioned_users.clone()),
1951 nostr::Filter::default()
1952 .kinds([
1953 nostr::Kind::GitPatch,
1954 KIND_PULL_REQUEST,
1955 KIND_PULL_REQUEST_UPDATE,
1956 ])
1957 .custom_tags(SingleLetterTag::uppercase(Alphabet::E), revision_roots)
1854 .authors(permissioned_users.clone()), 1958 .authors(permissioned_users.clone()),
1855 ], 1959 ],
1856 ) 1960 )
@@ -1891,7 +1995,7 @@ pub async fn send_events(
1891 silent: bool, 1995 silent: bool,
1892) -> Result<()> { 1996) -> Result<()> {
1893 let fallback = [ 1997 let fallback = [
1894 client.get_fallback_relays().clone(), 1998 client.get_relay_default_set().clone(),
1895 if events.iter().any(|e| e.kind.eq(&Kind::GitRepoAnnouncement)) { 1999 if events.iter().any(|e| e.kind.eq(&Kind::GitRepoAnnouncement)) {
1896 client.get_blaster_relays().clone() 2000 client.get_blaster_relays().clone()
1897 } else { 2001 } else {
diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs
index d4bf2f5..b275b49 100644
--- a/src/lib/git/mod.rs
+++ b/src/lib/git/mod.rs
@@ -10,6 +10,7 @@ use nostr_sdk::{
10 Tags, 10 Tags,
11 hashes::{Hash, sha1::Hash as Sha1Hash}, 11 hashes::{Hash, sha1::Hash as Sha1Hash},
12}; 12};
13use nostr_url::NostrUrlDecoded;
13 14
14use crate::git_events::{get_commit_id_from_patch, tag_value}; 15use crate::git_events::{get_commit_id_from_patch, tag_value};
15pub mod identify_ahead_behind; 16pub mod identify_ahead_behind;
@@ -92,6 +93,10 @@ pub trait RepoActions {
92 fn get_git_config_item(&self, item: &str, global: Option<bool>) -> Result<Option<String>>; 93 fn get_git_config_item(&self, item: &str, global: Option<bool>) -> Result<Option<String>>;
93 fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()>; 94 fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()>;
94 fn remove_git_config_item(&self, item: &str, global: bool) -> Result<bool>; 95 fn remove_git_config_item(&self, item: &str, global: bool) -> Result<bool>;
96 #[allow(async_fn_in_trait)]
97 async fn get_first_nostr_remote_when_in_ngit_binary(
98 &self,
99 ) -> Result<Option<(String, NostrUrlDecoded)>>;
95} 100}
96 101
97impl RepoActions for Repo { 102impl RepoActions for Repo {
@@ -796,6 +801,21 @@ impl RepoActions for Repo {
796 Ok(true) 801 Ok(true)
797 } 802 }
798 } 803 }
804
805 async fn get_first_nostr_remote_when_in_ngit_binary(
806 &self,
807 ) -> Result<Option<(String, NostrUrlDecoded)>> {
808 for remote_name in self.git_repo.remotes()?.iter().flatten() {
809 if let Some(remote_url) = self.git_repo.find_remote(remote_name)?.url() {
810 if let Ok(nostr_url_decoded) =
811 NostrUrlDecoded::parse_and_resolve(remote_url, &Some(self)).await
812 {
813 return Ok(Some((remote_name.to_string(), nostr_url_decoded)));
814 }
815 }
816 }
817 Ok(None)
818 }
799} 819}
800 820
801fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { 821fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] {
diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs
index 69406c1..2e1f215 100644
--- a/src/lib/git_events.rs
+++ b/src/lib/git_events.rs
@@ -1,7 +1,10 @@
1use std::{str::FromStr, sync::Arc}; 1use std::{str::FromStr, sync::Arc};
2 2
3use anyhow::{Context, Result, bail}; 3use anyhow::{Context, Result, bail};
4use nostr::nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19}; 4use nostr::{
5 event::UnsignedEvent,
6 nips::{nip01::Coordinate, nip10::Marker, nip19::Nip19},
7};
5use nostr_sdk::{ 8use nostr_sdk::{
6 Event, EventBuilder, EventId, FromBech32, Kind, NostrSigner, PublicKey, Tag, TagKind, 9 Event, EventBuilder, EventId, FromBech32, Kind, NostrSigner, PublicKey, Tag, TagKind,
7 TagStandard, hashes::sha1::Hash as Sha1Hash, 10 TagStandard, hashes::sha1::Hash as Sha1Hash,
@@ -58,6 +61,9 @@ pub fn status_kinds() -> Vec<Kind> {
58 ] 61 ]
59} 62}
60 63
64pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618);
65pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619);
66
61pub fn event_is_patch_set_root(event: &Event) -> bool { 67pub fn event_is_patch_set_root(event: &Event) -> bool {
62 event.kind.eq(&Kind::GitPatch) 68 event.kind.eq(&Kind::GitPatch)
63 && event 69 && event
@@ -67,11 +73,16 @@ pub fn event_is_patch_set_root(event: &Event) -> bool {
67} 73}
68 74
69pub fn event_is_revision_root(event: &Event) -> bool { 75pub fn event_is_revision_root(event: &Event) -> bool {
70 event.kind.eq(&Kind::GitPatch) 76 (event.kind.eq(&Kind::GitPatch)
71 && event 77 && event
72 .tags 78 .tags
73 .iter() 79 .iter()
74 .any(|t| t.as_slice().len() > 1 && t.as_slice()[1].eq("revision-root")) 80 .any(|t| t.as_slice().len() > 1 && t.as_slice()[1].eq("revision-root")))
81 || (event.kind.eq(&KIND_PULL_REQUEST)
82 && event
83 .tags
84 .iter()
85 .any(|t| t.as_slice().len() > 1 && t.as_slice()[0].eq("e")))
75} 86}
76 87
77pub fn patch_supports_commit_ids(event: &Event) -> bool { 88pub fn patch_supports_commit_ids(event: &Event) -> bool {
@@ -82,6 +93,19 @@ pub fn patch_supports_commit_ids(event: &Event) -> bool {
82 .any(|t| !t.as_slice().is_empty() && t.as_slice()[0].eq("commit-pgp-sig")) 93 .any(|t| !t.as_slice().is_empty() && t.as_slice()[0].eq("commit-pgp-sig"))
83} 94}
84 95
96pub fn event_is_valid_pr_or_pr_update(event: &Event) -> bool {
97 [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&event.kind)
98 && event.tags.iter().any(|t| {
99 t.as_slice().len().gt(&1)
100 && t.as_slice()[0].eq("c")
101 && git2::Oid::from_str(&t.as_slice()[1]).is_ok()
102 })
103 && event
104 .tags
105 .iter()
106 .any(|t| t.as_slice().len().gt(&1) && t.as_slice()[0].eq("clone"))
107}
108
85#[allow(clippy::too_many_arguments)] 109#[allow(clippy::too_many_arguments)]
86#[allow(clippy::too_many_lines)] 110#[allow(clippy::too_many_lines)]
87pub async fn generate_patch_event( 111pub async fn generate_patch_event(
@@ -326,6 +350,180 @@ pub fn event_tag_from_nip19_or_hex(
326 } 350 }
327} 351}
328 352
353pub fn generate_unsigned_pr_or_update_event(
354 git_repo: &Repo,
355 repo_ref: &RepoRef,
356 signing_public_key: &PublicKey,
357 root_proposal: Option<&Event>,
358 commit: &Sha1Hash,
359 clone_url_hint: &[&str],
360 mentions: &[nostr::Tag],
361) -> Result<UnsignedEvent> {
362 let root_patch_cover_letter = if let Some(root_proposal) = root_proposal {
363 if root_proposal.kind.eq(&Kind::GitPatch) {
364 Some(event_to_cover_letter(root_proposal)?)
365 } else {
366 None
367 }
368 } else {
369 None
370 };
371
372 let title = if let Some(cl) = &root_patch_cover_letter {
373 cl.title.clone()
374 } else {
375 git_repo.get_commit_message_summary(commit)?
376 };
377
378 let description = if let Some(cl) = &root_patch_cover_letter {
379 cl.description.clone()
380 } else {
381 let mut description = git_repo.get_commit_message(commit)?.trim().to_string();
382 if let Some(remaining_description) = description.strip_prefix(&title) {
383 description = remaining_description.trim().to_string();
384 }
385 description
386 };
387
388 let root_commit = git_repo
389 .get_root_commit()
390 .context("failed to get root commit of the repository")?;
391
392 let pr_update_specific_tags = |root_proposal: &Event| {
393 vec![
394 Tag::custom(
395 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
396 vec![format!("git Pull Request Update")],
397 ),
398 Tag::custom(
399 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("E")),
400 vec![root_proposal.id],
401 ),
402 Tag::custom(
403 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("P")),
404 vec![root_proposal.pubkey],
405 ),
406 ]
407 };
408 let pr_specific_tags = || {
409 [
410 vec![
411 Tag::from_standardized(TagStandard::Subject(title.clone())),
412 Tag::custom(
413 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
414 vec![format!("git Pull Request: {}", title.clone())],
415 ),
416 ],
417 if let Some(cl) = &root_patch_cover_letter {
418 vec![
419 Tag::custom(
420 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("e")),
421 vec![root_proposal.unwrap().id],
422 ),
423 Tag::custom(
424 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")),
425 vec![cl.branch_name_without_id_or_prefix.clone()],
426 ),
427 Tag::public_key(root_proposal.unwrap().pubkey),
428 ]
429 } else if let Some(branch_name_tag) =
430 make_branch_name_tag_from_check_out_branch(git_repo)
431 {
432 vec![branch_name_tag]
433 } else {
434 vec![]
435 },
436 ]
437 .concat()
438 };
439
440 Ok(
441 if root_proposal.is_some() && root_patch_cover_letter.is_none() {
442 EventBuilder::new(KIND_PULL_REQUEST_UPDATE, "")
443 } else {
444 EventBuilder::new(KIND_PULL_REQUEST, description)
445 }
446 .tags(
447 [
448 repo_ref
449 .maintainers
450 .iter()
451 .map(|m| {
452 Tag::from_standardized(TagStandard::Coordinate {
453 coordinate: Coordinate {
454 kind: nostr::Kind::GitRepoAnnouncement,
455 public_key: *m,
456 identifier: repo_ref.identifier.to_string(),
457 },
458 relay_url: repo_ref.relays.first().cloned(),
459 uppercase: false,
460 })
461 })
462 .collect::<Vec<Tag>>(),
463 mentions.to_vec(),
464 if let Some(root_proposal) = root_proposal {
465 if root_patch_cover_letter.is_none() {
466 pr_update_specific_tags(root_proposal)
467 } else {
468 pr_specific_tags()
469 }
470 } else {
471 pr_specific_tags()
472 },
473 vec![
474 Tag::from_standardized(TagStandard::Reference(format!("{root_commit}"))),
475 Tag::custom(
476 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("c")),
477 vec![format!("{commit}")],
478 ),
479 Tag::custom(
480 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("clone")),
481 clone_url_hint
482 .iter()
483 .map(|s| s.to_string())
484 .collect::<Vec<String>>(),
485 ),
486 ],
487 repo_ref
488 .maintainers
489 .iter()
490 .map(|pk| Tag::public_key(*pk))
491 .collect(),
492 ]
493 .concat(),
494 )
495 .build(*signing_public_key),
496 )
497}
498
499fn make_branch_name_tag_from_check_out_branch(git_repo: &Repo) -> Option<Tag> {
500 if let Ok(branch_name) = git_repo.get_checked_out_branch_name() {
501 if !branch_name.eq("main")
502 && !branch_name.eq("master")
503 && !branch_name.eq("origin/main")
504 && !branch_name.eq("origin/master")
505 {
506 Some(Tag::custom(
507 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")),
508 vec![
509 if let Some(branch_name) = branch_name.strip_prefix("pr/") {
510 branch_name.to_string()
511 } else {
512 branch_name
513 }
514 .chars()
515 .take(60)
516 .collect::<String>(),
517 ],
518 ))
519 } else {
520 None
521 }
522 } else {
523 None
524 }
525}
526
329#[allow(clippy::too_many_lines)] 527#[allow(clippy::too_many_lines)]
330pub async fn generate_cover_letter_and_patch_events( 528pub async fn generate_cover_letter_and_patch_events(
331 cover_letter_title_description: Option<(String, String)>, 529 cover_letter_title_description: Option<(String, String)>,
@@ -388,24 +586,8 @@ pub async fn generate_cover_letter_and_patch_events(
388 // eventually a prefix will be needed of the event id to stop 2 proposals with the same name colliding 586 // eventually a prefix will be needed of the event id to stop 2 proposals with the same name colliding
389 // a change like this, or the removal of this tag will require the actual branch name to be tracked 587 // a change like this, or the removal of this tag will require the actual branch name to be tracked
390 // so pulling and pushing still work 588 // so pulling and pushing still work
391 if let Ok(branch_name) = git_repo.get_checked_out_branch_name() { 589 if let Some(branch_name_tag) = make_branch_name_tag_from_check_out_branch(git_repo) {
392 if !branch_name.eq("main") 590 vec![branch_name_tag]
393 && !branch_name.eq("master")
394 && !branch_name.eq("origin/main")
395 && !branch_name.eq("origin/master")
396 {
397 vec![
398 Tag::custom(
399 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("branch-name")),
400 vec![if let Some(branch_name) = branch_name.strip_prefix("pr/") {
401 branch_name.to_string()
402 } else {
403 branch_name
404 }.chars().take(60).collect::<String>()],
405 ),
406 ]
407 }
408 else { vec![] }
409 } else { 591 } else {
410 vec![] 592 vec![]
411 }, 593 },
@@ -531,13 +713,22 @@ pub fn commit_msg_from_patch_oneliner(patch: &nostr::Event) -> Result<String> {
531} 713}
532 714
533pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> { 715pub fn event_to_cover_letter(event: &nostr::Event) -> Result<CoverLetter> {
534 if !event_is_patch_set_root(event) { 716 if !event.kind.eq(&KIND_PULL_REQUEST) && !event_is_patch_set_root(event) {
535 bail!("event is not a patch set root event (root patch or cover letter)") 717 bail!("event is not a patch set root event (root patch or cover letter)")
536 } 718 }
537 719
538 let title = commit_msg_from_patch_oneliner(event)?; 720 let title = if event.kind.eq(&KIND_PULL_REQUEST) {
539 let full = commit_msg_from_patch(event)?; 721 tag_value(event, "subject").unwrap_or("untitled".to_owned())
540 let description = full[title.len()..].trim().to_string(); 722 } else {
723 commit_msg_from_patch_oneliner(event)?
724 };
725 let description = if event.kind.eq(&KIND_PULL_REQUEST) {
726 event.content.clone()
727 } else {
728 commit_msg_from_patch(event)?[title.len()..]
729 .trim()
730 .to_string()
731 };
541 732
542 Ok(CoverLetter { 733 Ok(CoverLetter {
543 title: title.clone(), 734 title: title.clone(),
@@ -569,25 +760,25 @@ fn safe_branch_name_for_pr(s: &str) -> String {
569 .collect() 760 .collect()
570} 761}
571 762
572pub fn get_most_recent_patch_with_ancestors( 763pub fn get_pr_tip_event_or_most_recent_patch_with_ancestors(
573 mut patches: Vec<nostr::Event>, 764 mut proposal_events: Vec<nostr::Event>,
574) -> Result<Vec<nostr::Event>> { 765) -> Result<Vec<nostr::Event>> {
575 patches.sort_by_key(|e| e.created_at); 766 proposal_events.sort_by_key(|e| e.created_at);
576 767
577 let youngest_patch = patches.last().context("no patches found")?; 768 let youngest = proposal_events.last().context("no proposal events found")?;
578 769
579 let patches_with_youngest_created_at: Vec<&nostr::Event> = patches 770 let events_with_youngest_created_at: Vec<&nostr::Event> = proposal_events
580 .iter() 771 .iter()
581 .filter(|p| p.created_at.eq(&youngest_patch.created_at)) 772 .filter(|p| p.created_at.eq(&youngest.created_at))
582 .collect(); 773 .collect();
583 774
584 let mut res = vec![]; 775 let mut res = vec![];
585 776
586 let mut event_id_to_search = patches_with_youngest_created_at 777 let mut event_id_to_search = events_with_youngest_created_at
587 .clone() 778 .clone()
588 .iter() 779 .iter()
589 .find(|p| { 780 .find(|p| {
590 !patches_with_youngest_created_at.iter().any(|p2| { 781 !events_with_youngest_created_at.iter().any(|p2| {
591 if let Ok(reply_to) = get_event_parent_id(p2) { 782 if let Ok(reply_to) = get_event_parent_id(p2) {
592 reply_to.eq(&p.id.to_string()) 783 reply_to.eq(&p.id.to_string())
593 } else { 784 } else {
@@ -595,16 +786,18 @@ pub fn get_most_recent_patch_with_ancestors(
595 } 786 }
596 }) 787 })
597 }) 788 })
598 .context("failed to find patches_with_youngest_created_at")? 789 .context("failed to find events_with_youngest_created_at")?
599 .id 790 .id
600 .to_string(); 791 .to_string();
601 792
602 while let Some(event) = patches 793 while let Some(event) = proposal_events
603 .iter() 794 .iter()
604 .find(|e| e.id.to_string().eq(&event_id_to_search)) 795 .find(|e| e.id.to_string().eq(&event_id_to_search))
605 { 796 {
606 res.push(event.clone()); 797 res.push(event.clone());
607 if event_is_patch_set_root(event) { 798 if [KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE].contains(&event.kind)
799 || event_is_patch_set_root(event)
800 {
608 break; 801 break;
609 } 802 }
610 event_id_to_search = get_event_parent_id(event).unwrap_or_default(); 803 event_id_to_search = get_event_parent_id(event).unwrap_or_default();
@@ -642,7 +835,61 @@ pub fn is_event_proposal_root_for_branch(
642 || cl 835 || cl
643 .get_branch_name_with_pr_prefix_and_shorthand_id() 836 .get_branch_name_with_pr_prefix_and_shorthand_id()
644 .is_ok_and(|s| s.eq(&branch_name)) 837 .is_ok_and(|s| s.eq(&branch_name))
645 }) && !event_is_revision_root(e)) 838 }) && (
839 // If we wanted to treat to list Pull Requests that revise a Patch we would do this:
840 // Note: whilst this the the case elsewhere event_is_revision_root is used, there is more to
841 // think about here?
842 // e.kind.eq(&KIND_PULL_REQUEST) ||
843 !event_is_revision_root(e)
844 ))
845}
846
847pub fn get_status(
848 proposal: &Event,
849 repo_ref: &RepoRef,
850 all_status_in_repo: &[Event],
851 all_pr_roots_in_repo: &[Event],
852) -> Kind {
853 let get_direct_status = |proposal: &Event| {
854 if let Some(e) = all_status_in_repo
855 .iter()
856 .filter(|e| {
857 status_kinds().contains(&e.kind)
858 && e.tags.iter().any(|t| {
859 t.as_slice().len() > 1 && t.as_slice()[1].eq(&proposal.id.to_string())
860 })
861 && (proposal.pubkey.eq(&e.pubkey) || repo_ref.maintainers.contains(&e.pubkey))
862 })
863 .collect::<Vec<&nostr::Event>>()
864 .first()
865 {
866 e.kind
867 } else {
868 Kind::GitStatusOpen
869 }
870 };
871 let is_proposal_pr_revision_of_patch = |proposal: &Event, patch: &Event| {
872 proposal.kind.eq(&KIND_PULL_REQUEST)
873 && proposal.tags.clone().into_iter().any(|t| {
874 t.as_slice().len() > 1
875 && t.as_slice()[0].eq("e")
876 && t.as_slice()[1].eq(&patch.id.to_string())
877 })
878 };
879
880 let direct_status = get_direct_status(proposal);
881 if direct_status.eq(&Kind::GitStatusClosed) && proposal.kind.eq(&Kind::GitPatch) {
882 if let Some(pr_revision) = all_pr_roots_in_repo
883 .iter()
884 .find(|p| is_proposal_pr_revision_of_patch(p, proposal))
885 {
886 get_direct_status(pr_revision)
887 } else {
888 direct_status
889 }
890 } else {
891 direct_status
892 }
646} 893}
647 894
648#[cfg(test)] 895#[cfg(test)]
diff --git a/src/lib/login/fresh.rs b/src/lib/login/fresh.rs
index a169177..358045a 100644
--- a/src/lib/login/fresh.rs
+++ b/src/lib/login/fresh.rs
@@ -728,7 +728,7 @@ async fn signup(
728 EventBuilder::metadata(&Metadata::new().name(name)).sign_with_keys(&keys)?; 728 EventBuilder::metadata(&Metadata::new().name(name)).sign_with_keys(&keys)?;
729 let relay_list = EventBuilder::relay_list( 729 let relay_list = EventBuilder::relay_list(
730 client 730 client
731 .get_fallback_relays() 731 .get_relay_default_set()
732 .iter() 732 .iter()
733 .map(|s| (RelayUrl::parse(s).unwrap(), None)), 733 .map(|s| (RelayUrl::parse(s).unwrap(), None)),
734 ) 734 )
@@ -738,7 +738,7 @@ async fn signup(
738 client, 738 client,
739 None, 739 None,
740 vec![profile, relay_list], 740 vec![profile, relay_list],
741 client.get_fallback_relays().clone(), 741 client.get_relay_default_set().clone(),
742 vec![], 742 vec![],
743 true, 743 true,
744 false, 744 false,
diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs
index 0236e34..bca4a3b 100644
--- a/src/lib/repo_ref.rs
+++ b/src/lib/repo_ref.rs
@@ -309,7 +309,7 @@ impl RepoRef {
309 } 309 }
310 310
311 pub fn grasp_servers(&self) -> Vec<String> { 311 pub fn grasp_servers(&self) -> Vec<String> {
312 detect_existing_grasp_servers(Some(self), &[], &[], &[], &self.identifier) 312 detect_existing_grasp_servers(Some(self), &[], &[], &self.identifier)
313 } 313 }
314} 314}
315 315
@@ -593,7 +593,6 @@ pub fn detect_existing_grasp_servers(
593 repo_ref: Option<&RepoRef>, 593 repo_ref: Option<&RepoRef>,
594 args_relays: &[String], 594 args_relays: &[String],
595 args_clone_url: &[String], 595 args_clone_url: &[String],
596 args_blossoms: &[String],
597 identifier: &str, 596 identifier: &str,
598) -> Vec<String> { 597) -> Vec<String> {
599 // Collect clone URLs from arguments or repo_ref 598 // Collect clone URLs from arguments or repo_ref
@@ -617,18 +616,6 @@ pub fn detect_existing_grasp_servers(
617 Vec::new() 616 Vec::new()
618 }; 617 };
619 618
620 // Collect blossom server URLs from arguments or repo_ref
621 let blossoms: Vec<Url> = if !args_blossoms.is_empty() {
622 args_blossoms
623 .iter()
624 .filter_map(|r| Url::parse(r).ok())
625 .collect()
626 } else if let Some(repo) = repo_ref {
627 repo.blossoms.clone()
628 } else {
629 Vec::new()
630 };
631
632 let mut existing_grasp_servers = Vec::new(); 619 let mut existing_grasp_servers = Vec::new();
633 for url in &clone_urls { 620 for url in &clone_urls {
634 let Ok(formatted_as_grasp_server_url) = normalize_grasp_server_url(url) else { 621 let Ok(formatted_as_grasp_server_url) = normalize_grasp_server_url(url) else {
@@ -655,14 +642,6 @@ pub fn detect_existing_grasp_servers(
655 continue; 642 continue;
656 } 643 }
657 644
658 let matches_blossoms = blossoms.iter().any(|r| {
659 normalize_grasp_server_url(r.as_str())
660 .is_ok_and(|r| r.eq(&formatted_as_grasp_server_url))
661 });
662 if !matches_blossoms {
663 continue;
664 }
665
666 existing_grasp_servers.push(formatted_as_grasp_server_url); 645 existing_grasp_servers.push(formatted_as_grasp_server_url);
667 } 646 }
668 existing_grasp_servers 647 existing_grasp_servers