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>2025-08-06 12:52:59 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2025-08-07 17:25:50 +0100
commita9b2ebf8216be34950e54dd9a446dbdc0c9c744a (patch)
tree5a103933852fbcfcd42b13716cb92eeca5325d6d /src/lib
parent29f61ffdf155ea88b8d9aec23d28cf70baba577e (diff)
feat(send): PR fallback to user / custom grasp
if use is maintainer, push PR to all repo git servers. if user has a fork, push to all git servers it lists, and repo grasp servers. if user hasn't got a fork but has a user grasp list and pushing push to repo grasp servers fails, create a personal-fork automatically at each user grasp server and push there. fallback to prompting user for either grasp servers or git server with write permission. if user provides grasp servers, suggesting adding to user preference list.
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/cli_interactor.rs87
-rw-r--r--src/lib/client.rs54
-rw-r--r--src/lib/git_events.rs1
-rw-r--r--src/lib/login/user.rs77
-rw-r--r--src/lib/repo_ref.rs83
5 files changed, 267 insertions, 35 deletions
diff --git a/src/lib/cli_interactor.rs b/src/lib/cli_interactor.rs
index 8fca81d..8bcda19 100644
--- a/src/lib/cli_interactor.rs
+++ b/src/lib/cli_interactor.rs
@@ -236,6 +236,93 @@ impl PromptMultiChoiceParms {
236 } 236 }
237} 237}
238 238
239pub fn multi_select_with_custom_value<F>(
240 prompt: &str,
241 custom_choice_prompt: &str,
242 mut choices: Vec<String>,
243 mut defaults: Vec<bool>,
244 validate_choice: F,
245) -> Result<Vec<String>>
246where
247 F: Fn(&str) -> Result<String>,
248{
249 let mut selected_choices = vec![];
250
251 // Loop to allow users to add more choices
252 loop {
253 // Add 'add another' option at the end of the choices
254 let mut current_choices = choices.clone();
255 current_choices.push(if current_choices.is_empty() {
256 "add".to_string()
257 } else {
258 "add another".to_string()
259 });
260
261 // Create default selections based on the provided defaults
262 let mut current_defaults = defaults.clone();
263 current_defaults.push(current_choices.len() == 1); // 'add another' should not be selected by default
264
265 // Prompt for selections
266 let selected_indices: Vec<usize> = Interactor::default().multi_choice(
267 PromptMultiChoiceParms::default()
268 .with_prompt(prompt)
269 .dont_report()
270 .with_choices(current_choices.clone())
271 .with_defaults(current_defaults),
272 )?;
273
274 // Collect selected choices
275 selected_choices.clear(); // Clear previous selections to update
276 for &index in &selected_indices {
277 if index < choices.len() {
278 // Exclude 'add another' option
279 selected_choices.push(choices[index].clone());
280 }
281 }
282
283 // Check if 'add another' was selected
284 if selected_indices.contains(&(choices.len())) {
285 // Last index is 'add another'
286 let mut new_choice: String;
287 loop {
288 new_choice = Interactor::default().input(
289 PromptInputParms::default()
290 .with_prompt(custom_choice_prompt)
291 .dont_report()
292 .optional(),
293 )?;
294
295 if new_choice.is_empty() {
296 break;
297 }
298 // Validate the new choice
299 match validate_choice(&new_choice) {
300 Ok(valid_choice) => {
301 new_choice = valid_choice; // Use the fixed version of the input
302 break; // Valid choice, exit the loop
303 }
304 Err(err) => {
305 // Inform the user about the validation error
306 println!("Error: {err}");
307 }
308 }
309 }
310
311 // Add the new choice to the choices vector
312 if !new_choice.is_empty() {
313 choices.push(new_choice.clone()); // Add new choice to the end of the list
314 selected_choices.push(new_choice); // Automatically select the new choice
315 defaults.push(true); // Set the new choice as selected by default
316 }
317 } else {
318 // Exit the loop if 'add another' was not selected
319 break;
320 }
321 }
322
323 Ok(selected_choices)
324}
325
239#[derive(Debug, Default)] 326#[derive(Debug, Default)]
240pub struct Printer { 327pub struct Printer {
241 printed_lines: Vec<String>, 328 printed_lines: Vec<String>,
diff --git a/src/lib/client.rs b/src/lib/client.rs
index b27f9b1..9ce3e24 100644
--- a/src/lib/client.rs
+++ b/src/lib/client.rs
@@ -53,7 +53,7 @@ use crate::{
53 get_dirs, 53 get_dirs,
54 git::{Repo, RepoActions, get_git_config_item}, 54 git::{Repo, RepoActions, get_git_config_item},
55 git_events::{ 55 git_events::{
56 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_is_cover_letter, 56 KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, KIND_USER_GRASP_LIST, event_is_cover_letter,
57 event_is_patch_set_root, event_is_revision_root, event_is_valid_pr_or_pr_update, 57 event_is_patch_set_root, event_is_revision_root, event_is_valid_pr_or_pr_update,
58 status_kinds, 58 status_kinds,
59 }, 59 },
@@ -233,7 +233,7 @@ impl Connect for Client {
233 if let Some(git_repo_path) = git_repo_path { 233 if let Some(git_repo_path) = git_repo_path {
234 save_event_in_local_cache(git_repo_path, &event).await?; 234 save_event_in_local_cache(git_repo_path, &event).await?;
235 } 235 }
236 if event.kind.eq(&Kind::GitRepoAnnouncement) { 236 if [Kind::GitRepoAnnouncement, KIND_USER_GRASP_LIST].contains(&event.kind) {
237 save_event_in_global_cache(git_repo_path, &event).await?; 237 save_event_in_global_cache(git_repo_path, &event).await?;
238 } 238 }
239 Ok(event.id) 239 Ok(event.id)
@@ -1310,17 +1310,21 @@ async fn create_relays_request(
1310 user_profiles.insert(current_user); 1310 user_profiles.insert(current_user);
1311 } 1311 }
1312 } 1312 }
1313 let mut map: HashMap<PublicKey, (Timestamp, Timestamp)> = HashMap::new(); 1313 let mut map: HashMap<PublicKey, (Timestamp, Timestamp, Timestamp)> = HashMap::new();
1314 for public_key in &user_profiles { 1314 for public_key in &user_profiles {
1315 if let Ok(user_ref) = get_user_ref_from_cache(git_repo_path, public_key).await { 1315 if let Ok(user_ref) = get_user_ref_from_cache(git_repo_path, public_key).await {
1316 map.insert( 1316 map.insert(
1317 public_key.to_owned(), 1317 public_key.to_owned(),
1318 (user_ref.metadata.created_at, user_ref.relays.created_at), 1318 (
1319 user_ref.metadata.created_at,
1320 user_ref.relays.created_at,
1321 user_ref.grasp_list.created_at,
1322 ),
1319 ); 1323 );
1320 } else { 1324 } else {
1321 map.insert( 1325 map.insert(
1322 public_key.to_owned(), 1326 public_key.to_owned(),
1323 (Timestamp::from(0), Timestamp::from(0)), 1327 (Timestamp::from(0), Timestamp::from(0), Timestamp::from(0)),
1324 ); 1328 );
1325 } 1329 }
1326 } 1330 }
@@ -1547,16 +1551,22 @@ async fn process_fetched_events(
1547 { 1551 {
1548 fresh_profiles.insert(event.pubkey); 1552 fresh_profiles.insert(event.pubkey);
1549 } 1553 }
1550 } else if [Kind::RelayList, Kind::Metadata].contains(&event.kind) { 1554 } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind)
1555 {
1551 if request.missing_contributor_profiles.contains(&event.pubkey) { 1556 if request.missing_contributor_profiles.contains(&event.pubkey) {
1552 report.contributor_profiles.insert(event.pubkey); 1557 report.contributor_profiles.insert(event.pubkey);
1553 } else if let Some((_, (metadata_timestamp, relay_list_timestamp))) = request 1558 } else if let Some((
1559 _,
1560 (metadata_timestamp, relay_list_timestamp, grasp_list_timestamp),
1561 )) = request
1554 .profiles_to_fetch_from_user_relays 1562 .profiles_to_fetch_from_user_relays
1555 .get_key_value(&event.pubkey) 1563 .get_key_value(&event.pubkey)
1556 { 1564 {
1557 if (Kind::Metadata.eq(&event.kind) && event.created_at.gt(metadata_timestamp)) 1565 if (Kind::Metadata.eq(&event.kind) && event.created_at.gt(metadata_timestamp))
1558 || (Kind::RelayList.eq(&event.kind) 1566 || (Kind::RelayList.eq(&event.kind)
1559 && event.created_at.gt(relay_list_timestamp)) 1567 && event.created_at.gt(relay_list_timestamp))
1568 || (KIND_USER_GRASP_LIST.eq(&event.kind)
1569 && event.created_at.gt(grasp_list_timestamp))
1560 { 1570 {
1561 report.profile_updates.insert(event.pubkey); 1571 report.profile_updates.insert(event.pubkey);
1562 } 1572 }
@@ -1718,35 +1728,21 @@ pub fn get_filter_repo_events(repo_coordinates: &HashSet<Nip19Coordinate>) -> no
1718 .map(|c| c.identifier.clone()) 1728 .map(|c| c.identifier.clone())
1719 .collect::<Vec<String>>(), 1729 .collect::<Vec<String>>(),
1720 ) 1730 )
1721 .authors(
1722 repo_coordinates
1723 .iter()
1724 .map(|c| c.public_key)
1725 .collect::<Vec<PublicKey>>(),
1726 )
1727} 1731}
1728 1732
1729pub static STATE_KIND: nostr::Kind = Kind::Custom(30618); 1733pub static STATE_KIND: nostr::Kind = Kind::Custom(30618);
1730pub fn get_filter_state_events(repo_coordinates: &HashSet<Nip19Coordinate>) -> nostr::Filter { 1734pub fn get_filter_state_events(repo_coordinates: &HashSet<Nip19Coordinate>) -> nostr::Filter {
1731 nostr::Filter::default() 1735 nostr::Filter::default().kind(STATE_KIND).identifiers(
1732 .kind(STATE_KIND) 1736 repo_coordinates
1733 .identifiers( 1737 .iter()
1734 repo_coordinates 1738 .map(|c| c.identifier.clone())
1735 .iter() 1739 .collect::<Vec<String>>(),
1736 .map(|c| c.identifier.clone()) 1740 )
1737 .collect::<Vec<String>>(),
1738 )
1739 .authors(
1740 repo_coordinates
1741 .iter()
1742 .map(|c| c.public_key)
1743 .collect::<Vec<PublicKey>>(),
1744 )
1745} 1741}
1746 1742
1747pub fn get_filter_contributor_profiles(contributors: HashSet<PublicKey>) -> nostr::Filter { 1743pub fn get_filter_contributor_profiles(contributors: HashSet<PublicKey>) -> nostr::Filter {
1748 nostr::Filter::default() 1744 nostr::Filter::default()
1749 .kinds(vec![Kind::Metadata, Kind::RelayList]) 1745 .kinds(vec![Kind::Metadata, Kind::RelayList, KIND_USER_GRASP_LIST])
1750 .authors(contributors) 1746 .authors(contributors)
1751} 1747}
1752 1748
@@ -1850,7 +1846,7 @@ pub struct FetchRequest {
1850 contributors: HashSet<PublicKey>, 1846 contributors: HashSet<PublicKey>,
1851 missing_contributor_profiles: HashSet<PublicKey>, 1847 missing_contributor_profiles: HashSet<PublicKey>,
1852 existing_events: HashSet<EventId>, 1848 existing_events: HashSet<EventId>,
1853 profiles_to_fetch_from_user_relays: HashMap<PublicKey, (Timestamp, Timestamp)>, 1849 profiles_to_fetch_from_user_relays: HashMap<PublicKey, (Timestamp, Timestamp, Timestamp)>,
1854 user_relays_for_profiles: HashSet<RelayUrl>, 1850 user_relays_for_profiles: HashSet<RelayUrl>,
1855} 1851}
1856 1852
diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs
index bbfcbea..76c31de 100644
--- a/src/lib/git_events.rs
+++ b/src/lib/git_events.rs
@@ -63,6 +63,7 @@ pub fn status_kinds() -> Vec<Kind> {
63 63
64pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618); 64pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618);
65pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619); 65pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619);
66pub const KIND_USER_GRASP_LIST: Kind = Kind::Custom(10317);
66 67
67pub fn event_is_patch_set_root(event: &Event) -> bool { 68pub fn event_is_patch_set_root(event: &Event) -> bool {
68 event.kind.eq(&Kind::GitPatch) 69 event.kind.eq(&Kind::GitPatch)
diff --git a/src/lib/login/user.rs b/src/lib/login/user.rs
index 071cb25..0b702ef 100644
--- a/src/lib/login/user.rs
+++ b/src/lib/login/user.rs
@@ -1,7 +1,7 @@
1use std::{collections::HashSet, path::Path}; 1use std::{collections::HashSet, path::Path, sync::Arc};
2 2
3use anyhow::{Context, Result, bail}; 3use anyhow::{Context, Result, bail};
4use nostr::PublicKey; 4use nostr::{PublicKey, Url, event::Tag, signer::NostrSigner};
5use nostr_sdk::{Alphabet, JsonUtil, Kind, SingleLetterTag, Timestamp, ToBech32}; 5use nostr_sdk::{Alphabet, JsonUtil, Kind, SingleLetterTag, Timestamp, ToBech32};
6use serde::{self, Deserialize, Serialize}; 6use serde::{self, Deserialize, Serialize};
7 7
@@ -9,13 +9,17 @@ use serde::{self, Deserialize, Serialize};
9use crate::client::Client; 9use crate::client::Client;
10#[cfg(test)] 10#[cfg(test)]
11use crate::client::MockConnect; 11use crate::client::MockConnect;
12use crate::client::{Connect, get_event_from_global_cache}; 12use crate::{
13 client::{Connect, get_event_from_global_cache, sign_event},
14 git_events::KIND_USER_GRASP_LIST,
15};
13 16
14#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 17#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
15pub struct UserRef { 18pub struct UserRef {
16 pub public_key: PublicKey, 19 pub public_key: PublicKey,
17 pub metadata: UserMetadata, 20 pub metadata: UserMetadata,
18 pub relays: UserRelays, 21 pub relays: UserRelays,
22 pub grasp_list: UserGraspList,
19} 23}
20 24
21#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 25#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
@@ -49,6 +53,35 @@ impl UserRelays {
49} 53}
50 54
51#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] 55#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
56pub struct UserGraspList {
57 pub urls: Vec<Url>,
58 pub created_at: Timestamp,
59}
60
61impl UserGraspList {
62 pub async fn to_event(&mut self, signer: &Arc<dyn NostrSigner>) -> Result<nostr::Event> {
63 let event = sign_event(
64 nostr_sdk::EventBuilder::new(KIND_USER_GRASP_LIST, "").tags(
65 self.urls
66 .iter()
67 .map(|url| {
68 Tag::custom(
69 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("g")),
70 vec![url.to_string()],
71 )
72 })
73 .collect::<Vec<_>>(),
74 ),
75 signer,
76 "user grasp list".to_string(),
77 )
78 .await?;
79 self.created_at = event.created_at;
80 Ok(event)
81 }
82}
83
84#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
52pub struct UserRelayRef { 85pub struct UserRelayRef {
53 pub url: String, 86 pub url: String,
54 pub read: bool, 87 pub read: bool,
@@ -84,6 +117,7 @@ pub async fn get_user_details(
84 public_key: public_key.to_owned(), 117 public_key: public_key.to_owned(),
85 metadata: extract_user_metadata(public_key, &[])?, 118 metadata: extract_user_metadata(public_key, &[])?,
86 relays: extract_user_relays(public_key, &[]), 119 relays: extract_user_relays(public_key, &[]),
120 grasp_list: extract_user_grasp_list(public_key, &[]),
87 }; 121 };
88 if cache_only { 122 if cache_only {
89 Ok(empty) 123 Ok(empty)
@@ -117,6 +151,9 @@ pub async fn get_user_ref_from_cache(
117 nostr::Filter::default() 151 nostr::Filter::default()
118 .author(*public_key) 152 .author(*public_key)
119 .kind(Kind::RelayList), 153 .kind(Kind::RelayList),
154 nostr::Filter::default()
155 .author(*public_key)
156 .kind(KIND_USER_GRASP_LIST),
120 ]; 157 ];
121 158
122 let events = get_event_from_global_cache(git_repo_path, filters.clone()).await?; 159 let events = get_event_from_global_cache(git_repo_path, filters.clone()).await?;
@@ -128,6 +165,7 @@ pub async fn get_user_ref_from_cache(
128 public_key: public_key.to_owned(), 165 public_key: public_key.to_owned(),
129 metadata: extract_user_metadata(public_key, &events)?, 166 metadata: extract_user_metadata(public_key, &events)?,
130 relays: extract_user_relays(public_key, &events), 167 relays: extract_user_relays(public_key, &events),
168 grasp_list: extract_user_grasp_list(public_key, &events),
131 }) 169 })
132} 170}
133 171
@@ -215,3 +253,36 @@ pub fn extract_user_relays(public_key: &nostr::PublicKey, events: &[nostr::Event
215 }, 253 },
216 } 254 }
217} 255}
256
257pub fn extract_user_grasp_list(
258 public_key: &nostr::PublicKey,
259 events: &[nostr::Event],
260) -> UserGraspList {
261 let event = events
262 .iter()
263 .filter(|e| e.kind.eq(&KIND_USER_GRASP_LIST) && e.pubkey.eq(public_key))
264 .max_by_key(|e| e.created_at);
265
266 UserGraspList {
267 urls: if let Some(event) = event {
268 event
269 .tags
270 .iter()
271 .filter_map(|t| {
272 if t.as_slice().len() > 1 && t.as_slice()[0] == "g" {
273 Url::parse(&t.as_slice()[1]).ok()
274 } else {
275 None
276 }
277 })
278 .collect()
279 } else {
280 vec![]
281 },
282 created_at: if let Some(event) = event {
283 event.created_at
284 } else {
285 Timestamp::from(0)
286 },
287 }
288}
diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs
index a3e1317..e3f71a1 100644
--- a/src/lib/repo_ref.rs
+++ b/src/lib/repo_ref.rs
@@ -40,6 +40,7 @@ pub struct RepoRef {
40 pub web: Vec<String>, 40 pub web: Vec<String>,
41 pub relays: Vec<RelayUrl>, 41 pub relays: Vec<RelayUrl>,
42 pub blossoms: Vec<Url>, 42 pub blossoms: Vec<Url>,
43 pub hashtags: Vec<String>,
43 pub maintainers: Vec<PublicKey>, 44 pub maintainers: Vec<PublicKey>,
44 pub trusted_maintainer: PublicKey, 45 pub trusted_maintainer: PublicKey,
45 // set to None if not known 46 // set to None if not known
@@ -71,6 +72,7 @@ impl TryFrom<(nostr::Event, Option<PublicKey>)> for RepoRef {
71 web: Vec::new(), 72 web: Vec::new(),
72 relays: Vec::new(), 73 relays: Vec::new(),
73 blossoms: Vec::new(), 74 blossoms: Vec::new(),
75 hashtags: Vec::new(),
74 maintainers: Vec::new(), 76 maintainers: Vec::new(),
75 trusted_maintainer: trusted_maintainer.unwrap_or(event.pubkey), 77 trusted_maintainer: trusted_maintainer.unwrap_or(event.pubkey),
76 maintainers_without_annoucnement: None, 78 maintainers_without_annoucnement: None,
@@ -118,6 +120,7 @@ impl TryFrom<(nostr::Event, Option<PublicKey>)> for RepoRef {
118 } 120 }
119 } 121 }
120 } 122 }
123 [t, hashtag, ..] if t == "t" => r.hashtags.push(hashtag.clone()),
121 [t, blossoms @ ..] if t == "blossoms" => { 124 [t, blossoms @ ..] if t == "blossoms" => {
122 for b in blossoms { 125 for b in blossoms {
123 if let Ok(b) = Url::parse(b) { 126 if let Ok(b) = Url::parse(b) {
@@ -217,6 +220,15 @@ impl RepoRef {
217 vec![format!("git repository: {}", self.name.clone())], 220 vec![format!("git repository: {}", self.name.clone())],
218 ), 221 ),
219 ], 222 ],
223 self.hashtags
224 .iter()
225 .map(|h| {
226 Tag::custom(
227 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("t")),
228 vec![h.clone()],
229 )
230 })
231 .collect(),
220 if self.blossoms.is_empty() { 232 if self.blossoms.is_empty() {
221 vec![] 233 vec![]
222 } else { 234 } else {
@@ -311,6 +323,34 @@ impl RepoRef {
311 pub fn grasp_servers(&self) -> Vec<String> { 323 pub fn grasp_servers(&self) -> Vec<String> {
312 detect_existing_grasp_servers(Some(self), &[], &[], &self.identifier) 324 detect_existing_grasp_servers(Some(self), &[], &[], &self.identifier)
313 } 325 }
326
327 // returns false if already present so didn't need adding
328 pub fn add_grasp_server(&mut self, clone_url: &str) -> Result<bool> {
329 if !clone_url.starts_with("http") {
330 bail!("invalid grasp server clone url");
331 }
332 extract_npub(clone_url)
333 .context("invalid grasp server clone url. does not contain valid npub")?;
334 if !(clone_url.ends_with(".git") || clone_url.ends_with(".git/")) {
335 bail!("invalid grasp server clone url. does not end with .git");
336 }
337
338 let relay_url = RelayUrl::parse(
339 &format_grasp_server_url_as_relay_url(clone_url)
340 .context("invalid grasp server clone url")?,
341 )
342 .context("invalid grasp server clone url")?;
343
344 if !self.relays.contains(&relay_url) {
345 self.relays.push(relay_url);
346 }
347 if !self.git_server.contains(&clone_url.to_string()) {
348 self.git_server.push(clone_url.to_string());
349 Ok(true)
350 } else {
351 Ok(false)
352 }
353 }
314} 354}
315 355
316pub async fn get_repo_coordinates_when_remote_unknown( 356pub async fn get_repo_coordinates_when_remote_unknown(
@@ -699,13 +739,49 @@ pub fn extract_npub(s: &str) -> Result<&str> {
699 } 739 }
700} 740}
701 741
742// this should be called is_grasp_server_in_list
702pub fn is_grasp_server(url: &str, grasp_servers: &[String]) -> bool { 743pub fn is_grasp_server(url: &str, grasp_servers: &[String]) -> bool {
703 if !grasp_servers.is_empty() { 744 if !grasp_servers.is_empty() {
704 if let Ok(n) = normalize_grasp_server_url(url) { 745 if let Ok(url) = normalize_grasp_server_url(url) {
705 return grasp_servers.contains(&n); 746 grasp_servers.iter().any(|s| {
747 if let Ok(s) = normalize_grasp_server_url(s) {
748 s == url
749 } else {
750 false
751 }
752 })
753 } else {
754 false
706 } 755 }
756 } else {
757 false
758 }
759}
760
761pub fn format_grasp_server_url_as_relay_url(url: &str) -> Result<String> {
762 let grasp_server_url = normalize_grasp_server_url(url)?;
763 if grasp_server_url.contains("http://") {
764 return Ok(grasp_server_url.replace("http://", "ws://"));
707 } 765 }
708 false 766 Ok(format!("wss://{grasp_server_url}"))
767}
768
769pub fn format_grasp_server_url_as_clone_url(
770 grasp_server: &str,
771 public_key: &PublicKey,
772 identifier: &str,
773) -> Result<String> {
774 let grasp_server_url = normalize_grasp_server_url(grasp_server)?;
775
776 let prefix = if grasp_server_url.contains("http://") {
777 ""
778 } else {
779 "https://"
780 };
781 Ok(format!(
782 "{prefix}{grasp_server_url}/{}/{identifier}.git",
783 public_key.to_bech32()?
784 ))
709} 785}
710 786
711#[cfg(test)] 787#[cfg(test)]
@@ -730,6 +806,7 @@ mod tests {
730 RelayUrl::parse("ws://relay2.io").unwrap(), 806 RelayUrl::parse("ws://relay2.io").unwrap(),
731 ], 807 ],
732 blossoms: vec![], 808 blossoms: vec![],
809 hashtags: vec![],
733 trusted_maintainer: TEST_KEY_1_KEYS.public_key(), 810 trusted_maintainer: TEST_KEY_1_KEYS.public_key(),
734 maintainers_without_annoucnement: None, 811 maintainers_without_annoucnement: None,
735 maintainers: vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()], 812 maintainers: vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()],