upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-12-12 07:33:15 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2024-12-12 07:33:15 +0000
commit2b49a548169bf9bc3a3ef667a7e952e31e878bab (patch)
tree69e3fe439a0326729422a92488ee62420e28c9b7 /src
parentd8f4f7641312bff32f772cbc070b3f99ced0c8fe (diff)
parent2c6e281c1643593cd079936924dc1d4e65c54ed6 (diff)
feat(NostrUrlDecode): add nip05 address support
enable nostr git url format alongside and format Merge branch 'pr/nip05-lez(ff1845c0)'
Diffstat (limited to 'src')
-rw-r--r--src/bin/git_remote_nostr/main.rs12
-rw-r--r--src/bin/ngit/sub_commands/init.rs16
-rw-r--r--src/lib/git/nostr_url.rs203
-rw-r--r--src/lib/repo_ref.rs34
4 files changed, 193 insertions, 72 deletions
diff --git a/src/bin/git_remote_nostr/main.rs b/src/bin/git_remote_nostr/main.rs
index 5f9f712..1a21341 100644
--- a/src/bin/git_remote_nostr/main.rs
+++ b/src/bin/git_remote_nostr/main.rs
@@ -9,7 +9,6 @@ use std::{
9 collections::HashSet, 9 collections::HashSet,
10 env, io, 10 env, io,
11 path::{Path, PathBuf}, 11 path::{Path, PathBuf},
12 str::FromStr,
13}; 12};
14 13
15use anyhow::{bail, Context, Result}; 14use anyhow::{bail, Context, Result};
@@ -28,7 +27,7 @@ mod utils;
28 27
29#[tokio::main] 28#[tokio::main]
30async fn main() -> Result<()> { 29async fn main() -> Result<()> {
31 let Some((decoded_nostr_url, git_repo)) = process_args()? else { 30 let Some((decoded_nostr_url, git_repo)) = process_args().await? else {
32 return Ok(()); 31 return Ok(());
33 }; 32 };
34 33
@@ -118,7 +117,7 @@ async fn main() -> Result<()> {
118 } 117 }
119} 118}
120 119
121fn process_args() -> Result<Option<(NostrUrlDecoded, Repo)>> { 120async fn process_args() -> Result<Option<(NostrUrlDecoded, Repo)>> {
122 let args = env::args(); 121 let args = env::args();
123 let args = args.skip(1).take(2).collect::<Vec<_>>(); 122 let args = args.skip(1).take(2).collect::<Vec<_>>();
124 123
@@ -144,13 +143,14 @@ fn process_args() -> Result<Option<(NostrUrlDecoded, Repo)>> {
144 return Ok(None); 143 return Ok(None);
145 }; 144 };
146 145
147 let decoded_nostr_url =
148 NostrUrlDecoded::from_str(nostr_remote_url).context("invalid nostr url")?;
149
150 let git_repo = Repo::from_path(&PathBuf::from( 146 let git_repo = Repo::from_path(&PathBuf::from(
151 std::env::var("GIT_DIR").context("git should set GIT_DIR when remote helper is called")?, 147 std::env::var("GIT_DIR").context("git should set GIT_DIR when remote helper is called")?,
152 ))?; 148 ))?;
153 149
150 let decoded_nostr_url = NostrUrlDecoded::parse_and_resolve(nostr_remote_url, &Some(&git_repo))
151 .await
152 .context("invalid nostr url")?;
153
154 Ok(Some((decoded_nostr_url, git_repo))) 154 Ok(Some((decoded_nostr_url, git_repo)))
155} 155}
156 156
diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs
index 7ed98f5..1695e4c 100644
--- a/src/bin/ngit/sub_commands/init.rs
+++ b/src/bin/ngit/sub_commands/init.rs
@@ -1,4 +1,4 @@
1use std::{collections::HashMap, str::FromStr}; 1use std::collections::HashMap;
2 2
3use anyhow::{Context, Result}; 3use anyhow::{Context, Result};
4use console::Style; 4use console::Style;
@@ -443,7 +443,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
443 .map(std::string::ToString::to_string) 443 .map(std::string::ToString::to_string)
444 .collect::<Vec<String>>(); 444 .collect::<Vec<String>>();
445 445
446 prompt_to_set_nostr_url_as_origin(&repo_ref, &git_repo)?; 446 prompt_to_set_nostr_url_as_origin(&repo_ref, &git_repo).await?;
447 447
448 // TODO: if no state event exists and there is currently a remote called 448 // TODO: if no state event exists and there is currently a remote called
449 // "origin", automtically push rather than waiting for the next commit 449 // "origin", automtically push rather than waiting for the next commit
@@ -484,7 +484,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
484 Ok(()) 484 Ok(())
485} 485}
486 486
487fn prompt_to_set_nostr_url_as_origin(repo_ref: &RepoRef, git_repo: &Repo) -> Result<()> { 487async fn prompt_to_set_nostr_url_as_origin(repo_ref: &RepoRef, git_repo: &Repo) -> Result<()> {
488 println!( 488 println!(
489 "starting from your next commit, when you `git push` to a remote that uses your nostr url, it will store your repository state on nostr and update the state of the git server(s) you just listed." 489 "starting from your next commit, when you `git push` to a remote that uses your nostr url, it will store your repository state on nostr and update the state of the git server(s) you just listed."
490 ); 490 );
@@ -494,7 +494,9 @@ fn prompt_to_set_nostr_url_as_origin(repo_ref: &RepoRef, git_repo: &Repo) -> Res
494 494
495 if let Ok(origin_remote) = git_repo.git_repo.find_remote("origin") { 495 if let Ok(origin_remote) = git_repo.git_repo.find_remote("origin") {
496 if let Some(origin_url) = origin_remote.url() { 496 if let Some(origin_url) = origin_remote.url() {
497 if let Ok(nostr_url) = NostrUrlDecoded::from_str(origin_url) { 497 if let Ok(nostr_url) =
498 NostrUrlDecoded::parse_and_resolve(origin_url, &Some(git_repo)).await
499 {
498 if nostr_url.coordinate.identifier == repo_ref.identifier { 500 if nostr_url.coordinate.identifier == repo_ref.identifier {
499 if nostr_url.coordinate.public_key == repo_ref.trusted_maintainer { 501 if nostr_url.coordinate.public_key == repo_ref.trusted_maintainer {
500 return Ok(()); 502 return Ok(());
@@ -521,7 +523,7 @@ fn prompt_to_set_nostr_url_as_origin(repo_ref: &RepoRef, git_repo: &Repo) -> Res
521 } 523 }
522 } 524 }
523 println!("contributors can clone your repository by installing ngit and using this clone url:"); 525 println!("contributors can clone your repository by installing ngit and using this clone url:");
524 println!("{}", repo_ref.to_nostr_git_url()); 526 println!("{}", repo_ref.to_nostr_git_url(&Some(git_repo)));
525 527
526 Ok(()) 528 Ok(())
527} 529}
@@ -534,7 +536,7 @@ fn ask_to_set_origin_remote(repo_ref: &RepoRef, git_repo: &Repo) -> Result<()> {
534 )? { 536 )? {
535 git_repo 537 git_repo
536 .git_repo 538 .git_repo
537 .remote_set_url("origin", &repo_ref.to_nostr_git_url())?; 539 .remote_set_url("origin", &repo_ref.to_nostr_git_url(&Some(git_repo)))?;
538 } 540 }
539 Ok(()) 541 Ok(())
540} 542}
@@ -547,7 +549,7 @@ fn ask_to_create_new_origin_remote(repo_ref: &RepoRef, git_repo: &Repo) -> Resul
547 )? { 549 )? {
548 git_repo 550 git_repo
549 .git_repo 551 .git_repo
550 .remote("origin", &repo_ref.to_nostr_git_url())?; 552 .remote("origin", &repo_ref.to_nostr_git_url(&Some(git_repo)))?;
551 } 553 }
552 Ok(()) 554 Ok(())
553} 555}
diff --git a/src/lib/git/nostr_url.rs b/src/lib/git/nostr_url.rs
index c26bb2e..ca616a0 100644
--- a/src/lib/git/nostr_url.rs
+++ b/src/lib/git/nostr_url.rs
@@ -1,10 +1,12 @@
1use core::fmt; 1use core::fmt;
2use std::str::FromStr; 2use std::{collections::HashMap, str::FromStr};
3 3
4use anyhow::{anyhow, bail, Context, Error, Result}; 4use anyhow::{anyhow, bail, Context, Error, Result};
5use nostr::nips::nip01::Coordinate; 5use nostr::nips::{nip01::Coordinate, nip05};
6use nostr_sdk::{PublicKey, RelayUrl, ToBech32, Url}; 6use nostr_sdk::{PublicKey, RelayUrl, ToBech32, Url};
7 7
8use super::{get_git_config_item, save_git_config_item, Repo};
9
8#[derive(Debug, PartialEq, Default, Clone)] 10#[derive(Debug, PartialEq, Default, Clone)]
9pub enum ServerProtocol { 11pub enum ServerProtocol {
10 Ssh, 12 Ssh,
@@ -59,6 +61,7 @@ pub struct NostrUrlDecoded {
59 pub coordinate: Coordinate, 61 pub coordinate: Coordinate,
60 pub protocol: Option<ServerProtocol>, 62 pub protocol: Option<ServerProtocol>,
61 pub user: Option<String>, 63 pub user: Option<String>,
64 pub nip05: Option<String>,
62} 65}
63 66
64impl fmt::Display for NostrUrlDecoded { 67impl fmt::Display for NostrUrlDecoded {
@@ -89,10 +92,8 @@ impl fmt::Display for NostrUrlDecoded {
89 92
90static INCORRECT_NOSTR_URL_FORMAT_ERROR: &str = "incorrect nostr git url format. try nostr://naddr123 or nostr://npub123/my-repo or nostr://ssh/npub123/relay.damus.io/my-repo"; 93static INCORRECT_NOSTR_URL_FORMAT_ERROR: &str = "incorrect nostr git url format. try nostr://naddr123 or nostr://npub123/my-repo or nostr://ssh/npub123/relay.damus.io/my-repo";
91 94
92impl std::str::FromStr for NostrUrlDecoded { 95impl NostrUrlDecoded {
93 type Err = anyhow::Error; 96 pub async fn parse_and_resolve(url: &str, git_repo: &Option<&Repo>) -> Result<Self> {
94
95 fn from_str(url: &str) -> Result<Self> {
96 let mut protocol = None; 97 let mut protocol = None;
97 let mut user = None; 98 let mut user = None;
98 let mut relays = vec![]; 99 let mut relays = vec![];
@@ -135,7 +136,9 @@ impl std::str::FromStr for NostrUrlDecoded {
135 // extract optional protocol 136 // extract optional protocol
136 if protocol.is_none() { 137 if protocol.is_none() {
137 let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?; 138 let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?;
138 let protocol_str = if let Some(at_index) = part.find('@') { 139 let protocol_str = if part.contains('.') {
140 part
141 } else if let Some(at_index) = part.find('@') {
139 user = Some(part[..at_index].to_string()); 142 user = Some(part[..at_index].to_string());
140 &part[at_index + 1..] 143 &part[at_index + 1..]
141 } else { 144 } else {
@@ -146,7 +149,7 @@ impl std::str::FromStr for NostrUrlDecoded {
146 "https" => Some(ServerProtocol::Https), 149 "https" => Some(ServerProtocol::Https),
147 "http" => Some(ServerProtocol::Http), 150 "http" => Some(ServerProtocol::Http),
148 "git" => Some(ServerProtocol::Git), 151 "git" => Some(ServerProtocol::Git),
149 _ => protocol, 152 _ => None,
150 }; 153 };
151 if protocol.is_some() { 154 if protocol.is_some() {
152 parts.remove(0); 155 parts.remove(0);
@@ -154,6 +157,7 @@ impl std::str::FromStr for NostrUrlDecoded {
154 } 157 }
155 // extract naddr npub/<optional-relays>/identifer 158 // extract naddr npub/<optional-relays>/identifer
156 let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?; 159 let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?;
160 let mut nip05 = None;
157 // naddr used 161 // naddr used
158 let coordinate = if let Ok(coordinate) = Coordinate::parse(part) { 162 let coordinate = if let Ok(coordinate) = Coordinate::parse(part) {
159 if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) { 163 if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) {
@@ -161,8 +165,9 @@ impl std::str::FromStr for NostrUrlDecoded {
161 } else { 165 } else {
162 bail!("naddr doesnt point to a git repository announcement"); 166 bail!("naddr doesnt point to a git repository announcement");
163 } 167 }
164 // npub/<optional-relays>/identifer used 168 // <npub|nip05_address>/<optional-relays>/identifer used
165 } else if let Ok(public_key) = PublicKey::parse(part) { 169 } else {
170 let npub_or_nip05 = part.to_owned();
166 parts.remove(0); 171 parts.remove(0);
167 let identifier = parts 172 let identifier = parts
168 .pop() 173 .pop()
@@ -179,14 +184,46 @@ impl std::str::FromStr for NostrUrlDecoded {
179 RelayUrl::parse(&decoded).context("could not parse relays in nostr git url")?; 184 RelayUrl::parse(&decoded).context("could not parse relays in nostr git url")?;
180 relays.push(url); 185 relays.push(url);
181 } 186 }
187 let public_key = match PublicKey::parse(npub_or_nip05) {
188 Ok(public_key) => public_key,
189 Err(_) => {
190 nip05 = Some(npub_or_nip05.to_string());
191 if let Ok(public_key) =
192 resolve_nip05_from_git_config_cache(npub_or_nip05, git_repo)
193 {
194 public_key
195 } else {
196 let term = console::Term::stderr();
197 let domain = {
198 let s = npub_or_nip05.split('@').collect::<Vec<&str>>();
199 if s.len() == 2 { s[1] } else { s[0] }
200 };
201 term.write_line(&format!("fetching pubic key info from {domain}..."))?;
202 let res = nip05::profile(npub_or_nip05, None)
203 .await
204 .context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?;
205 term.clear_last_lines(1)?;
206 nip05 = Some(npub_or_nip05.to_string());
207 let _ = save_nip05_to_git_config_cache(
208 npub_or_nip05,
209 &res.public_key,
210 git_repo,
211 );
212 if relays.is_empty() {
213 for r in res.relays {
214 relays.push(r);
215 }
216 }
217 res.public_key
218 }
219 }
220 };
182 Coordinate { 221 Coordinate {
183 identifier, 222 identifier,
184 public_key, 223 public_key,
185 kind: nostr_sdk::Kind::GitRepoAnnouncement, 224 kind: nostr_sdk::Kind::GitRepoAnnouncement,
186 relays, 225 relays,
187 } 226 }
188 } else {
189 bail!(INCORRECT_NOSTR_URL_FORMAT_ERROR);
190 }; 227 };
191 228
192 Ok(Self { 229 Ok(Self {
@@ -194,10 +231,62 @@ impl std::str::FromStr for NostrUrlDecoded {
194 coordinate, 231 coordinate,
195 protocol, 232 protocol,
196 user, 233 user,
234 nip05,
197 }) 235 })
198 } 236 }
199} 237}
200 238
239fn resolve_nip05_from_git_config_cache(nip05: &str, git_repo: &Option<&Repo>) -> Result<PublicKey> {
240 if let Some(public_key) = load_nip_cache(git_repo)?.get(nip05) {
241 Ok(*public_key)
242 } else {
243 bail!("nip05 not stored in local git config cache")
244 }
245}
246
247pub fn use_nip05_git_config_cache_to_find_nip05_from_public_key(
248 public_key: &PublicKey,
249 git_repo: &Option<&Repo>,
250) -> Result<Option<String>> {
251 let h = load_nip_cache(git_repo)?;
252 Ok(h.iter()
253 .find_map(|(k, v)| if *v == *public_key { Some(k) } else { None })
254 .cloned())
255}
256
257fn save_nip05_to_git_config_cache(
258 nip05: &str,
259 public_key: &PublicKey,
260 git_repo: &Option<&Repo>,
261) -> Result<()> {
262 let mut h = load_nip_cache(git_repo)?;
263 h.insert(nip05.to_string(), *public_key);
264
265 let s = h
266 .into_iter()
267 .map(|(nip05, public_key)| format!("{nip05}:{}", public_key.to_hex()))
268 .collect::<Vec<String>>()
269 .join(",");
270
271 save_git_config_item(git_repo, "nostr.nip05", s.as_str())
272 .context("could not save nip05 cache in git config")
273}
274
275fn load_nip_cache(git_repo: &Option<&Repo>) -> Result<HashMap<String, PublicKey>> {
276 let mut h = HashMap::new();
277 let stored_value = get_git_config_item(git_repo, "nostr.nip05")?
278 .context("no nip05s in local git config cache so retun empty cache")
279 .unwrap_or_default();
280 for pair in stored_value.split(',') {
281 if let Some((cached_nip05, pubkey)) = pair.split_once(':') {
282 if let Ok(public_key) = PublicKey::parse(pubkey) {
283 h.insert(cached_nip05.to_string(), public_key);
284 }
285 }
286 }
287 Ok(h)
288}
289
201#[derive(Debug, PartialEq, Default)] 290#[derive(Debug, PartialEq, Default)]
202pub struct CloneUrl { 291pub struct CloneUrl {
203 original_string: String, 292 original_string: String,
@@ -887,6 +976,7 @@ mod tests {
887 }, 976 },
888 protocol: None, 977 protocol: None,
889 user: None, 978 user: None,
979 nip05: None,
890 } 980 }
891 ), 981 ),
892 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit", 982 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit",
@@ -912,6 +1002,7 @@ mod tests {
912 }, 1002 },
913 protocol: None, 1003 protocol: None,
914 user: None, 1004 user: None,
1005 nip05: None,
915 } 1006 }
916 ), 1007 ),
917 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit", 1008 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit",
@@ -937,6 +1028,7 @@ mod tests {
937 }, 1028 },
938 protocol: Some(ServerProtocol::Ssh), 1029 protocol: Some(ServerProtocol::Ssh),
939 user: None, 1030 user: None,
1031 nip05: None,
940 } 1032 }
941 ), 1033 ),
942 "nostr://ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit", 1034 "nostr://ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit",
@@ -962,6 +1054,7 @@ mod tests {
962 }, 1054 },
963 protocol: Some(ServerProtocol::Ssh), 1055 protocol: Some(ServerProtocol::Ssh),
964 user: Some("bla".to_string()), 1056 user: Some("bla".to_string()),
1057 nip05: None,
965 } 1058 }
966 ), 1059 ),
967 "nostr://bla@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit", 1060 "nostr://bla@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit",
@@ -971,8 +1064,6 @@ mod tests {
971 } 1064 }
972 1065
973 mod nostr_url_decoded_paramemters_from_str { 1066 mod nostr_url_decoded_paramemters_from_str {
974 use std::str::FromStr;
975
976 use super::*; 1067 use super::*;
977 1068
978 fn get_model_coordinate(relays: bool) -> Coordinate { 1069 fn get_model_coordinate(relays: bool) -> Coordinate {
@@ -991,11 +1082,11 @@ mod tests {
991 } 1082 }
992 } 1083 }
993 1084
994 #[test] 1085 #[tokio::test]
995 fn from_naddr() -> Result<()> { 1086 async fn from_naddr() -> Result<()> {
996 let url = "nostr://naddr1qqzxuemfwsqs6amnwvaz7tmwdaejumr0dspzpgqgmmc409hm4xsdd74sf68a2uyf9pwel4g9mfdg8l5244t6x4jdqvzqqqrhnym0k2qj".to_string(); 1087 let url = "nostr://naddr1qqzxuemfwsqs6amnwvaz7tmwdaejumr0dspzpgqgmmc409hm4xsdd74sf68a2uyf9pwel4g9mfdg8l5244t6x4jdqvzqqqrhnym0k2qj".to_string();
997 assert_eq!( 1088 assert_eq!(
998 NostrUrlDecoded::from_str(&url)?, 1089 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
999 NostrUrlDecoded { 1090 NostrUrlDecoded {
1000 original_string: url.clone(), 1091 original_string: url.clone(),
1001 coordinate: Coordinate { 1092 coordinate: Coordinate {
@@ -1010,6 +1101,7 @@ mod tests {
1010 }, 1101 },
1011 protocol: None, 1102 protocol: None,
1012 user: None, 1103 user: None,
1104 nip05: None,
1013 }, 1105 },
1014 ); 1106 );
1015 Ok(()) 1107 Ok(())
@@ -1018,18 +1110,19 @@ mod tests {
1018 mod from_npub_slash_identifier { 1110 mod from_npub_slash_identifier {
1019 use super::*; 1111 use super::*;
1020 1112
1021 #[test] 1113 #[tokio::test]
1022 fn without_relay() -> Result<()> { 1114 async fn without_relay() -> Result<()> {
1023 let url = 1115 let url =
1024 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit" 1116 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit"
1025 .to_string(); 1117 .to_string();
1026 assert_eq!( 1118 assert_eq!(
1027 NostrUrlDecoded::from_str(&url)?, 1119 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
1028 NostrUrlDecoded { 1120 NostrUrlDecoded {
1029 original_string: url.clone(), 1121 original_string: url.clone(),
1030 coordinate: get_model_coordinate(false), 1122 coordinate: get_model_coordinate(false),
1031 protocol: None, 1123 protocol: None,
1032 user: None, 1124 user: None,
1125 nip05: None,
1033 }, 1126 },
1034 ); 1127 );
1035 Ok(()) 1128 Ok(())
@@ -1038,48 +1131,50 @@ mod tests {
1038 mod with_url_parameters { 1131 mod with_url_parameters {
1039 use super::*; 1132 use super::*;
1040 1133
1041 #[test] 1134 #[tokio::test]
1042 fn with_relay_without_scheme_defaults_to_wss() -> Result<()> { 1135 async fn with_relay_without_scheme_defaults_to_wss() -> Result<()> {
1043 let url = "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay=nos.lol".to_string(); 1136 let url = "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay=nos.lol".to_string();
1044 assert_eq!( 1137 assert_eq!(
1045 NostrUrlDecoded::from_str(&url)?, 1138 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
1046 NostrUrlDecoded { 1139 NostrUrlDecoded {
1047 original_string: url.clone(), 1140 original_string: url.clone(),
1048 coordinate: get_model_coordinate(true), 1141 coordinate: get_model_coordinate(true),
1049 protocol: None, 1142 protocol: None,
1050 user: None, 1143 user: None,
1144 nip05: None,
1051 }, 1145 },
1052 ); 1146 );
1053 Ok(()) 1147 Ok(())
1054 } 1148 }
1055 1149
1056 #[test] 1150 #[tokio::test]
1057 fn with_encoded_relay() -> Result<()> { 1151 async fn with_encoded_relay() -> Result<()> {
1058 let url = format!( 1152 let url = format!(
1059 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}", 1153 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}",
1060 urlencoding::encode("wss://nos.lol") 1154 urlencoding::encode("wss://nos.lol")
1061 ); 1155 );
1062 assert_eq!( 1156 assert_eq!(
1063 NostrUrlDecoded::from_str(&url)?, 1157 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
1064 NostrUrlDecoded { 1158 NostrUrlDecoded {
1065 original_string: url.clone(), 1159 original_string: url.clone(),
1066 coordinate: get_model_coordinate(true), 1160 coordinate: get_model_coordinate(true),
1067 protocol: None, 1161 protocol: None,
1068 user: None, 1162 user: None,
1163 nip05: None,
1069 }, 1164 },
1070 ); 1165 );
1071 Ok(()) 1166 Ok(())
1072 } 1167 }
1073 1168
1074 #[test] 1169 #[tokio::test]
1075 fn with_multiple_encoded_relays() -> Result<()> { 1170 async fn with_multiple_encoded_relays() -> Result<()> {
1076 let url = format!( 1171 let url = format!(
1077 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}&relay1={}", 1172 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}&relay1={}",
1078 urlencoding::encode("wss://nos.lol/"), 1173 urlencoding::encode("wss://nos.lol/"),
1079 urlencoding::encode("wss://relay.damus.io/"), 1174 urlencoding::encode("wss://relay.damus.io/"),
1080 ); 1175 );
1081 assert_eq!( 1176 assert_eq!(
1082 NostrUrlDecoded::from_str(&url)?, 1177 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
1083 NostrUrlDecoded { 1178 NostrUrlDecoded {
1084 original_string: url.clone(), 1179 original_string: url.clone(),
1085 coordinate: Coordinate { 1180 coordinate: Coordinate {
@@ -1096,36 +1191,39 @@ mod tests {
1096 }, 1191 },
1097 protocol: None, 1192 protocol: None,
1098 user: None, 1193 user: None,
1194 nip05: None,
1099 }, 1195 },
1100 ); 1196 );
1101 Ok(()) 1197 Ok(())
1102 } 1198 }
1103 1199
1104 #[test] 1200 #[tokio::test]
1105 fn with_server_protocol() -> Result<()> { 1201 async fn with_server_protocol() -> Result<()> {
1106 let url = "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?protocol=ssh".to_string(); 1202 let url = "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?protocol=ssh".to_string();
1107 assert_eq!( 1203 assert_eq!(
1108 NostrUrlDecoded::from_str(&url)?, 1204 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
1109 NostrUrlDecoded { 1205 NostrUrlDecoded {
1110 original_string: url.clone(), 1206 original_string: url.clone(),
1111 coordinate: get_model_coordinate(false), 1207 coordinate: get_model_coordinate(false),
1112 protocol: Some(ServerProtocol::Ssh), 1208 protocol: Some(ServerProtocol::Ssh),
1113 user: None, 1209 user: None,
1210 nip05: None,
1114 }, 1211 },
1115 ); 1212 );
1116 Ok(()) 1213 Ok(())
1117 } 1214 }
1118 1215
1119 #[test] 1216 #[tokio::test]
1120 fn with_server_protocol_and_user() -> Result<()> { 1217 async fn with_server_protocol_and_user() -> Result<()> {
1121 let url = "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?protocol=ssh&user=fred".to_string(); 1218 let url = "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?protocol=ssh&user=fred".to_string();
1122 assert_eq!( 1219 assert_eq!(
1123 NostrUrlDecoded::from_str(&url)?, 1220 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
1124 NostrUrlDecoded { 1221 NostrUrlDecoded {
1125 original_string: url.clone(), 1222 original_string: url.clone(),
1126 coordinate: get_model_coordinate(false), 1223 coordinate: get_model_coordinate(false),
1127 protocol: Some(ServerProtocol::Ssh), 1224 protocol: Some(ServerProtocol::Ssh),
1128 user: Some("fred".to_string()), 1225 user: Some("fred".to_string()),
1226 nip05: None,
1129 }, 1227 },
1130 ); 1228 );
1131 Ok(()) 1229 Ok(())
@@ -1135,48 +1233,50 @@ mod tests {
1135 mod with_parameters_embedded_with_slashes { 1233 mod with_parameters_embedded_with_slashes {
1136 use super::*; 1234 use super::*;
1137 1235
1138 #[test] 1236 #[tokio::test]
1139 fn with_relay_without_scheme_defaults_to_wss() -> Result<()> { 1237 async fn with_relay_without_scheme_defaults_to_wss() -> Result<()> {
1140 let url = "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit".to_string(); 1238 let url = "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit".to_string();
1141 assert_eq!( 1239 assert_eq!(
1142 NostrUrlDecoded::from_str(&url)?, 1240 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
1143 NostrUrlDecoded { 1241 NostrUrlDecoded {
1144 original_string: url.clone(), 1242 original_string: url.clone(),
1145 coordinate: get_model_coordinate(true), 1243 coordinate: get_model_coordinate(true),
1146 protocol: None, 1244 protocol: None,
1147 user: None, 1245 user: None,
1246 nip05: None,
1148 }, 1247 },
1149 ); 1248 );
1150 Ok(()) 1249 Ok(())
1151 } 1250 }
1152 1251
1153 #[test] 1252 #[tokio::test]
1154 fn with_encoded_relay() -> Result<()> { 1253 async fn with_encoded_relay() -> Result<()> {
1155 let url = format!( 1254 let url = format!(
1156 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/{}/ngit", 1255 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/{}/ngit",
1157 urlencoding::encode("wss://nos.lol") 1256 urlencoding::encode("wss://nos.lol")
1158 ); 1257 );
1159 assert_eq!( 1258 assert_eq!(
1160 NostrUrlDecoded::from_str(&url)?, 1259 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
1161 NostrUrlDecoded { 1260 NostrUrlDecoded {
1162 original_string: url.clone(), 1261 original_string: url.clone(),
1163 coordinate: get_model_coordinate(true), 1262 coordinate: get_model_coordinate(true),
1164 protocol: None, 1263 protocol: None,
1165 user: None, 1264 user: None,
1265 nip05: None,
1166 }, 1266 },
1167 ); 1267 );
1168 Ok(()) 1268 Ok(())
1169 } 1269 }
1170 1270
1171 #[test] 1271 #[tokio::test]
1172 fn with_multiple_encoded_relays() -> Result<()> { 1272 async fn with_multiple_encoded_relays() -> Result<()> {
1173 let url = format!( 1273 let url = format!(
1174 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/{}/{}/ngit", 1274 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/{}/{}/ngit",
1175 urlencoding::encode("wss://nos.lol/"), 1275 urlencoding::encode("wss://nos.lol/"),
1176 urlencoding::encode("wss://relay.damus.io/"), 1276 urlencoding::encode("wss://relay.damus.io/"),
1177 ); 1277 );
1178 assert_eq!( 1278 assert_eq!(
1179 NostrUrlDecoded::from_str(&url)?, 1279 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
1180 NostrUrlDecoded { 1280 NostrUrlDecoded {
1181 original_string: url.clone(), 1281 original_string: url.clone(),
1182 coordinate: Coordinate { 1282 coordinate: Coordinate {
@@ -1193,36 +1293,39 @@ mod tests {
1193 }, 1293 },
1194 protocol: None, 1294 protocol: None,
1195 user: None, 1295 user: None,
1296 nip05: None,
1196 }, 1297 },
1197 ); 1298 );
1198 Ok(()) 1299 Ok(())
1199 } 1300 }
1200 1301
1201 #[test] 1302 #[tokio::test]
1202 fn with_server_protocol() -> Result<()> { 1303 async fn with_server_protocol() -> Result<()> {
1203 let url = "nostr://ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit".to_string(); 1304 let url = "nostr://ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit".to_string();
1204 assert_eq!( 1305 assert_eq!(
1205 NostrUrlDecoded::from_str(&url)?, 1306 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
1206 NostrUrlDecoded { 1307 NostrUrlDecoded {
1207 original_string: url.clone(), 1308 original_string: url.clone(),
1208 coordinate: get_model_coordinate(false), 1309 coordinate: get_model_coordinate(false),
1209 protocol: Some(ServerProtocol::Ssh), 1310 protocol: Some(ServerProtocol::Ssh),
1210 user: None, 1311 user: None,
1312 nip05: None,
1211 }, 1313 },
1212 ); 1314 );
1213 Ok(()) 1315 Ok(())
1214 } 1316 }
1215 1317
1216 #[test] 1318 #[tokio::test]
1217 fn with_server_protocol_and_user() -> Result<()> { 1319 async fn with_server_protocol_and_user() -> Result<()> {
1218 let url = "nostr://fred@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit".to_string(); 1320 let url = "nostr://fred@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit".to_string();
1219 assert_eq!( 1321 assert_eq!(
1220 NostrUrlDecoded::from_str(&url)?, 1322 NostrUrlDecoded::parse_and_resolve(&url, &None).await?,
1221 NostrUrlDecoded { 1323 NostrUrlDecoded {
1222 original_string: url.clone(), 1324 original_string: url.clone(),
1223 coordinate: get_model_coordinate(false), 1325 coordinate: get_model_coordinate(false),
1224 protocol: Some(ServerProtocol::Ssh), 1326 protocol: Some(ServerProtocol::Ssh),
1225 user: Some("fred".to_string()), 1327 user: Some("fred".to_string()),
1328 nip05: None,
1226 }, 1329 },
1227 ); 1330 );
1228 Ok(()) 1331 Ok(())
diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs
index da76182..5d6f4eb 100644
--- a/src/lib/repo_ref.rs
+++ b/src/lib/repo_ref.rs
@@ -19,7 +19,10 @@ use crate::{
19 Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms, PromptInputParms, 19 Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms, PromptInputParms,
20 }, 20 },
21 client::{consolidate_fetch_reports, get_repo_ref_from_cache, sign_event, Connect}, 21 client::{consolidate_fetch_reports, get_repo_ref_from_cache, sign_event, Connect},
22 git::{nostr_url::NostrUrlDecoded, Repo, RepoActions}, 22 git::{
23 nostr_url::{use_nip05_git_config_cache_to_find_nip05_from_public_key, NostrUrlDecoded},
24 Repo, RepoActions,
25 },
23 login::user::get_user_details, 26 login::user::get_user_details,
24}; 27};
25 28
@@ -41,6 +44,7 @@ impl TryFrom<(nostr::Event, Option<PublicKey>)> for RepoRef {
41 type Error = anyhow::Error; 44 type Error = anyhow::Error;
42 45
43 fn try_from((event, trusted_maintainer): (nostr::Event, Option<PublicKey>)) -> Result<Self> { 46 fn try_from((event, trusted_maintainer): (nostr::Event, Option<PublicKey>)) -> Result<Self> {
47 // TODO: turn trusted maintainer into NostrUrlDecoded
44 if !event.kind.eq(&Kind::GitRepoAnnouncement) { 48 if !event.kind.eq(&Kind::GitRepoAnnouncement) {
45 bail!("incorrect kind"); 49 bail!("incorrect kind");
46 } 50 }
@@ -231,12 +235,18 @@ impl RepoRef {
231 .collect::<Vec<(Coordinate, Option<Timestamp>)>>() 235 .collect::<Vec<(Coordinate, Option<Timestamp>)>>()
232 } 236 }
233 237
234 pub fn to_nostr_git_url(&self) -> String { 238 pub fn to_nostr_git_url(&self, git_repo: &Option<&Repo>) -> String {
239 let c = self.coordinate_with_hint();
235 format!( 240 format!(
236 "{}", 241 "{}",
237 NostrUrlDecoded { 242 NostrUrlDecoded {
238 original_string: String::new(), 243 original_string: String::new(),
239 coordinate: self.coordinate_with_hint(), 244 nip05: use_nip05_git_config_cache_to_find_nip05_from_public_key(
245 &c.public_key,
246 git_repo,
247 )
248 .unwrap_or_default(),
249 coordinate: c,
240 protocol: None, 250 protocol: None,
241 user: None, 251 user: None,
242 } 252 }
@@ -259,7 +269,7 @@ pub async fn get_repo_coordinates_when_remote_unknown(
259pub async fn try_and_get_repo_coordinates_when_remote_unknown( 269pub async fn try_and_get_repo_coordinates_when_remote_unknown(
260 git_repo: &Repo, 270 git_repo: &Repo,
261) -> Result<Coordinate> { 271) -> Result<Coordinate> {
262 let remote_coordinates = get_repo_coordinates_from_nostr_remotes(git_repo)?; 272 let remote_coordinates = get_repo_coordinates_from_nostr_remotes(git_repo).await?;
263 if remote_coordinates.is_empty() { 273 if remote_coordinates.is_empty() {
264 if let Ok(c) = get_repo_coordinates_from_git_config(git_repo) { 274 if let Ok(c) = get_repo_coordinates_from_git_config(git_repo) {
265 Ok(c) 275 Ok(c)
@@ -327,11 +337,15 @@ fn get_repo_coordinates_from_git_config(git_repo: &Repo) -> Result<Coordinate> {
327 .context("git config item \"nostr.repo\" is not an naddr") 337 .context("git config item \"nostr.repo\" is not an naddr")
328} 338}
329 339
330fn get_repo_coordinates_from_nostr_remotes(git_repo: &Repo) -> Result<HashMap<String, Coordinate>> { 340async fn get_repo_coordinates_from_nostr_remotes(
341 git_repo: &Repo,
342) -> Result<HashMap<String, Coordinate>> {
331 let mut repo_coordinates = HashMap::new(); 343 let mut repo_coordinates = HashMap::new();
332 for remote_name in git_repo.git_repo.remotes()?.iter().flatten() { 344 for remote_name in git_repo.git_repo.remotes()?.iter().flatten() {
333 if let Some(remote_url) = git_repo.git_repo.find_remote(remote_name)?.url() { 345 if let Some(remote_url) = git_repo.git_repo.find_remote(remote_name)?.url() {
334 if let Ok(nostr_url_decoded) = NostrUrlDecoded::from_str(remote_url) { 346 if let Ok(nostr_url_decoded) =
347 NostrUrlDecoded::parse_and_resolve(remote_url, &Some(git_repo)).await
348 {
335 repo_coordinates.insert(remote_name.to_string(), nostr_url_decoded.coordinate); 349 repo_coordinates.insert(remote_name.to_string(), nostr_url_decoded.coordinate);
336 } 350 }
337 } 351 }
@@ -383,7 +397,9 @@ async fn get_repo_coordinate_from_user_prompt(
383 .input(PromptInputParms::default().with_prompt("nostr repository"))?; 397 .input(PromptInputParms::default().with_prompt("nostr repository"))?;
384 let coordinate = if let Ok(c) = Coordinate::parse(&input) { 398 let coordinate = if let Ok(c) = Coordinate::parse(&input) {
385 c 399 c
386 } else if let Ok(nostr_url) = NostrUrlDecoded::from_str(&input) { 400 } else if let Ok(nostr_url) =
401 NostrUrlDecoded::parse_and_resolve(&input, &Some(git_repo)).await
402 {
387 nostr_url.coordinate 403 nostr_url.coordinate
388 } else { 404 } else {
389 eprintln!("not a valid naddr or git nostr remote URL starting nostr://"); 405 eprintln!("not a valid naddr or git nostr remote URL starting nostr://");
@@ -438,10 +454,10 @@ fn set_or_create_git_remote_with_nostr_url(
438 repo_ref: &RepoRef, 454 repo_ref: &RepoRef,
439 git_repo: &Repo, 455 git_repo: &Repo,
440) -> Result<()> { 456) -> Result<()> {
441 let url = repo_ref.to_nostr_git_url(); 457 let url = repo_ref.to_nostr_git_url(&Some(git_repo));
442 if git_repo 458 if git_repo
443 .git_repo 459 .git_repo
444 .remote_set_url(name, &repo_ref.to_nostr_git_url()) 460 .remote_set_url(name, &repo_ref.to_nostr_git_url(&Some(git_repo)))
445 .is_err() 461 .is_err()
446 { 462 {
447 git_repo.git_repo.remote(name, &url)?; 463 git_repo.git_repo.remote(name, &url)?;