upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/lib/git
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/git')
-rw-r--r--src/lib/git/identify_ahead_behind.rs196
-rw-r--r--src/lib/git/mod.rs266
-rw-r--r--src/lib/git/nostr_url.rs501
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 @@
1use anyhow::{Context, Result};
2use nostr_sdk::hashes::sha1::Hash as Sha1Hash;
3
4use super::{Repo, RepoActions};
5
6/**
7 * returns `(from_branch,to_branch,ahead,behind)`
8 */
9pub 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)]
67mod 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 @@
1use std::{ 1use std::{
2 collections::HashSet,
3 env::current_dir, 2 env::current_dir,
4 path::{Path, PathBuf}, 3 path::{Path, PathBuf},
5}; 4};
6 5
7use anyhow::{bail, Context, Result}; 6use anyhow::{bail, Context, Result};
8use git2::{DiffOptions, Oid, Revwalk}; 7use git2::{DiffOptions, Oid, Revwalk};
9use nostr::nips::nip01::Coordinate; 8pub use identify_ahead_behind::identify_ahead_behind;
10use nostr_sdk::{ 9use nostr_sdk::hashes::{sha1::Hash as Sha1Hash, Hash};
11 hashes::{sha1::Hash as Sha1Hash, Hash},
12 PublicKey, Url,
13};
14 10
15use crate::sub_commands::list::{get_commit_id_from_patch, tag_value}; 11use crate::git_events::{get_commit_id_from_patch, tag_value};
12pub mod identify_ahead_behind;
13pub mod nostr_url;
16 14
17pub struct Repo { 15pub 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)]
839pub enum ServerProtocol {
840 Ssh,
841 Https,
842 Http,
843 Git,
844}
845
846#[derive(Debug, PartialEq)]
847pub struct NostrUrlDecoded {
848 pub coordinates: HashSet<Coordinate>,
849 pub protocol: Option<ServerProtocol>,
850 pub user: Option<String>,
851}
852
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";
854
855impl 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 */
963pub 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
1002fn 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)]
1021mod tests { 837mod 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 @@
1use std::collections::HashSet;
2
3use anyhow::{bail, Context, Result};
4use nostr::nips::nip01::Coordinate;
5use nostr_sdk::{PublicKey, Url};
6
7#[derive(Debug, PartialEq)]
8pub enum ServerProtocol {
9 Ssh,
10 Https,
11 Http,
12 Git,
13}
14
15#[derive(Debug, PartialEq)]
16pub struct NostrUrlDecoded {
17 pub coordinates: HashSet<Coordinate>,
18 pub protocol: Option<ServerProtocol>,
19 pub user: Option<String>,
20}
21
22static 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
24impl 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 */
132pub 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
171fn 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)]
190mod 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}