use super::{
ForgeKind, ForgeRemote, GitHubReviewRequestAdapter, GitLabReviewRequestAdapter,
ReviewRequestError,
};
pub fn detect_remote(repo_url: &str) -> Result<ForgeRemote, ReviewRequestError> {
if let Some(remote) = GitHubReviewRequestAdapter::detect_remote(repo_url) {
return Ok(remote);
}
if let Some(remote) = GitLabReviewRequestAdapter::detect_remote(repo_url) {
return Ok(remote);
}
Err(ReviewRequestError::UnsupportedRemote {
repo_url: display_safe_remote_url(repo_url),
})
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct ParsedRemote {
pub(crate) host: String,
pub(crate) namespace: String,
pub(crate) project: String,
pub(crate) repo_url: String,
pub(crate) web_url: String,
}
impl ParsedRemote {
pub(crate) fn into_forge_remote(self, forge_kind: ForgeKind) -> ForgeRemote {
ForgeRemote {
command_working_directory: None,
forge_kind,
host: self.host,
namespace: self.namespace,
project: self.project,
repo_url: self.repo_url,
web_url: self.web_url,
}
}
fn from_parts(
repo_url: &str,
host: &str,
path: &str,
strip_transport_port: bool,
) -> Option<ParsedRemote> {
let host = host.trim().trim_matches('/').to_ascii_lowercase();
let host = if strip_transport_port {
strip_port(&host).to_string()
} else {
host
};
let path = path.trim().trim_matches('/').trim_end_matches(".git");
if host.is_empty() || path.is_empty() {
return None;
}
let (namespace, project) = path.rsplit_once('/')?;
if namespace.is_empty() || project.is_empty() {
return None;
}
Some(Self {
host: host.clone(),
namespace: namespace.to_string(),
project: project.to_string(),
repo_url: display_safe_remote_url(repo_url),
web_url: format!("https://{host}/{path}"),
})
}
}
pub(crate) fn parse_remote_url(repo_url: &str) -> Option<ParsedRemote> {
let trimmed_url = repo_url.trim().trim_end_matches('/');
if trimmed_url.is_empty() {
return None;
}
if let Some((authority, path)) = trimmed_url.split_once(':')
&& authority.contains('@')
{
let host = strip_userinfo(authority);
return ParsedRemote::from_parts(trimmed_url, host, path, true);
}
let (scheme, scheme_rest) = trimmed_url.split_once("://")?;
let scheme_rest = scheme_rest.strip_prefix("git@").unwrap_or(scheme_rest);
let (authority, path) = scheme_rest.split_once('/')?;
let host = strip_userinfo(authority);
let strip_transport_port = scheme.eq_ignore_ascii_case("ssh");
ParsedRemote::from_parts(trimmed_url, host, path, strip_transport_port)
}
pub(crate) fn strip_port(host: &str) -> &str {
host.split(':').next().unwrap_or(host)
}
fn display_safe_remote_url(repo_url: &str) -> String {
let trimmed_url = repo_url.trim();
let Some((scheme, scheme_rest)) = trimmed_url.split_once("://") else {
if let Some((authority, suffix)) = trimmed_url.split_once(':')
&& authority.contains('@')
{
return format!("{}:{suffix}", strip_userinfo(authority));
}
return trimmed_url.to_string();
};
let (authority, suffix) = scheme_rest
.split_once('/')
.map_or((scheme_rest, ""), |(authority, path)| (authority, path));
let authority = strip_userinfo(authority);
if suffix.is_empty() {
return format!("{scheme}://{authority}");
}
format!("{scheme}://{authority}/{suffix}")
}
fn strip_userinfo(authority: &str) -> &str {
authority
.rsplit_once('@')
.map_or(authority, |(_, host)| host)
}
#[cfg(test)]
#[path = "remote_test.rs"]
mod tests;