diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2024-09-04 11:32:05 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2024-09-04 14:23:54 +0100 |
| commit | 771f944af447c202eba045936a36dee71ab797ac (patch) | |
| tree | e691de4ebc8dde7ac4855e139881ff923bc254ce /src/lib/git | |
| parent | 949c6459aa7683453a7160423b689ceadb08954b (diff) | |
refactor: fix imports, etc based on restructure
move some functions out of ngit and into lib/mod
and lib/git_events
remove MockConnect from binaries so it is only used in the library.
this was done:
* mainly because automocks were not being imported from
lib into each binary
* but also because the these functions were being
tested with MockConnect
Diffstat (limited to 'src/lib/git')
| -rw-r--r-- | src/lib/git/identify_ahead_behind.rs | 196 | ||||
| -rw-r--r-- | src/lib/git/mod.rs | 266 | ||||
| -rw-r--r-- | src/lib/git/nostr_url.rs | 501 |
3 files changed, 704 insertions, 259 deletions
diff --git a/src/lib/git/identify_ahead_behind.rs b/src/lib/git/identify_ahead_behind.rs new file mode 100644 index 0000000..c98c994 --- /dev/null +++ b/src/lib/git/identify_ahead_behind.rs | |||
| @@ -0,0 +1,196 @@ | |||
| 1 | use anyhow::{Context, Result}; | ||
| 2 | use nostr_sdk::hashes::sha1::Hash as Sha1Hash; | ||
| 3 | |||
| 4 | use super::{Repo, RepoActions}; | ||
| 5 | |||
| 6 | /** | ||
| 7 | * returns `(from_branch,to_branch,ahead,behind)` | ||
| 8 | */ | ||
| 9 | pub fn identify_ahead_behind( | ||
| 10 | git_repo: &Repo, | ||
| 11 | from_branch: &Option<String>, | ||
| 12 | to_branch: &Option<String>, | ||
| 13 | ) -> Result<(String, String, Vec<Sha1Hash>, Vec<Sha1Hash>)> { | ||
| 14 | let (from_branch, from_tip) = match from_branch { | ||
| 15 | Some(name) => ( | ||
| 16 | name.to_string(), | ||
| 17 | git_repo | ||
| 18 | .get_tip_of_branch(name) | ||
| 19 | .context(format!("cannot find from_branch '{name}'"))?, | ||
| 20 | ), | ||
| 21 | None => ( | ||
| 22 | if let Ok(name) = git_repo.get_checked_out_branch_name() { | ||
| 23 | name | ||
| 24 | } else { | ||
| 25 | "head".to_string() | ||
| 26 | }, | ||
| 27 | git_repo | ||
| 28 | .get_head_commit() | ||
| 29 | .context("failed to get head commit") | ||
| 30 | .context( | ||
| 31 | "checkout a commit or specify a from_branch. head does not reveal a commit", | ||
| 32 | )?, | ||
| 33 | ), | ||
| 34 | }; | ||
| 35 | |||
| 36 | let (to_branch, to_tip) = match to_branch { | ||
| 37 | Some(name) => ( | ||
| 38 | name.to_string(), | ||
| 39 | git_repo | ||
| 40 | .get_tip_of_branch(name) | ||
| 41 | .context(format!("cannot find to_branch '{name}'"))?, | ||
| 42 | ), | ||
| 43 | None => { | ||
| 44 | let (name, commit) = git_repo | ||
| 45 | .get_main_or_master_branch() | ||
| 46 | .context("the default branches (main or master) do not exist")?; | ||
| 47 | (name.to_string(), commit) | ||
| 48 | } | ||
| 49 | }; | ||
| 50 | |||
| 51 | match git_repo.get_commits_ahead_behind(&to_tip, &from_tip) { | ||
| 52 | Err(e) => { | ||
| 53 | if e.to_string().contains("is not an ancestor of") { | ||
| 54 | return Err(e).context(format!( | ||
| 55 | "'{from_branch}' is not branched from '{to_branch}'" | ||
| 56 | )); | ||
| 57 | } | ||
| 58 | Err(e).context(format!( | ||
| 59 | "failed to get commits ahead and behind from '{from_branch}' to '{to_branch}'" | ||
| 60 | )) | ||
| 61 | } | ||
| 62 | Ok((ahead, behind)) => Ok((from_branch, to_branch, ahead, behind)), | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | #[cfg(test)] | ||
| 67 | mod tests { | ||
| 68 | |||
| 69 | use test_utils::git::GitTestRepo; | ||
| 70 | |||
| 71 | use super::*; | ||
| 72 | use crate::git::oid_to_sha1; | ||
| 73 | |||
| 74 | #[test] | ||
| 75 | fn when_from_branch_doesnt_exist_return_error() -> Result<()> { | ||
| 76 | let test_repo = GitTestRepo::default(); | ||
| 77 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 78 | |||
| 79 | test_repo.populate()?; | ||
| 80 | let branch_name = "doesnt_exist"; | ||
| 81 | assert_eq!( | ||
| 82 | identify_ahead_behind(&git_repo, &Some(branch_name.to_string()), &None) | ||
| 83 | .unwrap_err() | ||
| 84 | .to_string(), | ||
| 85 | format!("cannot find from_branch '{}'", &branch_name), | ||
| 86 | ); | ||
| 87 | Ok(()) | ||
| 88 | } | ||
| 89 | |||
| 90 | #[test] | ||
| 91 | fn when_to_branch_doesnt_exist_return_error() -> Result<()> { | ||
| 92 | let test_repo = GitTestRepo::default(); | ||
| 93 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 94 | |||
| 95 | test_repo.populate()?; | ||
| 96 | let branch_name = "doesnt_exist"; | ||
| 97 | assert_eq!( | ||
| 98 | identify_ahead_behind(&git_repo, &None, &Some(branch_name.to_string())) | ||
| 99 | .unwrap_err() | ||
| 100 | .to_string(), | ||
| 101 | format!("cannot find to_branch '{}'", &branch_name), | ||
| 102 | ); | ||
| 103 | Ok(()) | ||
| 104 | } | ||
| 105 | |||
| 106 | #[test] | ||
| 107 | fn when_to_branch_is_none_and_no_main_or_master_branch_return_error() -> Result<()> { | ||
| 108 | let test_repo = GitTestRepo::new("notmain")?; | ||
| 109 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 110 | |||
| 111 | test_repo.populate()?; | ||
| 112 | |||
| 113 | assert_eq!( | ||
| 114 | identify_ahead_behind(&git_repo, &None, &None) | ||
| 115 | .unwrap_err() | ||
| 116 | .to_string(), | ||
| 117 | "the default branches (main or master) do not exist", | ||
| 118 | ); | ||
| 119 | Ok(()) | ||
| 120 | } | ||
| 121 | |||
| 122 | #[test] | ||
| 123 | fn when_from_branch_is_not_head_return_as_from_branch() -> Result<()> { | ||
| 124 | let test_repo = GitTestRepo::default(); | ||
| 125 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 126 | |||
| 127 | test_repo.populate()?; | ||
| 128 | // create feature branch with 1 commit ahead | ||
| 129 | test_repo.create_branch("feature")?; | ||
| 130 | test_repo.checkout("feature")?; | ||
| 131 | std::fs::write(test_repo.dir.join("t3.md"), "some content")?; | ||
| 132 | let head_oid = test_repo.stage_and_commit("add t3.md")?; | ||
| 133 | |||
| 134 | // make feature branch 1 commit behind | ||
| 135 | test_repo.checkout("main")?; | ||
| 136 | std::fs::write(test_repo.dir.join("t4.md"), "some content")?; | ||
| 137 | let main_oid = test_repo.stage_and_commit("add t4.md")?; | ||
| 138 | |||
| 139 | let (from_branch, to_branch, ahead, behind) = | ||
| 140 | identify_ahead_behind(&git_repo, &Some("feature".to_string()), &None)?; | ||
| 141 | |||
| 142 | assert_eq!(from_branch, "feature"); | ||
| 143 | assert_eq!(ahead, vec![oid_to_sha1(&head_oid)]); | ||
| 144 | assert_eq!(to_branch, "main"); | ||
| 145 | assert_eq!(behind, vec![oid_to_sha1(&main_oid)]); | ||
| 146 | Ok(()) | ||
| 147 | } | ||
| 148 | |||
| 149 | #[test] | ||
| 150 | fn when_to_branch_is_not_main_return_as_to_branch() -> Result<()> { | ||
| 151 | let test_repo = GitTestRepo::default(); | ||
| 152 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 153 | |||
| 154 | test_repo.populate()?; | ||
| 155 | // create dev branch with 1 commit ahead | ||
| 156 | test_repo.create_branch("dev")?; | ||
| 157 | test_repo.checkout("dev")?; | ||
| 158 | std::fs::write(test_repo.dir.join("t3.md"), "some content")?; | ||
| 159 | let dev_oid_first = test_repo.stage_and_commit("add t3.md")?; | ||
| 160 | |||
| 161 | // create feature branch with 1 commit ahead of dev | ||
| 162 | test_repo.create_branch("feature")?; | ||
| 163 | test_repo.checkout("feature")?; | ||
| 164 | std::fs::write(test_repo.dir.join("t4.md"), "some content")?; | ||
| 165 | let feature_oid = test_repo.stage_and_commit("add t4.md")?; | ||
| 166 | |||
| 167 | // make feature branch 1 behind | ||
| 168 | test_repo.checkout("dev")?; | ||
| 169 | std::fs::write(test_repo.dir.join("t3.md"), "some content")?; | ||
| 170 | let dev_oid = test_repo.stage_and_commit("add t3.md")?; | ||
| 171 | |||
| 172 | let (from_branch, to_branch, ahead, behind) = identify_ahead_behind( | ||
| 173 | &git_repo, | ||
| 174 | &Some("feature".to_string()), | ||
| 175 | &Some("dev".to_string()), | ||
| 176 | )?; | ||
| 177 | |||
| 178 | assert_eq!(from_branch, "feature"); | ||
| 179 | assert_eq!(ahead, vec![oid_to_sha1(&feature_oid)]); | ||
| 180 | assert_eq!(to_branch, "dev"); | ||
| 181 | assert_eq!(behind, vec![oid_to_sha1(&dev_oid)]); | ||
| 182 | |||
| 183 | let (from_branch, to_branch, ahead, behind) = | ||
| 184 | identify_ahead_behind(&git_repo, &Some("feature".to_string()), &None)?; | ||
| 185 | |||
| 186 | assert_eq!(from_branch, "feature"); | ||
| 187 | assert_eq!( | ||
| 188 | ahead, | ||
| 189 | vec![oid_to_sha1(&feature_oid), oid_to_sha1(&dev_oid_first)] | ||
| 190 | ); | ||
| 191 | assert_eq!(to_branch, "main"); | ||
| 192 | assert_eq!(behind, vec![]); | ||
| 193 | |||
| 194 | Ok(()) | ||
| 195 | } | ||
| 196 | } | ||
diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs index 5919667..f92272f 100644 --- a/src/lib/git/mod.rs +++ b/src/lib/git/mod.rs | |||
| @@ -1,18 +1,16 @@ | |||
| 1 | use std::{ | 1 | use std::{ |
| 2 | collections::HashSet, | ||
| 3 | env::current_dir, | 2 | env::current_dir, |
| 4 | path::{Path, PathBuf}, | 3 | path::{Path, PathBuf}, |
| 5 | }; | 4 | }; |
| 6 | 5 | ||
| 7 | use anyhow::{bail, Context, Result}; | 6 | use anyhow::{bail, Context, Result}; |
| 8 | use git2::{DiffOptions, Oid, Revwalk}; | 7 | use git2::{DiffOptions, Oid, Revwalk}; |
| 9 | use nostr::nips::nip01::Coordinate; | 8 | pub use identify_ahead_behind::identify_ahead_behind; |
| 10 | use nostr_sdk::{ | 9 | use nostr_sdk::hashes::{sha1::Hash as Sha1Hash, Hash}; |
| 11 | hashes::{sha1::Hash as Sha1Hash, Hash}, | ||
| 12 | PublicKey, Url, | ||
| 13 | }; | ||
| 14 | 10 | ||
| 15 | use crate::sub_commands::list::{get_commit_id_from_patch, tag_value}; | 11 | use crate::git_events::{get_commit_id_from_patch, tag_value}; |
| 12 | pub mod identify_ahead_behind; | ||
| 13 | pub mod nostr_url; | ||
| 16 | 14 | ||
| 17 | pub struct Repo { | 15 | pub struct Repo { |
| 18 | pub git_repo: git2::Repository, | 16 | pub git_repo: git2::Repository, |
| @@ -835,188 +833,6 @@ fn extract_sig_from_patch_tags<'a>( | |||
| 835 | .context("failed to create git signature") | 833 | .context("failed to create git signature") |
| 836 | } | 834 | } |
| 837 | 835 | ||
| 838 | #[derive(Debug, PartialEq)] | ||
| 839 | pub enum ServerProtocol { | ||
| 840 | Ssh, | ||
| 841 | Https, | ||
| 842 | Http, | ||
| 843 | Git, | ||
| 844 | } | ||
| 845 | |||
| 846 | #[derive(Debug, PartialEq)] | ||
| 847 | pub struct NostrUrlDecoded { | ||
| 848 | pub coordinates: HashSet<Coordinate>, | ||
| 849 | pub protocol: Option<ServerProtocol>, | ||
| 850 | pub user: Option<String>, | ||
| 851 | } | ||
| 852 | |||
| 853 | 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"; | ||
| 854 | |||
| 855 | impl NostrUrlDecoded { | ||
| 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 | } | ||
| 888 | } | ||
| 889 | |||
| 890 | let mut parts: Vec<&str> = url[8..] | ||
| 891 | .split('?') | ||
| 892 | .next() | ||
| 893 | .unwrap_or("") | ||
| 894 | .split('/') | ||
| 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); | ||
| 915 | } | ||
| 916 | } | ||
| 917 | // extract naddr npub/<optional-relays>/identifer | ||
| 918 | let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?; | ||
| 919 | // naddr used | ||
| 920 | if let Ok(coordinate) = Coordinate::parse(part) { | ||
| 921 | if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) { | ||
| 922 | coordinates.insert(coordinate); | ||
| 923 | } else { | ||
| 924 | bail!("naddr doesnt point to a git repository announcement"); | ||
| 925 | } | ||
| 926 | // npub/<optional-relays>/identifer used | ||
| 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}"); | ||
| 939 | } | ||
| 940 | let url = | ||
| 941 | Url::parse(&decoded).context("could not parse relays in nostr git url")?; | ||
| 942 | relays.push(url.to_string()); | ||
| 943 | } | ||
| 944 | coordinates.insert(Coordinate { | ||
| 945 | identifier, | ||
| 946 | public_key, | ||
| 947 | kind: nostr_sdk::Kind::GitRepoAnnouncement, | ||
| 948 | relays, | ||
| 949 | }); | ||
| 950 | } else { | ||
| 951 | bail!(INCORRECT_NOSTR_URL_FORMAT_ERROR); | ||
| 952 | } | ||
| 953 | |||
| 954 | Ok(Self { | ||
| 955 | coordinates, | ||
| 956 | protocol, | ||
| 957 | user, | ||
| 958 | }) | ||
| 959 | } | ||
| 960 | } | ||
| 961 | |||
| 962 | /** produce error when using local repo or custom protocols */ | ||
| 963 | pub fn convert_clone_url_to_https(url: &str) -> Result<String> { | ||
| 964 | // Strip credentials if present | ||
| 965 | let stripped_url = strip_credentials(url); | ||
| 966 | |||
| 967 | // Check if the URL is already in HTTPS format | ||
| 968 | if stripped_url.starts_with("https://") { | ||
| 969 | return Ok(stripped_url); | ||
| 970 | } | ||
| 971 | // Convert http:// to https:// | ||
| 972 | else if stripped_url.starts_with("http://") { | ||
| 973 | return Ok(stripped_url.replace("http://", "https://")); | ||
| 974 | } | ||
| 975 | // Check if the URL starts with SSH | ||
| 976 | else if stripped_url.starts_with("ssh://") { | ||
| 977 | // Convert SSH to HTTPS | ||
| 978 | let parts: Vec<&str> = stripped_url | ||
| 979 | .trim_start_matches("ssh://") | ||
| 980 | .split('/') | ||
| 981 | .collect(); | ||
| 982 | if parts.len() >= 2 { | ||
| 983 | // Construct the HTTPS URL | ||
| 984 | return Ok(format!("https://{}/{}", parts[0], parts[1..].join("/"))); | ||
| 985 | } | ||
| 986 | bail!("Invalid SSH URL format: {}", url); | ||
| 987 | } | ||
| 988 | // Convert ftp:// to https:// | ||
| 989 | else if stripped_url.starts_with("ftp://") { | ||
| 990 | return Ok(stripped_url.replace("ftp://", "https://")); | ||
| 991 | } | ||
| 992 | // Convert git:// to https:// | ||
| 993 | else if stripped_url.starts_with("git://") { | ||
| 994 | return Ok(stripped_url.replace("git://", "https://")); | ||
| 995 | } | ||
| 996 | |||
| 997 | // If the URL is neither HTTPS, SSH, nor git@, return an error | ||
| 998 | bail!("Unsupported URL protocol: {}", url); | ||
| 999 | } | ||
| 1000 | |||
| 1001 | // Function to strip username and password from the URL | ||
| 1002 | fn strip_credentials(url: &str) -> String { | ||
| 1003 | if let Some(pos) = url.find("://") { | ||
| 1004 | let (protocol, rest) = url.split_at(pos + 3); // Split at "://" | ||
| 1005 | let rest_parts: Vec<&str> = rest.split('@').collect(); | ||
| 1006 | if rest_parts.len() > 1 { | ||
| 1007 | // If there are credentials, return the URL without them | ||
| 1008 | return format!("{}{}", protocol, rest_parts[1]); | ||
| 1009 | } | ||
| 1010 | } else if let Some(at_pos) = url.find('@') { | ||
| 1011 | // Handle user@host:path format | ||
| 1012 | let (_, rest) = url.split_at(at_pos); | ||
| 1013 | // This is a git@ syntax | ||
| 1014 | let host_and_repo = &rest[1..]; // Skip the ':' | ||
| 1015 | return format!("ssh://{}", host_and_repo.replace(':', "/")); | ||
| 1016 | } | ||
| 1017 | url.to_string() // Return the original URL if no credentials are found | ||
| 1018 | } | ||
| 1019 | |||
| 1020 | #[cfg(test)] | 836 | #[cfg(test)] |
| 1021 | mod tests { | 837 | mod tests { |
| 1022 | use std::fs; | 838 | use std::fs; |
| @@ -1813,7 +1629,7 @@ mod tests { | |||
| 1813 | use test_utils::TEST_KEY_1_SIGNER; | 1629 | use test_utils::TEST_KEY_1_SIGNER; |
| 1814 | 1630 | ||
| 1815 | use super::*; | 1631 | use super::*; |
| 1816 | use crate::{repo_ref::RepoRef, sub_commands::send::generate_patch_event}; | 1632 | use crate::{git_events::generate_patch_event, repo_ref::RepoRef}; |
| 1817 | 1633 | ||
| 1818 | async fn generate_patch_from_head_commit(test_repo: &GitTestRepo) -> Result<nostr::Event> { | 1634 | async fn generate_patch_from_head_commit(test_repo: &GitTestRepo) -> Result<nostr::Event> { |
| 1819 | let original_oid = test_repo.git_repo.head()?.peel_to_commit()?.id(); | 1635 | let original_oid = test_repo.git_repo.head()?.peel_to_commit()?.id(); |
| @@ -1959,9 +1775,7 @@ mod tests { | |||
| 1959 | use test_utils::TEST_KEY_1_SIGNER; | 1775 | use test_utils::TEST_KEY_1_SIGNER; |
| 1960 | 1776 | ||
| 1961 | use super::*; | 1777 | use super::*; |
| 1962 | use crate::{ | 1778 | use crate::{git_events::generate_cover_letter_and_patch_events, repo_ref::RepoRef}; |
| 1963 | repo_ref::RepoRef, sub_commands::send::generate_cover_letter_and_patch_events, | ||
| 1964 | }; | ||
| 1965 | 1779 | ||
| 1966 | static BRANCH_NAME: &str = "add-example-feature"; | 1780 | static BRANCH_NAME: &str = "add-example-feature"; |
| 1967 | // returns original_repo, cover_letter_event, patch_events | 1781 | // returns original_repo, cover_letter_event, patch_events |
| @@ -2497,70 +2311,4 @@ mod tests { | |||
| 2497 | Ok(()) | 2311 | Ok(()) |
| 2498 | } | 2312 | } |
| 2499 | } | 2313 | } |
| 2500 | mod convert_clone_url_to_https { | ||
| 2501 | use super::*; | ||
| 2502 | |||
| 2503 | #[test] | ||
| 2504 | fn test_https_url() { | ||
| 2505 | let url = "https://github.com/user/repo.git"; | ||
| 2506 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2507 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 2508 | } | ||
| 2509 | |||
| 2510 | #[test] | ||
| 2511 | fn test_http_url() { | ||
| 2512 | let url = "http://github.com/user/repo.git"; | ||
| 2513 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2514 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 2515 | } | ||
| 2516 | |||
| 2517 | #[test] | ||
| 2518 | fn test_http_url_with_credentials() { | ||
| 2519 | let url = "http://username:password@github.com/user/repo.git"; | ||
| 2520 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2521 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 2522 | } | ||
| 2523 | |||
| 2524 | #[test] | ||
| 2525 | fn test_git_at_url() { | ||
| 2526 | let url = "git@github.com:user/repo.git"; | ||
| 2527 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2528 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 2529 | } | ||
| 2530 | |||
| 2531 | #[test] | ||
| 2532 | fn test_user_at_url() { | ||
| 2533 | let url = "user1@github.com:user/repo.git"; | ||
| 2534 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2535 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 2536 | } | ||
| 2537 | |||
| 2538 | #[test] | ||
| 2539 | fn test_ssh_url() { | ||
| 2540 | let url = "ssh://github.com/user/repo.git"; | ||
| 2541 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2542 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 2543 | } | ||
| 2544 | |||
| 2545 | #[test] | ||
| 2546 | fn test_ftp_url() { | ||
| 2547 | let url = "ftp://example.com/repo.git"; | ||
| 2548 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2549 | assert_eq!(result, "https://example.com/repo.git"); | ||
| 2550 | } | ||
| 2551 | |||
| 2552 | #[test] | ||
| 2553 | fn test_git_protocol_url() { | ||
| 2554 | let url = "git://example.com/repo.git"; | ||
| 2555 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 2556 | assert_eq!(result, "https://example.com/repo.git"); | ||
| 2557 | } | ||
| 2558 | |||
| 2559 | #[test] | ||
| 2560 | fn test_invalid_url() { | ||
| 2561 | let url = "unsupported://example.com/repo.git"; | ||
| 2562 | let result = convert_clone_url_to_https(url); | ||
| 2563 | assert!(result.is_err()); | ||
| 2564 | } | ||
| 2565 | } | ||
| 2566 | } | 2314 | } |
diff --git a/src/lib/git/nostr_url.rs b/src/lib/git/nostr_url.rs new file mode 100644 index 0000000..ce3e973 --- /dev/null +++ b/src/lib/git/nostr_url.rs | |||
| @@ -0,0 +1,501 @@ | |||
| 1 | use std::collections::HashSet; | ||
| 2 | |||
| 3 | use anyhow::{bail, Context, Result}; | ||
| 4 | use nostr::nips::nip01::Coordinate; | ||
| 5 | use nostr_sdk::{PublicKey, Url}; | ||
| 6 | |||
| 7 | #[derive(Debug, PartialEq)] | ||
| 8 | pub enum ServerProtocol { | ||
| 9 | Ssh, | ||
| 10 | Https, | ||
| 11 | Http, | ||
| 12 | Git, | ||
| 13 | } | ||
| 14 | |||
| 15 | #[derive(Debug, PartialEq)] | ||
| 16 | pub struct NostrUrlDecoded { | ||
| 17 | pub coordinates: HashSet<Coordinate>, | ||
| 18 | pub protocol: Option<ServerProtocol>, | ||
| 19 | pub user: Option<String>, | ||
| 20 | } | ||
| 21 | |||
| 22 | 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"; | ||
| 23 | |||
| 24 | impl NostrUrlDecoded { | ||
| 25 | pub fn from_str(url: &str) -> Result<Self> { | ||
| 26 | let mut coordinates = HashSet::new(); | ||
| 27 | let mut protocol = None; | ||
| 28 | let mut user = None; | ||
| 29 | let mut relays = vec![]; | ||
| 30 | |||
| 31 | if !url.starts_with("nostr://") { | ||
| 32 | bail!("nostr git url must start with nostr://"); | ||
| 33 | } | ||
| 34 | // process get url parameters if present | ||
| 35 | for (name, value) in Url::parse(url)?.query_pairs() { | ||
| 36 | if name.contains("relay") { | ||
| 37 | let mut decoded = urlencoding::decode(&value) | ||
| 38 | .context("could not parse relays in nostr git url")? | ||
| 39 | .to_string(); | ||
| 40 | if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") { | ||
| 41 | decoded = format!("wss://{decoded}"); | ||
| 42 | } | ||
| 43 | let url = | ||
| 44 | Url::parse(&decoded).context("could not parse relays in nostr git url")?; | ||
| 45 | relays.push(url.to_string()); | ||
| 46 | } else if name == "protocol" { | ||
| 47 | protocol = match value.as_ref() { | ||
| 48 | "ssh" => Some(ServerProtocol::Ssh), | ||
| 49 | "https" => Some(ServerProtocol::Https), | ||
| 50 | "http" => Some(ServerProtocol::Http), | ||
| 51 | "git" => Some(ServerProtocol::Git), | ||
| 52 | _ => None, | ||
| 53 | }; | ||
| 54 | } else if name == "user" { | ||
| 55 | user = Some(value.to_string()); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | let mut parts: Vec<&str> = url[8..] | ||
| 60 | .split('?') | ||
| 61 | .next() | ||
| 62 | .unwrap_or("") | ||
| 63 | .split('/') | ||
| 64 | .collect(); | ||
| 65 | |||
| 66 | // extract optional protocol | ||
| 67 | if protocol.is_none() { | ||
| 68 | let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?; | ||
| 69 | let protocol_str = if let Some(at_index) = part.find('@') { | ||
| 70 | user = Some(part[..at_index].to_string()); | ||
| 71 | &part[at_index + 1..] | ||
| 72 | } else { | ||
| 73 | part | ||
| 74 | }; | ||
| 75 | protocol = match protocol_str { | ||
| 76 | "ssh" => Some(ServerProtocol::Ssh), | ||
| 77 | "https" => Some(ServerProtocol::Https), | ||
| 78 | "http" => Some(ServerProtocol::Http), | ||
| 79 | "git" => Some(ServerProtocol::Git), | ||
| 80 | _ => protocol, | ||
| 81 | }; | ||
| 82 | if protocol.is_some() { | ||
| 83 | parts.remove(0); | ||
| 84 | } | ||
| 85 | } | ||
| 86 | // extract naddr npub/<optional-relays>/identifer | ||
| 87 | let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?; | ||
| 88 | // naddr used | ||
| 89 | if let Ok(coordinate) = Coordinate::parse(part) { | ||
| 90 | if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) { | ||
| 91 | coordinates.insert(coordinate); | ||
| 92 | } else { | ||
| 93 | bail!("naddr doesnt point to a git repository announcement"); | ||
| 94 | } | ||
| 95 | // npub/<optional-relays>/identifer used | ||
| 96 | } else if let Ok(public_key) = PublicKey::parse(part) { | ||
| 97 | parts.remove(0); | ||
| 98 | let identifier = parts | ||
| 99 | .pop() | ||
| 100 | .context("nostr url must have an identifier eg. nostr://npub123/repo-identifier")? | ||
| 101 | .to_string(); | ||
| 102 | for relay in parts { | ||
| 103 | let mut decoded = urlencoding::decode(relay) | ||
| 104 | .context("could not parse relays in nostr git url")? | ||
| 105 | .to_string(); | ||
| 106 | if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") { | ||
| 107 | decoded = format!("wss://{decoded}"); | ||
| 108 | } | ||
| 109 | let url = | ||
| 110 | Url::parse(&decoded).context("could not parse relays in nostr git url")?; | ||
| 111 | relays.push(url.to_string()); | ||
| 112 | } | ||
| 113 | coordinates.insert(Coordinate { | ||
| 114 | identifier, | ||
| 115 | public_key, | ||
| 116 | kind: nostr_sdk::Kind::GitRepoAnnouncement, | ||
| 117 | relays, | ||
| 118 | }); | ||
| 119 | } else { | ||
| 120 | bail!(INCORRECT_NOSTR_URL_FORMAT_ERROR); | ||
| 121 | } | ||
| 122 | |||
| 123 | Ok(Self { | ||
| 124 | coordinates, | ||
| 125 | protocol, | ||
| 126 | user, | ||
| 127 | }) | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | /** produce error when using local repo or custom protocols */ | ||
| 132 | pub fn convert_clone_url_to_https(url: &str) -> Result<String> { | ||
| 133 | // Strip credentials if present | ||
| 134 | let stripped_url = strip_credentials(url); | ||
| 135 | |||
| 136 | // Check if the URL is already in HTTPS format | ||
| 137 | if stripped_url.starts_with("https://") { | ||
| 138 | return Ok(stripped_url); | ||
| 139 | } | ||
| 140 | // Convert http:// to https:// | ||
| 141 | else if stripped_url.starts_with("http://") { | ||
| 142 | return Ok(stripped_url.replace("http://", "https://")); | ||
| 143 | } | ||
| 144 | // Check if the URL starts with SSH | ||
| 145 | else if stripped_url.starts_with("ssh://") { | ||
| 146 | // Convert SSH to HTTPS | ||
| 147 | let parts: Vec<&str> = stripped_url | ||
| 148 | .trim_start_matches("ssh://") | ||
| 149 | .split('/') | ||
| 150 | .collect(); | ||
| 151 | if parts.len() >= 2 { | ||
| 152 | // Construct the HTTPS URL | ||
| 153 | return Ok(format!("https://{}/{}", parts[0], parts[1..].join("/"))); | ||
| 154 | } | ||
| 155 | bail!("Invalid SSH URL format: {}", url); | ||
| 156 | } | ||
| 157 | // Convert ftp:// to https:// | ||
| 158 | else if stripped_url.starts_with("ftp://") { | ||
| 159 | return Ok(stripped_url.replace("ftp://", "https://")); | ||
| 160 | } | ||
| 161 | // Convert git:// to https:// | ||
| 162 | else if stripped_url.starts_with("git://") { | ||
| 163 | return Ok(stripped_url.replace("git://", "https://")); | ||
| 164 | } | ||
| 165 | |||
| 166 | // If the URL is neither HTTPS, SSH, nor git@, return an error | ||
| 167 | bail!("Unsupported URL protocol: {}", url); | ||
| 168 | } | ||
| 169 | |||
| 170 | // Function to strip username and password from the URL | ||
| 171 | fn strip_credentials(url: &str) -> String { | ||
| 172 | if let Some(pos) = url.find("://") { | ||
| 173 | let (protocol, rest) = url.split_at(pos + 3); // Split at "://" | ||
| 174 | let rest_parts: Vec<&str> = rest.split('@').collect(); | ||
| 175 | if rest_parts.len() > 1 { | ||
| 176 | // If there are credentials, return the URL without them | ||
| 177 | return format!("{}{}", protocol, rest_parts[1]); | ||
| 178 | } | ||
| 179 | } else if let Some(at_pos) = url.find('@') { | ||
| 180 | // Handle user@host:path format | ||
| 181 | let (_, rest) = url.split_at(at_pos); | ||
| 182 | // This is a git@ syntax | ||
| 183 | let host_and_repo = &rest[1..]; // Skip the ':' | ||
| 184 | return format!("ssh://{}", host_and_repo.replace(':', "/")); | ||
| 185 | } | ||
| 186 | url.to_string() // Return the original URL if no credentials are found | ||
| 187 | } | ||
| 188 | |||
| 189 | #[cfg(test)] | ||
| 190 | mod tests { | ||
| 191 | use super::*; | ||
| 192 | mod convert_clone_url_to_https { | ||
| 193 | use super::*; | ||
| 194 | |||
| 195 | #[test] | ||
| 196 | fn test_https_url() { | ||
| 197 | let url = "https://github.com/user/repo.git"; | ||
| 198 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 199 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 200 | } | ||
| 201 | |||
| 202 | #[test] | ||
| 203 | fn test_http_url() { | ||
| 204 | let url = "http://github.com/user/repo.git"; | ||
| 205 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 206 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 207 | } | ||
| 208 | |||
| 209 | #[test] | ||
| 210 | fn test_http_url_with_credentials() { | ||
| 211 | let url = "http://username:password@github.com/user/repo.git"; | ||
| 212 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 213 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 214 | } | ||
| 215 | |||
| 216 | #[test] | ||
| 217 | fn test_git_at_url() { | ||
| 218 | let url = "git@github.com:user/repo.git"; | ||
| 219 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 220 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 221 | } | ||
| 222 | |||
| 223 | #[test] | ||
| 224 | fn test_user_at_url() { | ||
| 225 | let url = "user1@github.com:user/repo.git"; | ||
| 226 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 227 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 228 | } | ||
| 229 | |||
| 230 | #[test] | ||
| 231 | fn test_ssh_url() { | ||
| 232 | let url = "ssh://github.com/user/repo.git"; | ||
| 233 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 234 | assert_eq!(result, "https://github.com/user/repo.git"); | ||
| 235 | } | ||
| 236 | |||
| 237 | #[test] | ||
| 238 | fn test_ftp_url() { | ||
| 239 | let url = "ftp://example.com/repo.git"; | ||
| 240 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 241 | assert_eq!(result, "https://example.com/repo.git"); | ||
| 242 | } | ||
| 243 | |||
| 244 | #[test] | ||
| 245 | fn test_git_protocol_url() { | ||
| 246 | let url = "git://example.com/repo.git"; | ||
| 247 | let result = convert_clone_url_to_https(url).unwrap(); | ||
| 248 | assert_eq!(result, "https://example.com/repo.git"); | ||
| 249 | } | ||
| 250 | |||
| 251 | #[test] | ||
| 252 | fn test_invalid_url() { | ||
| 253 | let url = "unsupported://example.com/repo.git"; | ||
| 254 | let result = convert_clone_url_to_https(url); | ||
| 255 | assert!(result.is_err()); | ||
| 256 | } | ||
| 257 | } | ||
| 258 | |||
| 259 | mod nostr_git_url_paramemters_from_str { | ||
| 260 | use super::*; | ||
| 261 | |||
| 262 | fn get_model_coordinate(relays: bool) -> Coordinate { | ||
| 263 | Coordinate { | ||
| 264 | identifier: "ngit".to_string(), | ||
| 265 | public_key: PublicKey::parse( | ||
| 266 | "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", | ||
| 267 | ) | ||
| 268 | .unwrap(), | ||
| 269 | kind: nostr_sdk::Kind::GitRepoAnnouncement, | ||
| 270 | relays: if relays { | ||
| 271 | vec!["wss://nos.lol/".to_string()] | ||
| 272 | } else { | ||
| 273 | vec![] | ||
| 274 | }, | ||
| 275 | } | ||
| 276 | } | ||
| 277 | |||
| 278 | #[test] | ||
| 279 | fn from_naddr() -> Result<()> { | ||
| 280 | assert_eq!( | ||
| 281 | NostrUrlDecoded::from_str( | ||
| 282 | "nostr://naddr1qqzxuemfwsqs6amnwvaz7tmwdaejumr0dspzpgqgmmc409hm4xsdd74sf68a2uyf9pwel4g9mfdg8l5244t6x4jdqvzqqqrhnym0k2qj" | ||
| 283 | )?, | ||
| 284 | NostrUrlDecoded { | ||
| 285 | coordinates: HashSet::from([Coordinate { | ||
| 286 | identifier: "ngit".to_string(), | ||
| 287 | public_key: PublicKey::parse( | ||
| 288 | "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", | ||
| 289 | ) | ||
| 290 | .unwrap(), | ||
| 291 | kind: nostr_sdk::Kind::GitRepoAnnouncement, | ||
| 292 | relays: vec!["wss://nos.lol".to_string()], // wont add the slash | ||
| 293 | }]), | ||
| 294 | protocol: None, | ||
| 295 | user: None, | ||
| 296 | }, | ||
| 297 | ); | ||
| 298 | Ok(()) | ||
| 299 | } | ||
| 300 | mod from_npub_slash_identifier { | ||
| 301 | use super::*; | ||
| 302 | |||
| 303 | #[test] | ||
| 304 | fn without_relay() -> Result<()> { | ||
| 305 | assert_eq!( | ||
| 306 | NostrUrlDecoded::from_str( | ||
| 307 | "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit" | ||
| 308 | )?, | ||
| 309 | NostrUrlDecoded { | ||
| 310 | coordinates: HashSet::from([get_model_coordinate(false)]), | ||
| 311 | protocol: None, | ||
| 312 | user: None, | ||
| 313 | }, | ||
| 314 | ); | ||
| 315 | Ok(()) | ||
| 316 | } | ||
| 317 | |||
| 318 | mod with_url_parameters { | ||
| 319 | |||
| 320 | use super::*; | ||
| 321 | |||
| 322 | #[test] | ||
| 323 | fn with_relay_without_scheme_defaults_to_wss() -> Result<()> { | ||
| 324 | assert_eq!( | ||
| 325 | NostrUrlDecoded::from_str( | ||
| 326 | "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay=nos.lol" | ||
| 327 | )?, | ||
| 328 | NostrUrlDecoded { | ||
| 329 | coordinates: HashSet::from([get_model_coordinate(true)]), | ||
| 330 | protocol: None, | ||
| 331 | user: None, | ||
| 332 | }, | ||
| 333 | ); | ||
| 334 | Ok(()) | ||
| 335 | } | ||
| 336 | |||
| 337 | #[test] | ||
| 338 | fn with_encoded_relay() -> Result<()> { | ||
| 339 | assert_eq!( | ||
| 340 | NostrUrlDecoded::from_str(&format!( | ||
| 341 | "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}", | ||
| 342 | urlencoding::encode("wss://nos.lol") | ||
| 343 | ))?, | ||
| 344 | NostrUrlDecoded { | ||
| 345 | coordinates: HashSet::from([get_model_coordinate(true)]), | ||
| 346 | protocol: None, | ||
| 347 | user: None, | ||
| 348 | }, | ||
| 349 | ); | ||
| 350 | Ok(()) | ||
| 351 | } | ||
| 352 | #[test] | ||
| 353 | fn with_multiple_encoded_relays() -> Result<()> { | ||
| 354 | assert_eq!( | ||
| 355 | NostrUrlDecoded::from_str(&format!( | ||
| 356 | "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?relay={}&relay1={}", | ||
| 357 | urlencoding::encode("wss://nos.lol"), | ||
| 358 | urlencoding::encode("wss://relay.damus.io"), | ||
| 359 | ))?, | ||
| 360 | NostrUrlDecoded { | ||
| 361 | coordinates: HashSet::from([Coordinate { | ||
| 362 | identifier: "ngit".to_string(), | ||
| 363 | public_key: PublicKey::parse( | ||
| 364 | "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", | ||
| 365 | ) | ||
| 366 | .unwrap(), | ||
| 367 | kind: nostr_sdk::Kind::GitRepoAnnouncement, | ||
| 368 | relays: vec![ | ||
| 369 | "wss://nos.lol/".to_string(), | ||
| 370 | "wss://relay.damus.io/".to_string(), | ||
| 371 | ], | ||
| 372 | }]), | ||
| 373 | protocol: None, | ||
| 374 | user: None, | ||
| 375 | }, | ||
| 376 | ); | ||
| 377 | Ok(()) | ||
| 378 | } | ||
| 379 | |||
| 380 | #[test] | ||
| 381 | fn with_server_protocol() -> Result<()> { | ||
| 382 | assert_eq!( | ||
| 383 | NostrUrlDecoded::from_str( | ||
| 384 | "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?protocol=ssh" | ||
| 385 | )?, | ||
| 386 | NostrUrlDecoded { | ||
| 387 | coordinates: HashSet::from([get_model_coordinate(false)]), | ||
| 388 | protocol: Some(ServerProtocol::Ssh), | ||
| 389 | user: None, | ||
| 390 | }, | ||
| 391 | ); | ||
| 392 | Ok(()) | ||
| 393 | } | ||
| 394 | #[test] | ||
| 395 | fn with_server_protocol_and_user() -> Result<()> { | ||
| 396 | assert_eq!( | ||
| 397 | NostrUrlDecoded::from_str( | ||
| 398 | "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit?protocol=ssh&user=fred" | ||
| 399 | )?, | ||
| 400 | NostrUrlDecoded { | ||
| 401 | coordinates: HashSet::from([get_model_coordinate(false)]), | ||
| 402 | protocol: Some(ServerProtocol::Ssh), | ||
| 403 | user: Some("fred".to_string()), | ||
| 404 | }, | ||
| 405 | ); | ||
| 406 | Ok(()) | ||
| 407 | } | ||
| 408 | } | ||
| 409 | mod with_parameters_embedded_with_slashes { | ||
| 410 | use super::*; | ||
| 411 | |||
| 412 | #[test] | ||
| 413 | fn with_relay_without_scheme_defaults_to_wss() -> Result<()> { | ||
| 414 | assert_eq!( | ||
| 415 | NostrUrlDecoded::from_str( | ||
| 416 | "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/nos.lol/ngit" | ||
| 417 | )?, | ||
| 418 | NostrUrlDecoded { | ||
| 419 | coordinates: HashSet::from([get_model_coordinate(true)]), | ||
| 420 | protocol: None, | ||
| 421 | user: None, | ||
| 422 | }, | ||
| 423 | ); | ||
| 424 | Ok(()) | ||
| 425 | } | ||
| 426 | |||
| 427 | #[test] | ||
| 428 | fn with_encoded_relay() -> Result<()> { | ||
| 429 | assert_eq!( | ||
| 430 | NostrUrlDecoded::from_str(&format!( | ||
| 431 | "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/{}/ngit", | ||
| 432 | urlencoding::encode("wss://nos.lol") | ||
| 433 | ))?, | ||
| 434 | NostrUrlDecoded { | ||
| 435 | coordinates: HashSet::from([get_model_coordinate(true)]), | ||
| 436 | protocol: None, | ||
| 437 | user: None, | ||
| 438 | }, | ||
| 439 | ); | ||
| 440 | Ok(()) | ||
| 441 | } | ||
| 442 | #[test] | ||
| 443 | fn with_multiple_encoded_relays() -> Result<()> { | ||
| 444 | assert_eq!( | ||
| 445 | NostrUrlDecoded::from_str(&format!( | ||
| 446 | "nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/{}/{}/ngit", | ||
| 447 | urlencoding::encode("wss://nos.lol"), | ||
| 448 | urlencoding::encode("wss://relay.damus.io"), | ||
| 449 | ))?, | ||
| 450 | NostrUrlDecoded { | ||
| 451 | coordinates: HashSet::from([Coordinate { | ||
| 452 | identifier: "ngit".to_string(), | ||
| 453 | public_key: PublicKey::parse( | ||
| 454 | "npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr", | ||
| 455 | ) | ||
| 456 | .unwrap(), | ||
| 457 | kind: nostr_sdk::Kind::GitRepoAnnouncement, | ||
| 458 | relays: vec![ | ||
| 459 | "wss://nos.lol/".to_string(), | ||
| 460 | "wss://relay.damus.io/".to_string(), | ||
| 461 | ], | ||
| 462 | }]), | ||
| 463 | protocol: None, | ||
| 464 | user: None, | ||
| 465 | }, | ||
| 466 | ); | ||
| 467 | Ok(()) | ||
| 468 | } | ||
| 469 | |||
| 470 | #[test] | ||
| 471 | fn with_server_protocol() -> Result<()> { | ||
| 472 | assert_eq!( | ||
| 473 | NostrUrlDecoded::from_str( | ||
| 474 | "nostr://ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit" | ||
| 475 | )?, | ||
| 476 | NostrUrlDecoded { | ||
| 477 | coordinates: HashSet::from([get_model_coordinate(false)]), | ||
| 478 | protocol: Some(ServerProtocol::Ssh), | ||
| 479 | user: None, | ||
| 480 | }, | ||
| 481 | ); | ||
| 482 | Ok(()) | ||
| 483 | } | ||
| 484 | #[test] | ||
| 485 | fn with_server_protocol_and_user() -> Result<()> { | ||
| 486 | assert_eq!( | ||
| 487 | NostrUrlDecoded::from_str( | ||
| 488 | "nostr://fred@ssh/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit" | ||
| 489 | )?, | ||
| 490 | NostrUrlDecoded { | ||
| 491 | coordinates: HashSet::from([get_model_coordinate(false)]), | ||
| 492 | protocol: Some(ServerProtocol::Ssh), | ||
| 493 | user: Some("fred".to_string()), | ||
| 494 | }, | ||
| 495 | ); | ||
| 496 | Ok(()) | ||
| 497 | } | ||
| 498 | } | ||
| 499 | } | ||
| 500 | } | ||
| 501 | } | ||