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")]
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 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}