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:\n{0}")]
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}
44
45impl FromStr for RemoteGitUrl {
46    type Err = RemoteGitUrlParseError;
47
48    fn from_str(s: &str) -> Result<Self, Self::Err> {
49        let url: Url = s.parse()?;
50        let scheme = url.scheme();
51        if !matches!(scheme, "ssh" | "git" | "http" | "https" | "ftp" | "ftps") {
52            return Err(RemoteGitUrlParseError::UnsupportedRemoteScheme {
53                scheme: scheme.into(),
54                url,
55            });
56        }
57        let Some(host_str) = url.host_str() else {
58            return Err(RemoteGitUrlParseError::MissingHostName(url));
59        };
60        Ok(RemoteGitUrl {
61            host_str: String::from(host_str),
62            url,
63            url_str: String::from(s),
64        })
65    }
66}
67
68impl Display for RemoteGitUrl {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        self.url_str.fmt(f)
71    }
72}
73
74impl<'de> Deserialize<'de> for RemoteGitUrl {
75    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
76    where
77        D: Deserializer<'de>,
78    {
79        String::deserialize(deserializer)?
80            .parse()
81            .map_err(de::Error::custom)
82    }
83}
84
85impl Serialize for RemoteGitUrl {
86    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
87    where
88        S: serde::Serializer,
89    {
90        self.to_string().serialize(serializer)
91    }
92}