From a825311f2c55661aaab3a163bda9109295c96044 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Tue, 3 Sep 2024 15:30:37 +0100 Subject: feat(remote): enhance nostr url format add protocol and user parameters so that users can overide the protcol in the clone url and use specific protocols for fetch and push. see: nostr:nevent1qvzqqqqqqypzpgqgmmc409hm4xsdd74sf68a2uyf9pwel4g9mfdg8l5244t6x4jdqyxhwumn8ghj7mn0wvhxcmmvqqsp6a5ck6grd9lq0nu25dcfzggxde67erut76w0ucal5rcfq4y5gzc7gmpzm the override feature hasn't been implemented yet but this is an enabler. also added a new format so that macos (zsh) users don't have to use quotes: nostr:///npub123//identifer --- src/git.rs | 145 ++++++++++++++++++------- src/git_remote_helper.rs | 267 ++++++++++++++++++++++++++++++++++++----------- src/repo_ref.rs | 6 +- 3 files changed, 318 insertions(+), 100 deletions(-) (limited to 'src') diff --git a/src/git.rs b/src/git.rs index 42ff587..5919667 100644 --- a/src/git.rs +++ b/src/git.rs @@ -835,55 +835,128 @@ fn extract_sig_from_patch_tags<'a>( .context("failed to create git signature") } -pub fn nostr_git_url_to_repo_coordinates(url: &str) -> Result> { - let mut repo_coordinattes = HashSet::new(); - let url = Url::parse(url)?; +#[derive(Debug, PartialEq)] +pub enum ServerProtocol { + Ssh, + Https, + Http, + Git, +} - if url.scheme().ne("nostr") { - bail!("nostr git url must start with nostr://") - } +#[derive(Debug, PartialEq)] +pub struct NostrUrlDecoded { + pub coordinates: HashSet, + pub protocol: Option, + pub user: Option, +} - if let Ok(coordinate) = Coordinate::parse(url.domain().context("no naddr")?) { - if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) { - repo_coordinattes.insert(coordinate); - return Ok(repo_coordinattes); +static 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"; + +impl NostrUrlDecoded { + pub fn from_str(url: &str) -> Result { + let mut coordinates = HashSet::new(); + let mut protocol = None; + let mut user = None; + let mut relays = vec![]; + + if !url.starts_with("nostr://") { + bail!("nostr git url must start with nostr://"); + } + // process get url parameters if present + for (name, value) in Url::parse(url)?.query_pairs() { + if name.contains("relay") { + let mut decoded = urlencoding::decode(&value) + .context("could not parse relays in nostr git url")? + .to_string(); + if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") { + decoded = format!("wss://{decoded}"); + } + let url = + Url::parse(&decoded).context("could not parse relays in nostr git url")?; + relays.push(url.to_string()); + } else if name == "protocol" { + protocol = match value.as_ref() { + "ssh" => Some(ServerProtocol::Ssh), + "https" => Some(ServerProtocol::Https), + "http" => Some(ServerProtocol::Http), + "git" => Some(ServerProtocol::Git), + _ => None, + }; + } else if name == "user" { + user = Some(value.to_string()); + } } - bail!("naddr doesnt point to a git repository announcement"); - } - if let Some(domain) = url.domain() { - if let Ok(public_key) = PublicKey::parse(domain) { - if url.path().len() < 2 { - bail!( - "nostr git url should include the repo identifier eg nostr://npub123/the-repo-identifer" - ); + let mut parts: Vec<&str> = url[8..] + .split('?') + .next() + .unwrap_or("") + .split('/') + .collect(); + + // extract optional protocol + if protocol.is_none() { + let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?; + let protocol_str = if let Some(at_index) = part.find('@') { + user = Some(part[..at_index].to_string()); + &part[at_index + 1..] + } else { + part + }; + protocol = match protocol_str { + "ssh" => Some(ServerProtocol::Ssh), + "https" => Some(ServerProtocol::Https), + "http" => Some(ServerProtocol::Http), + "git" => Some(ServerProtocol::Git), + _ => protocol, + }; + if protocol.is_some() { + parts.remove(0); } - let mut relays = vec![]; - for (name, value) in url.query_pairs() { - if name.contains("relay") { - let mut decoded = urlencoding::decode(&value) - .context("could not parse relays in nostr git url")? - .to_string(); - if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") { - decoded = format!("wss://{decoded}"); - } - let url = - Url::parse(&decoded).context("could not parse relays in nostr git url")?; - relays.push(url.to_string()); + } + // extract naddr npub//identifer + let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?; + // naddr used + if let Ok(coordinate) = Coordinate::parse(part) { + if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) { + coordinates.insert(coordinate); + } else { + bail!("naddr doesnt point to a git repository announcement"); + } + // npub//identifer used + } else if let Ok(public_key) = PublicKey::parse(part) { + parts.remove(0); + let identifier = parts + .pop() + .context("nostr url must have an identifier eg. nostr://npub123/repo-identifier")? + .to_string(); + for relay in parts { + let mut decoded = urlencoding::decode(relay) + .context("could not parse relays in nostr git url")? + .to_string(); + if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") { + decoded = format!("wss://{decoded}"); } + let url = + Url::parse(&decoded).context("could not parse relays in nostr git url")?; + relays.push(url.to_string()); } - repo_coordinattes.insert(Coordinate { - identifier: url.path()[1..].to_string(), + coordinates.insert(Coordinate { + identifier, public_key, kind: nostr_sdk::Kind::GitRepoAnnouncement, relays, }); - return Ok(repo_coordinattes); + } else { + bail!(INCORRECT_NOSTR_URL_FORMAT_ERROR); } + + Ok(Self { + coordinates, + protocol, + user, + }) } - bail!( - "nostr git url must be in format nostr://naddr123 or nostr://npub123/identifer?relay=wss://relay-example.com&relay1=wss://relay-example.org" - ); } /** produce error when using local repo or custom protocols */ diff --git a/src/git_remote_helper.rs b/src/git_remote_helper.rs index d00ad0e..a5244bf 100644 --- a/src/git_remote_helper.rs +++ b/src/git_remote_helper.rs @@ -19,7 +19,7 @@ use client::{ get_state_from_cache, sign_event, Connect, STATE_KIND, }; use console::Term; -use git::{nostr_git_url_to_repo_coordinates, sha1_to_oid, RepoActions}; +use git::{sha1_to_oid, NostrUrlDecoded, RepoActions}; use git2::{Oid, Repository}; use nostr::nips::{nip01::Coordinate, nip10::Marker}; use nostr_sdk::{ @@ -81,12 +81,12 @@ async fn main() -> Result<()> { #[cfg(test)] let client = ::default(); - let repo_coordinates = - nostr_git_url_to_repo_coordinates(nostr_remote_url).context("invalid nostr url")?; + let decoded_nostr_url = + NostrUrlDecoded::from_str(nostr_remote_url).context("invalid nostr url")?; - fetching_with_report_for_helper(git_repo_path, &client, &repo_coordinates).await?; + fetching_with_report_for_helper(git_repo_path, &client, &decoded_nostr_url.coordinates).await?; - let repo_ref = get_repo_ref_from_cache(git_repo_path, &repo_coordinates).await?; + let repo_ref = get_repo_ref_from_cache(git_repo_path, &decoded_nostr_url.coordinates).await?; let stdin = io::stdin(); let mut line = String::new(); @@ -1639,7 +1639,8 @@ impl RepoState { mod tests { use super::*; - mod nostr_git_url_to_repo_coordinates { + mod nostr_git_url_paramemters_from_str { + use git::ServerProtocol; use nostr_sdk::PublicKey; use super::*; @@ -1663,79 +1664,223 @@ mod tests { #[test] fn from_naddr() -> Result<()> { assert_eq!( - nostr_git_url_to_repo_coordinates( + NostrUrlDecoded::from_str( "nostr://naddr1qqzxuemfwsqs6amnwvaz7tmwdaejumr0dspzpgqgmmc409hm4xsdd74sf68a2uyf9pwel4g9mfdg8l5244t6x4jdqvzqqqrhnym0k2qj" )?, - HashSet::from([Coordinate { - identifier: "ngit".to_string(), - public_key: PublicKey::parse( - "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", - ) - .unwrap(), - kind: nostr_sdk::Kind::GitRepoAnnouncement, - relays: vec!["wss://nos.lol".to_string()], // wont add the slash - }]), + NostrUrlDecoded { + coordinates: HashSet::from([Coordinate { + identifier: "ngit".to_string(), + public_key: PublicKey::parse( + "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", + ) + .unwrap(), + kind: nostr_sdk::Kind::GitRepoAnnouncement, + relays: vec!["wss://nos.lol".to_string()], // wont add the slash + }]), + protocol: None, + user: None, + }, ); Ok(()) } - mod from_npub_slah_identifier { + mod from_npub_slash_identifier { use super::*; #[test] fn without_relay() -> Result<()> { assert_eq!( - nostr_git_url_to_repo_coordinates( + NostrUrlDecoded::from_str( "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit" )?, - HashSet::from([get_model_coordinate(false)]), + NostrUrlDecoded { + coordinates: HashSet::from([get_model_coordinate(false)]), + protocol: None, + user: None, + }, ); Ok(()) } - #[test] - fn with_relay_without_scheme_defaults_to_wss() -> Result<()> { - assert_eq!( - nostr_git_url_to_repo_coordinates( - "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay=nos.lol" - )?, - HashSet::from([get_model_coordinate(true)]), - ); - Ok(()) - } + mod with_url_parameters { + + use super::*; + + #[test] + fn with_relay_without_scheme_defaults_to_wss() -> Result<()> { + assert_eq!( + NostrUrlDecoded::from_str( + "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay=nos.lol" + )?, + NostrUrlDecoded { + coordinates: HashSet::from([get_model_coordinate(true)]), + protocol: None, + user: None, + }, + ); + Ok(()) + } - #[test] - fn with_encoded_relay() -> Result<()> { - assert_eq!( - nostr_git_url_to_repo_coordinates(&format!( - "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}", - urlencoding::encode("wss://nos.lol") - ))?, - HashSet::from([get_model_coordinate(true)]), - ); - Ok(()) + #[test] + fn with_encoded_relay() -> Result<()> { + assert_eq!( + NostrUrlDecoded::from_str(&format!( + "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}", + urlencoding::encode("wss://nos.lol") + ))?, + NostrUrlDecoded { + coordinates: HashSet::from([get_model_coordinate(true)]), + protocol: None, + user: None, + }, + ); + Ok(()) + } + #[test] + fn with_multiple_encoded_relays() -> Result<()> { + assert_eq!( + NostrUrlDecoded::from_str(&format!( + "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}&relay1={}", + urlencoding::encode("wss://nos.lol"), + urlencoding::encode("wss://relay.damus.io"), + ))?, + NostrUrlDecoded { + coordinates: HashSet::from([Coordinate { + identifier: "ngit".to_string(), + public_key: PublicKey::parse( + "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", + ) + .unwrap(), + kind: nostr_sdk::Kind::GitRepoAnnouncement, + relays: vec![ + "wss://nos.lol/".to_string(), + "wss://relay.damus.io/".to_string(), + ], + }]), + protocol: None, + user: None, + }, + ); + Ok(()) + } + + #[test] + fn with_server_protocol() -> Result<()> { + assert_eq!( + NostrUrlDecoded::from_str( + "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?protocol=ssh" + )?, + NostrUrlDecoded { + coordinates: HashSet::from([get_model_coordinate(false)]), + protocol: Some(ServerProtocol::Ssh), + user: None, + }, + ); + Ok(()) + } + #[test] + fn with_server_protocol_and_user() -> Result<()> { + assert_eq!( + NostrUrlDecoded::from_str( + "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?protocol=ssh&user=fred" + )?, + NostrUrlDecoded { + coordinates: HashSet::from([get_model_coordinate(false)]), + protocol: Some(ServerProtocol::Ssh), + user: Some("fred".to_string()), + }, + ); + Ok(()) + } } - #[test] - fn with_multiple_encoded_relays() -> Result<()> { - assert_eq!( - nostr_git_url_to_repo_coordinates(&format!( - "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}&relay1={}", - urlencoding::encode("wss://nos.lol"), - urlencoding::encode("wss://relay.damus.io"), - ))?, - HashSet::from([Coordinate { - identifier: "ngit".to_string(), - public_key: PublicKey::parse( - "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", - ) - .unwrap(), - kind: nostr_sdk::Kind::GitRepoAnnouncement, - relays: vec![ - "wss://nos.lol/".to_string(), - "wss://relay.damus.io/".to_string(), - ], - }]), - ); - Ok(()) + mod with_parameters_embedded_with_slashes { + use super::*; + + #[test] + fn with_relay_without_scheme_defaults_to_wss() -> Result<()> { + assert_eq!( + NostrUrlDecoded::from_str( + "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit" + )?, + NostrUrlDecoded { + coordinates: HashSet::from([get_model_coordinate(true)]), + protocol: None, + user: None, + }, + ); + Ok(()) + } + + #[test] + fn with_encoded_relay() -> Result<()> { + assert_eq!( + NostrUrlDecoded::from_str(&format!( + "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/{}/ngit", + urlencoding::encode("wss://nos.lol") + ))?, + NostrUrlDecoded { + coordinates: HashSet::from([get_model_coordinate(true)]), + protocol: None, + user: None, + }, + ); + Ok(()) + } + #[test] + fn with_multiple_encoded_relays() -> Result<()> { + assert_eq!( + NostrUrlDecoded::from_str(&format!( + "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/{}/{}/ngit", + urlencoding::encode("wss://nos.lol"), + urlencoding::encode("wss://relay.damus.io"), + ))?, + NostrUrlDecoded { + coordinates: HashSet::from([Coordinate { + identifier: "ngit".to_string(), + public_key: PublicKey::parse( + "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", + ) + .unwrap(), + kind: nostr_sdk::Kind::GitRepoAnnouncement, + relays: vec![ + "wss://nos.lol/".to_string(), + "wss://relay.damus.io/".to_string(), + ], + }]), + protocol: None, + user: None, + }, + ); + Ok(()) + } + + #[test] + fn with_server_protocol() -> Result<()> { + assert_eq!( + NostrUrlDecoded::from_str( + "nostr://ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit" + )?, + NostrUrlDecoded { + coordinates: HashSet::from([get_model_coordinate(false)]), + protocol: Some(ServerProtocol::Ssh), + user: None, + }, + ); + Ok(()) + } + #[test] + fn with_server_protocol_and_user() -> Result<()> { + assert_eq!( + NostrUrlDecoded::from_str( + "nostr://fred@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit" + )?, + NostrUrlDecoded { + coordinates: HashSet::from([get_model_coordinate(false)]), + protocol: Some(ServerProtocol::Ssh), + user: Some("fred".to_string()), + }, + ); + Ok(()) + } } } } diff --git a/src/repo_ref.rs b/src/repo_ref.rs index 94af864..0e57d96 100644 --- a/src/repo_ref.rs +++ b/src/repo_ref.rs @@ -16,7 +16,7 @@ use crate::client::Client; use crate::{ cli_interactor::{Interactor, InteractorPrompt, PromptInputParms}, client::{get_event_from_global_cache, get_events_from_cache, sign_event, Connect}, - git::{nostr_git_url_to_repo_coordinates, Repo, RepoActions}, + git::{NostrUrlDecoded, Repo, RepoActions}, }; #[derive(Default)] @@ -263,8 +263,8 @@ fn get_repo_coordinates_from_nostr_remotes(git_repo: &Repo) -> Result