remote.rs

 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}