1use std::sync::LazyLock;
2
3use derive_more::Deref;
4use regex::Regex;
5use url::Url;
6
7/// The URL to a Git remote.
8#[derive(Debug, PartialEq, Eq, Clone, Deref)]
9pub struct RemoteUrl(Url);
10
11static USERNAME_REGEX: LazyLock<Regex> =
12 LazyLock::new(|| Regex::new(r"^[0-9a-zA-Z\-_]+@").expect("Failed to create USERNAME_REGEX"));
13
14impl std::str::FromStr for RemoteUrl {
15 type Err = url::ParseError;
16
17 fn from_str(input: &str) -> Result<Self, Self::Err> {
18 if USERNAME_REGEX.is_match(input) {
19 // Rewrite remote URLs like `git@github.com:user/repo.git` to `ssh://git@github.com/user/repo.git`
20 let ssh_url = format!("ssh://{}", input.replacen(':', "/", 1));
21 Ok(RemoteUrl(Url::parse(&ssh_url)?))
22 } else {
23 Ok(RemoteUrl(Url::parse(input)?))
24 }
25 }
26}
27
28#[cfg(test)]
29mod tests {
30 use pretty_assertions::assert_eq;
31
32 use super::*;
33
34 #[test]
35 fn test_parsing_valid_remote_urls() {
36 let valid_urls = vec![
37 (
38 "https://github.com/octocat/zed.git",
39 "https",
40 "github.com",
41 "/octocat/zed.git",
42 ),
43 (
44 "git@github.com:octocat/zed.git",
45 "ssh",
46 "github.com",
47 "/octocat/zed.git",
48 ),
49 (
50 "org-000000@github.com:octocat/zed.git",
51 "ssh",
52 "github.com",
53 "/octocat/zed.git",
54 ),
55 (
56 "ssh://git@github.com/octocat/zed.git",
57 "ssh",
58 "github.com",
59 "/octocat/zed.git",
60 ),
61 (
62 "file:///path/to/local/zed",
63 "file",
64 "",
65 "/path/to/local/zed",
66 ),
67 ];
68
69 for (input, expected_scheme, expected_host, expected_path) in valid_urls {
70 let parsed = input.parse::<RemoteUrl>().expect("failed to parse URL");
71 let url = parsed.0;
72 assert_eq!(
73 url.scheme(),
74 expected_scheme,
75 "unexpected scheme for {input:?}",
76 );
77 assert_eq!(
78 url.host_str().unwrap_or(""),
79 expected_host,
80 "unexpected host for {input:?}",
81 );
82 assert_eq!(url.path(), expected_path, "unexpected path for {input:?}");
83 }
84 }
85
86 #[test]
87 fn test_parsing_invalid_remote_urls() {
88 let invalid_urls = vec!["not_a_url", "http://"];
89
90 for url in invalid_urls {
91 assert!(
92 url.parse::<RemoteUrl>().is_err(),
93 "expected \"{url}\" to not parse as a Git remote URL",
94 );
95 }
96 }
97}