use anyhow::Result;
use log::debug;
use regex::Regex;
use std::fmt;
use std::str::FromStr;
use strum_macros::{Display, EnumString, EnumVariantNames};
use url::Url;
#[derive(Debug, PartialEq, EnumString, EnumVariantNames, Clone, Display, Copy)]
#[strum(serialize_all = "kebab_case")]
pub enum Scheme {
Unspecified,
File,
Http,
Https,
Ssh,
Git,
#[strum(serialize = "git+ssh")]
GitSsh,
}
#[derive(Debug, PartialEq, Clone)]
pub struct GitUrl {
pub host: Option<String>,
pub name: String,
pub owner: Option<String>,
pub organization: Option<String>,
pub fullname: String,
pub scheme: Scheme,
pub user: Option<String>,
pub token: Option<String>,
pub port: Option<u16>,
pub path: String,
pub git_suffix: bool,
pub scheme_prefix: bool,
}
impl fmt::Display for GitUrl {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let scheme_prefix = match self.scheme_prefix {
true => format!("{}://", self.scheme),
false => format!(""),
};
let auth_info = match self.scheme {
Scheme::Ssh | Scheme::Git | Scheme::GitSsh => {
if let Some(user) = &self.user {
format!("{}@", user)
} else {
format!("")
}
}
Scheme::Http | Scheme::Https => match (&self.user, &self.token) {
(Some(user), Some(token)) => format!("{}:{}@", user, token),
(Some(user), None) => format!("{}@", user),
(None, Some(token)) => format!("{}@", token),
(None, None) => format!(""),
},
_ => format!(""),
};
let host = match &self.host {
Some(host) => format!("{}", host),
None => format!(""),
};
let port = match &self.port {
Some(p) => format!(":{}", p),
None => format!(""),
};
let path = match &self.scheme {
Scheme::Ssh => {
if self.port.is_some() {
format!("/{}", &self.path)
} else {
format!(":{}", &self.path)
}
}
_ => format!("{}", &self.path),
};
let git_url_str = format!("{}{}{}{}{}", scheme_prefix, auth_info, host, port, path);
write!(f, "{}", git_url_str)
}
}
impl Default for GitUrl {
fn default() -> Self {
GitUrl {
host: None,
name: "".to_string(),
owner: None,
organization: None,
fullname: "".to_string(),
scheme: Scheme::Unspecified,
user: None,
token: None,
port: None,
path: "".to_string(),
git_suffix: false,
scheme_prefix: false,
}
}
}
impl GitUrl {
pub fn trim_auth(&self) -> GitUrl {
let mut new_giturl = self.clone();
new_giturl.user = None;
new_giturl.token = None;
new_giturl
}
pub fn parse(url: &str) -> Result<GitUrl> {
let normalized = normalize_url(url).expect("Url normalization failed");
let scheme = Scheme::from_str(normalized.scheme())
.expect(&format!("Scheme unsupported: {:?}", normalized.scheme()));
let urlpath = match &scheme {
Scheme::Ssh => {
normalized.path()[1..].to_string()
}
_ => normalized.path().to_string(),
};
let git_suffix_check = &urlpath.ends_with(".git");
debug!("The urlpath: {:?}", &urlpath);
let splitpath = &urlpath.rsplit_terminator("/").collect::<Vec<&str>>();
debug!("rsplit results for metadata: {:?}", splitpath);
let name = splitpath[0].trim_end_matches(".git").to_string();
let (owner, organization, fullname) = match &scheme {
Scheme::File => (None::<String>, None::<String>, name.clone()),
_ => {
let mut fullname: Vec<&str> = Vec::new();
let hosts_w_organization_in_path = vec!["dev.azure.com", "ssh.dev.azure.com"];
match hosts_w_organization_in_path.contains(&normalized.clone().host_str().unwrap())
{
true => {
debug!("Found a git provider with an org");
match &scheme {
Scheme::Ssh => {
fullname.push(splitpath[2].clone());
fullname.push(splitpath[1].clone());
fullname.push(splitpath[0].clone());
(
Some(splitpath[1].to_string()),
Some(splitpath[2].to_string()),
fullname.join("/").to_string(),
)
}
Scheme::Https => {
fullname.push(splitpath[3].clone());
fullname.push(splitpath[2].clone());
fullname.push(splitpath[0].clone());
(
Some(splitpath[2].to_string()),
Some(splitpath[3].to_string()),
fullname.join("/").to_string(),
)
}
_ => panic!("Scheme not supported for host"),
}
}
false => {
fullname.push(splitpath[1]);
fullname.push(name.as_str());
(
Some(splitpath[1].to_string()),
None::<String>,
fullname.join("/").to_string(),
)
}
}
}
};
Ok(GitUrl {
host: match normalized.host_str() {
Some(h) => Some(h.to_string()),
None => None,
},
name: name,
owner: owner,
organization: organization,
fullname: fullname,
scheme: Scheme::from_str(normalized.scheme()).expect("Scheme unsupported"),
user: match normalized.username().to_string().len() {
0 => None,
_ => Some(normalized.username().to_string()),
},
token: match normalized.password() {
Some(p) => Some(p.to_string()),
None => None,
},
port: normalized.port(),
path: urlpath,
git_suffix: *git_suffix_check,
scheme_prefix: url.contains("://"),
..Default::default()
})
}
}
fn normalize_ssh_url(url: &str) -> Result<Url> {
let u = url.split(":").collect::<Vec<&str>>();
match u.len() {
2 => {
debug!("Normalizing ssh url: {:?}", u);
normalize_url(&format!("ssh://{}/{}", u[0], u[1]))
}
3 => {
debug!("Normalizing ssh url with ports: {:?}", u);
normalize_url(&format!("ssh://{}:{}/{}", u[0], u[1], u[2]))
}
_default => {
panic!("SSH normalization pattern not covered for: {:?}", u);
}
}
}
fn normalize_file_path(filepath: &str) -> Result<Url> {
let fp = Url::from_file_path(filepath);
match fp {
Ok(path) => Ok(path),
Err(_e) => {
Ok(normalize_url(&format!("file://{}", filepath))
.expect("file:// normalization failed"))
}
}
}
pub fn normalize_url(url: &str) -> Result<Url> {
debug!("Processing: {:?}", &url);
let trim_url = url.trim_end_matches("/");
let url_parse = Url::parse(&trim_url);
Ok(match url_parse {
Ok(u) => {
match Scheme::from_str(u.scheme()) {
Ok(_p) => u,
Err(_e) => {
debug!("Scheme parse fail. Assuming a userless ssh url");
normalize_ssh_url(trim_url)?
}
}
}
Err(_e) => {
let re = Regex::new(r"^\S+(@)\S+(:).*$")?;
match re.is_match(&trim_url) {
true => {
debug!("Scheme::SSH match for normalization");
normalize_ssh_url(trim_url)?
}
false => {
debug!("Scheme::File match for normalization");
normalize_file_path(&format!("{}", trim_url))?
}
}
}
})
}