use serde::{de, Deserialize, Deserializer, Serialize};
use std::{fmt::Display, hash::Hash, str::FromStr};
use thiserror::Error;
use url::Url;
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct RemoteGitUrl {
pub(crate) url: Url,
host_str: String,
url_str: String,
}
#[derive(Debug, Error)]
pub enum RemoteGitUrlParseError {
#[error("error parsing git URL:\n{0}")]
GitUrlParse(#[from] url::ParseError),
#[error("unsupported git remote scheme {scheme} in URL {url}")]
UnsupportedRemoteScheme { scheme: String, url: Url },
#[error("URL {0} missing host name")]
MissingHostName(Url),
}
impl RemoteGitUrl {
pub fn repo(&self) -> &str {
let url = &self.url;
url.path_segments()
.into_iter()
.flatten()
.next_back()
.map(|part| part.strip_suffix(".git").unwrap_or(part))
.unwrap_or(&self.host_str)
}
pub fn owner(&self) -> Option<&str> {
self.url.path_segments().into_iter().flatten().rev().nth(1)
}
}
impl FromStr for RemoteGitUrl {
type Err = RemoteGitUrlParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let url: Url = s.parse()?;
let scheme = url.scheme();
if !matches!(scheme, "ssh" | "git" | "http" | "https" | "ftp" | "ftps") {
return Err(RemoteGitUrlParseError::UnsupportedRemoteScheme {
scheme: scheme.into(),
url,
});
}
let Some(host_str) = url.host_str() else {
return Err(RemoteGitUrlParseError::MissingHostName(url));
};
Ok(RemoteGitUrl {
host_str: String::from(host_str),
url,
url_str: String::from(s),
})
}
}
impl Display for RemoteGitUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.url_str.fmt(f)
}
}
impl<'de> Deserialize<'de> for RemoteGitUrl {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse()
.map_err(de::Error::custom)
}
}
impl Serialize for RemoteGitUrl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_string().serialize(serializer)
}
}