Skip to main content

lux_lib/git/
url.rs

1use serde::{de, Deserialize, Deserializer, Serialize};
2use std::{fmt::Display, hash::Hash, str::FromStr};
3use thiserror::Error;
4use url::Url;
5
6/// GitUrl represents an input url that is a url used by git
7#[derive(Debug, PartialEq, Eq, Hash, Clone)]
8pub struct RemoteGitUrl {
9    pub(crate) url: Url,
10    host_str: String,
11    /// The raw URL string
12    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    /// Get the repo name, as the final component of the path, with any .git
27    /// suffix removed, or as the hostname, if there is no final path component,
28    /// or as a hash of the whole URL otherwise.
29    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    /// Get the repo owner, as second-final component of the path.
39    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}