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-09-03 15:30:37 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-09-03 15:30:37 +0100
commita825311f2c55661aaab3a163bda9109295c96044 (patch)
treeaf56db8858187a540ae90c38998a5222c8cc1f01 /src
parenta0fdc17426afa0e55a2a3b733983bab763226e5a (diff)
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://<optional-protocol>/npub123/<optional-relay>/identifer
Diffstat (limited to 'src')
-rw-r--r--src/git.rs145
-rw-r--r--src/git_remote_helper.rs267
-rw-r--r--src/repo_ref.rs6
3 files changed, 318 insertions, 100 deletions
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>(
835 .context("failed to create git signature") 835 .context("failed to create git signature")
836} 836}
837 837
838pub fn nostr_git_url_to_repo_coordinates(url: &str) -> Result<HashSet<Coordinate>> { 838#[derive(Debug, PartialEq)]
839 let mut repo_coordinattes = HashSet::new(); 839pub enum ServerProtocol {
840 let url = Url::parse(url)?; 840 Ssh,
841 Https,
842 Http,
843 Git,
844}
841 845
842 if url.scheme().ne("nostr") { 846#[derive(Debug, PartialEq)]
843 bail!("nostr git url must start with nostr://") 847pub struct NostrUrlDecoded {
844 } 848 pub coordinates: HashSet<Coordinate>,
849 pub protocol: Option<ServerProtocol>,
850 pub user: Option<String>,
851}
845 852
846 if let Ok(coordinate) = Coordinate::parse(url.domain().context("no naddr")?) { 853static 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";
847 if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) { 854
848 repo_coordinattes.insert(coordinate); 855impl NostrUrlDecoded {
849 return Ok(repo_coordinattes); 856 pub fn from_str(url: &str) -> Result<Self> {
857 let mut coordinates = HashSet::new();
858 let mut protocol = None;
859 let mut user = None;
860 let mut relays = vec![];
861
862 if !url.starts_with("nostr://") {
863 bail!("nostr git url must start with nostr://");
864 }
865 // process get url parameters if present
866 for (name, value) in Url::parse(url)?.query_pairs() {
867 if name.contains("relay") {
868 let mut decoded = urlencoding::decode(&value)
869 .context("could not parse relays in nostr git url")?
870 .to_string();
871 if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") {
872 decoded = format!("wss://{decoded}");
873 }
874 let url =
875 Url::parse(&decoded).context("could not parse relays in nostr git url")?;
876 relays.push(url.to_string());
877 } else if name == "protocol" {
878 protocol = match value.as_ref() {
879 "ssh" => Some(ServerProtocol::Ssh),
880 "https" => Some(ServerProtocol::Https),
881 "http" => Some(ServerProtocol::Http),
882 "git" => Some(ServerProtocol::Git),
883 _ => None,
884 };
885 } else if name == "user" {
886 user = Some(value.to_string());
887 }
850 } 888 }
851 bail!("naddr doesnt point to a git repository announcement");
852 }
853 889
854 if let Some(domain) = url.domain() { 890 let mut parts: Vec<&str> = url[8..]
855 if let Ok(public_key) = PublicKey::parse(domain) { 891 .split('?')
856 if url.path().len() < 2 { 892 .next()
857 bail!( 893 .unwrap_or("")
858 "nostr git url should include the repo identifier eg nostr://npub123/the-repo-identifer" 894 .split('/')
859 ); 895 .collect();
896
897 // extract optional protocol
898 if protocol.is_none() {
899 let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?;
900 let protocol_str = if let Some(at_index) = part.find('@') {
901 user = Some(part[..at_index].to_string());
902 &part[at_index + 1..]
903 } else {
904 part
905 };
906 protocol = match protocol_str {
907 "ssh" => Some(ServerProtocol::Ssh),
908 "https" => Some(ServerProtocol::Https),
909 "http" => Some(ServerProtocol::Http),
910 "git" => Some(ServerProtocol::Git),
911 _ => protocol,
912 };
913 if protocol.is_some() {
914 parts.remove(0);
860 } 915 }
861 let mut relays = vec![]; 916 }
862 for (name, value) in url.query_pairs() { 917 // extract naddr npub/<optional-relays>/identifer
863 if name.contains("relay") { 918 let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?;
864 let mut decoded = urlencoding::decode(&value) 919 // naddr used
865 .context("could not parse relays in nostr git url")? 920 if let Ok(coordinate) = Coordinate::parse(part) {
866 .to_string(); 921 if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) {
867 if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") { 922 coordinates.insert(coordinate);
868 decoded = format!("wss://{decoded}"); 923 } else {
869 } 924 bail!("naddr doesnt point to a git repository announcement");
870 let url = 925 }
871 Url::parse(&decoded).context("could not parse relays in nostr git url")?; 926 // npub/<optional-relays>/identifer used
872 relays.push(url.to_string()); 927 } else if let Ok(public_key) = PublicKey::parse(part) {
928 parts.remove(0);
929 let identifier = parts
930 .pop()
931 .context("nostr url must have an identifier eg. nostr://npub123/repo-identifier")?
932 .to_string();
933 for relay in parts {
934 let mut decoded = urlencoding::decode(relay)
935 .context("could not parse relays in nostr git url")?
936 .to_string();
937 if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") {
938 decoded = format!("wss://{decoded}");
873 } 939 }
940 let url =
941 Url::parse(&decoded).context("could not parse relays in nostr git url")?;
942 relays.push(url.to_string());
874 } 943 }
875 repo_coordinattes.insert(Coordinate { 944 coordinates.insert(Coordinate {
876 identifier: url.path()[1..].to_string(), 945 identifier,
877 public_key, 946 public_key,
878 kind: nostr_sdk::Kind::GitRepoAnnouncement, 947 kind: nostr_sdk::Kind::GitRepoAnnouncement,
879 relays, 948 relays,
880 }); 949 });
881 return Ok(repo_coordinattes); 950 } else {
951 bail!(INCORRECT_NOSTR_URL_FORMAT_ERROR);
882 } 952 }
953
954 Ok(Self {
955 coordinates,
956 protocol,
957 user,
958 })
883 } 959 }
884 bail!(
885 "nostr git url must be in format nostr://naddr123 or nostr://npub123/identifer?relay=wss://relay-example.com&relay1=wss://relay-example.org"
886 );
887} 960}
888 961
889/** produce error when using local repo or custom protocols */ 962/** 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::{
19 get_state_from_cache, sign_event, Connect, STATE_KIND, 19 get_state_from_cache, sign_event, Connect, STATE_KIND,
20}; 20};
21use console::Term; 21use console::Term;
22use git::{nostr_git_url_to_repo_coordinates, sha1_to_oid, RepoActions}; 22use git::{sha1_to_oid, NostrUrlDecoded, RepoActions};
23use git2::{Oid, Repository}; 23use git2::{Oid, Repository};
24use nostr::nips::{nip01::Coordinate, nip10::Marker}; 24use nostr::nips::{nip01::Coordinate, nip10::Marker};
25use nostr_sdk::{ 25use nostr_sdk::{
@@ -81,12 +81,12 @@ async fn main() -> Result<()> {
81 #[cfg(test)] 81 #[cfg(test)]
82 let client = <MockConnect as std::default::Default>::default(); 82 let client = <MockConnect as std::default::Default>::default();
83 83
84 let repo_coordinates = 84 let decoded_nostr_url =
85 nostr_git_url_to_repo_coordinates(nostr_remote_url).context("invalid nostr url")?; 85 NostrUrlDecoded::from_str(nostr_remote_url).context("invalid nostr url")?;
86 86
87 fetching_with_report_for_helper(git_repo_path, &client, &repo_coordinates).await?; 87 fetching_with_report_for_helper(git_repo_path, &client, &decoded_nostr_url.coordinates).await?;
88 88
89 let repo_ref = get_repo_ref_from_cache(git_repo_path, &repo_coordinates).await?; 89 let repo_ref = get_repo_ref_from_cache(git_repo_path, &decoded_nostr_url.coordinates).await?;
90 90
91 let stdin = io::stdin(); 91 let stdin = io::stdin();
92 let mut line = String::new(); 92 let mut line = String::new();
@@ -1639,7 +1639,8 @@ impl RepoState {
1639mod tests { 1639mod tests {
1640 use super::*; 1640 use super::*;
1641 1641
1642 mod nostr_git_url_to_repo_coordinates { 1642 mod nostr_git_url_paramemters_from_str {
1643 use git::ServerProtocol;
1643 use nostr_sdk::PublicKey; 1644 use nostr_sdk::PublicKey;
1644 1645
1645 use super::*; 1646 use super::*;
@@ -1663,79 +1664,223 @@ mod tests {
1663 #[test] 1664 #[test]
1664 fn from_naddr() -> Result<()> { 1665 fn from_naddr() -> Result<()> {
1665 assert_eq!( 1666 assert_eq!(
1666 nostr_git_url_to_repo_coordinates( 1667 NostrUrlDecoded::from_str(
1667 "nostr://naddr1qqzxuemfwsqs6amnwvaz7tmwdaejumr0dspzpgqgmmc409hm4xsdd74sf68a2uyf9pwel4g9mfdg8l5244t6x4jdqvzqqqrhnym0k2qj" 1668 "nostr://naddr1qqzxuemfwsqs6amnwvaz7tmwdaejumr0dspzpgqgmmc409hm4xsdd74sf68a2uyf9pwel4g9mfdg8l5244t6x4jdqvzqqqrhnym0k2qj"
1668 )?, 1669 )?,
1669 HashSet::from([Coordinate { 1670 NostrUrlDecoded {
1670 identifier: "ngit".to_string(), 1671 coordinates: HashSet::from([Coordinate {
1671 public_key: PublicKey::parse( 1672 identifier: "ngit".to_string(),
1672 "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", 1673 public_key: PublicKey::parse(
1673 ) 1674 "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr",
1674 .unwrap(), 1675 )
1675 kind: nostr_sdk::Kind::GitRepoAnnouncement, 1676 .unwrap(),
1676 relays: vec!["wss://nos.lol".to_string()], // wont add the slash 1677 kind: nostr_sdk::Kind::GitRepoAnnouncement,
1677 }]), 1678 relays: vec!["wss://nos.lol".to_string()], // wont add the slash
1679 }]),
1680 protocol: None,
1681 user: None,
1682 },
1678 ); 1683 );
1679 Ok(()) 1684 Ok(())
1680 } 1685 }
1681 mod from_npub_slah_identifier { 1686 mod from_npub_slash_identifier {
1682 use super::*; 1687 use super::*;
1683 1688
1684 #[test] 1689 #[test]
1685 fn without_relay() -> Result<()> { 1690 fn without_relay() -> Result<()> {
1686 assert_eq!( 1691 assert_eq!(
1687 nostr_git_url_to_repo_coordinates( 1692 NostrUrlDecoded::from_str(
1688 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit" 1693 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit"
1689 )?, 1694 )?,
1690 HashSet::from([get_model_coordinate(false)]), 1695 NostrUrlDecoded {
1696 coordinates: HashSet::from([get_model_coordinate(false)]),
1697 protocol: None,
1698 user: None,
1699 },
1691 ); 1700 );
1692 Ok(()) 1701 Ok(())
1693 } 1702 }
1694 1703
1695 #[test] 1704 mod with_url_parameters {
1696 fn with_relay_without_scheme_defaults_to_wss() -> Result<()> { 1705
1697 assert_eq!( 1706 use super::*;
1698 nostr_git_url_to_repo_coordinates( 1707
1699 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay=nos.lol" 1708 #[test]
1700 )?, 1709 fn with_relay_without_scheme_defaults_to_wss() -> Result<()> {
1701 HashSet::from([get_model_coordinate(true)]), 1710 assert_eq!(
1702 ); 1711 NostrUrlDecoded::from_str(
1703 Ok(()) 1712 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay=nos.lol"
1704 } 1713 )?,
1714 NostrUrlDecoded {
1715 coordinates: HashSet::from([get_model_coordinate(true)]),
1716 protocol: None,
1717 user: None,
1718 },
1719 );
1720 Ok(())
1721 }
1705 1722
1706 #[test] 1723 #[test]
1707 fn with_encoded_relay() -> Result<()> { 1724 fn with_encoded_relay() -> Result<()> {
1708 assert_eq!( 1725 assert_eq!(
1709 nostr_git_url_to_repo_coordinates(&format!( 1726 NostrUrlDecoded::from_str(&format!(
1710 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}", 1727 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}",
1711 urlencoding::encode("wss://nos.lol") 1728 urlencoding::encode("wss://nos.lol")
1712 ))?, 1729 ))?,
1713 HashSet::from([get_model_coordinate(true)]), 1730 NostrUrlDecoded {
1714 ); 1731 coordinates: HashSet::from([get_model_coordinate(true)]),
1715 Ok(()) 1732 protocol: None,
1733 user: None,
1734 },
1735 );
1736 Ok(())
1737 }
1738 #[test]
1739 fn with_multiple_encoded_relays() -> Result<()> {
1740 assert_eq!(
1741 NostrUrlDecoded::from_str(&format!(
1742 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}&relay1={}",
1743 urlencoding::encode("wss://nos.lol"),
1744 urlencoding::encode("wss://relay.damus.io"),
1745 ))?,
1746 NostrUrlDecoded {
1747 coordinates: HashSet::from([Coordinate {
1748 identifier: "ngit".to_string(),
1749 public_key: PublicKey::parse(
1750 "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr",
1751 )
1752 .unwrap(),
1753 kind: nostr_sdk::Kind::GitRepoAnnouncement,
1754 relays: vec![
1755 "wss://nos.lol/".to_string(),
1756 "wss://relay.damus.io/".to_string(),
1757 ],
1758 }]),
1759 protocol: None,
1760 user: None,
1761 },
1762 );
1763 Ok(())
1764 }
1765
1766 #[test]
1767 fn with_server_protocol() -> Result<()> {
1768 assert_eq!(
1769 NostrUrlDecoded::from_str(
1770 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?protocol=ssh"
1771 )?,
1772 NostrUrlDecoded {
1773 coordinates: HashSet::from([get_model_coordinate(false)]),
1774 protocol: Some(ServerProtocol::Ssh),
1775 user: None,
1776 },
1777 );
1778 Ok(())
1779 }
1780 #[test]
1781 fn with_server_protocol_and_user() -> Result<()> {
1782 assert_eq!(
1783 NostrUrlDecoded::from_str(
1784 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?protocol=ssh&user=fred"
1785 )?,
1786 NostrUrlDecoded {
1787 coordinates: HashSet::from([get_model_coordinate(false)]),
1788 protocol: Some(ServerProtocol::Ssh),
1789 user: Some("fred".to_string()),
1790 },
1791 );
1792 Ok(())
1793 }
1716 } 1794 }
1717 #[test] 1795 mod with_parameters_embedded_with_slashes {
1718 fn with_multiple_encoded_relays() -> Result<()> { 1796 use super::*;
1719 assert_eq!( 1797
1720 nostr_git_url_to_repo_coordinates(&format!( 1798 #[test]
1721 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}&relay1={}", 1799 fn with_relay_without_scheme_defaults_to_wss() -> Result<()> {
1722 urlencoding::encode("wss://nos.lol"), 1800 assert_eq!(
1723 urlencoding::encode("wss://relay.damus.io"), 1801 NostrUrlDecoded::from_str(
1724 ))?, 1802 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit"
1725 HashSet::from([Coordinate { 1803 )?,
1726 identifier: "ngit".to_string(), 1804 NostrUrlDecoded {
1727 public_key: PublicKey::parse( 1805 coordinates: HashSet::from([get_model_coordinate(true)]),
1728 "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", 1806 protocol: None,
1729 ) 1807 user: None,
1730 .unwrap(), 1808 },
1731 kind: nostr_sdk::Kind::GitRepoAnnouncement, 1809 );
1732 relays: vec![ 1810 Ok(())
1733 "wss://nos.lol/".to_string(), 1811 }
1734 "wss://relay.damus.io/".to_string(), 1812
1735 ], 1813 #[test]
1736 }]), 1814 fn with_encoded_relay() -> Result<()> {
1737 ); 1815 assert_eq!(
1738 Ok(()) 1816 NostrUrlDecoded::from_str(&format!(
1817 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/{}/ngit",
1818 urlencoding::encode("wss://nos.lol")
1819 ))?,
1820 NostrUrlDecoded {
1821 coordinates: HashSet::from([get_model_coordinate(true)]),
1822 protocol: None,
1823 user: None,
1824 },
1825 );
1826 Ok(())
1827 }
1828 #[test]
1829 fn with_multiple_encoded_relays() -> Result<()> {
1830 assert_eq!(
1831 NostrUrlDecoded::from_str(&format!(
1832 "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/{}/{}/ngit",
1833 urlencoding::encode("wss://nos.lol"),
1834 urlencoding::encode("wss://relay.damus.io"),
1835 ))?,
1836 NostrUrlDecoded {
1837 coordinates: HashSet::from([Coordinate {
1838 identifier: "ngit".to_string(),
1839 public_key: PublicKey::parse(
1840 "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr",
1841 )
1842 .unwrap(),
1843 kind: nostr_sdk::Kind::GitRepoAnnouncement,
1844 relays: vec![
1845 "wss://nos.lol/".to_string(),
1846 "wss://relay.damus.io/".to_string(),
1847 ],
1848 }]),
1849 protocol: None,
1850 user: None,
1851 },
1852 );
1853 Ok(())
1854 }
1855
1856 #[test]
1857 fn with_server_protocol() -> Result<()> {
1858 assert_eq!(
1859 NostrUrlDecoded::from_str(
1860 "nostr://ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit"
1861 )?,
1862 NostrUrlDecoded {
1863 coordinates: HashSet::from([get_model_coordinate(false)]),
1864 protocol: Some(ServerProtocol::Ssh),
1865 user: None,
1866 },
1867 );
1868 Ok(())
1869 }
1870 #[test]
1871 fn with_server_protocol_and_user() -> Result<()> {
1872 assert_eq!(
1873 NostrUrlDecoded::from_str(
1874 "nostr://fred@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit"
1875 )?,
1876 NostrUrlDecoded {
1877 coordinates: HashSet::from([get_model_coordinate(false)]),
1878 protocol: Some(ServerProtocol::Ssh),
1879 user: Some("fred".to_string()),
1880 },
1881 );
1882 Ok(())
1883 }
1739 } 1884 }
1740 } 1885 }
1741 } 1886 }
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;
16use crate::{ 16use crate::{
17 cli_interactor::{Interactor, InteractorPrompt, PromptInputParms}, 17 cli_interactor::{Interactor, InteractorPrompt, PromptInputParms},
18 client::{get_event_from_global_cache, get_events_from_cache, sign_event, Connect}, 18 client::{get_event_from_global_cache, get_events_from_cache, sign_event, Connect},
19 git::{nostr_git_url_to_repo_coordinates, Repo, RepoActions}, 19 git::{NostrUrlDecoded, Repo, RepoActions},
20}; 20};
21 21
22#[derive(Default)] 22#[derive(Default)]
@@ -263,8 +263,8 @@ fn get_repo_coordinates_from_nostr_remotes(git_repo: &Repo) -> Result<HashSet<Co
263 let mut repo_coordinates = HashSet::new(); 263 let mut repo_coordinates = HashSet::new();
264 for remote_name in git_repo.git_repo.remotes()?.iter().flatten() { 264 for remote_name in git_repo.git_repo.remotes()?.iter().flatten() {
265 if let Some(remote_url) = git_repo.git_repo.find_remote(remote_name)?.url() { 265 if let Some(remote_url) = git_repo.git_repo.find_remote(remote_name)?.url() {
266 if let Ok(coordinates) = nostr_git_url_to_repo_coordinates(remote_url) { 266 if let Ok(nostr_url_decoded) = NostrUrlDecoded::from_str(remote_url) {
267 for c in coordinates { 267 for c in nostr_url_decoded.coordinates {
268 repo_coordinates.insert(c); 268 repo_coordinates.insert(c);
269 } 269 }
270 } 270 }