remote.rs

 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}