Skip to main content

lux_lib/git/
url.rs

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