1use serde::{de, Deserialize, Deserializer, Serialize};
2use std::{fmt::Display, hash::Hash, str::FromStr};
3use thiserror::Error;
4use url::Url;
5
6#[derive(Debug, PartialEq, Eq, Hash, Clone)]
8pub struct RemoteGitUrl {
9 pub(crate) url: Url,
10 host_str: String,
11 url_str: String,
13}
14
15#[derive(Debug, Error)]
16pub enum RemoteGitUrlParseError {
17 #[error("error parsing git URL:\n{0}")]
18 GitUrlParse(#[from] url::ParseError),
19 #[error("unsupported git remote scheme {scheme} in URL {url}")]
20 UnsupportedRemoteScheme { scheme: String, url: Url },
21 #[error("URL {0} missing host name")]
22 MissingHostName(Url),
23}
24
25impl RemoteGitUrl {
26 pub fn repo(&self) -> &str {
30 let url = &self.url;
31 url.path_segments()
32 .into_iter()
33 .flatten()
34 .next_back()
35 .map(|part| part.strip_suffix(".git").unwrap_or(part))
36 .unwrap_or(&self.host_str)
37 }
38 pub fn owner(&self) -> Option<&str> {
40 self.url.path_segments().into_iter().flatten().rev().nth(1)
41 }
42}
43
44impl FromStr for RemoteGitUrl {
45 type Err = RemoteGitUrlParseError;
46
47 fn from_str(s: &str) -> Result<Self, Self::Err> {
48 let url: Url = s.parse()?;
49 let scheme = url.scheme();
50 if !matches!(scheme, "ssh" | "git" | "http" | "https" | "ftp" | "ftps") {
51 return Err(RemoteGitUrlParseError::UnsupportedRemoteScheme {
52 scheme: scheme.into(),
53 url,
54 });
55 }
56 let Some(host_str) = url.host_str() else {
57 return Err(RemoteGitUrlParseError::MissingHostName(url));
58 };
59 Ok(RemoteGitUrl {
60 host_str: String::from(host_str),
61 url,
62 url_str: String::from(s),
63 })
64 }
65}
66
67impl Display for RemoteGitUrl {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 self.url_str.fmt(f)
70 }
71}
72
73impl<'de> Deserialize<'de> for RemoteGitUrl {
74 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
75 where
76 D: Deserializer<'de>,
77 {
78 String::deserialize(deserializer)?
79 .parse()
80 .map_err(de::Error::custom)
81 }
82}
83
84impl Serialize for RemoteGitUrl {
85 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
86 where
87 S: serde::Serializer,
88 {
89 self.to_string().serialize(serializer)
90 }
91}