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