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#[derive(Debug, PartialEq, Eq, Hash, Clone)]
9pub struct RemoteGitUrl {
10 pub(crate) url: Url,
11 host_str: String,
12 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 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 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}