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