use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) enum RepoIdentity {
Remote(String),
Local(String),
}
impl RepoIdentity {
pub(crate) fn from_remote_url(url: &str) -> Option<RepoIdentity> {
normalize_remote(url).map(RepoIdentity::Remote)
}
pub(crate) fn local(path: &str) -> RepoIdentity {
RepoIdentity::Local(path.to_string())
}
}
impl fmt::Display for RepoIdentity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RepoIdentity::Remote(s) => f.write_str(s),
RepoIdentity::Local(p) => write!(f, "local:{p}"),
}
}
}
fn normalize_remote(url: &str) -> Option<String> {
let url = url.trim();
if url.is_empty() {
return None;
}
let rest = if let Some(stripped) = strip_scheme(url) {
stripped
} else if let Some((host_part, path)) = url.split_once(':') {
let host = host_part.rsplit('@').next().unwrap_or(host_part);
return assemble(host, path);
} else {
return None;
};
let (authority, path) = rest.split_once('/')?;
let host_with_user = authority;
let host = host_with_user.rsplit('@').next().unwrap_or(host_with_user);
let host = host.split(':').next().unwrap_or(host); assemble(host, path)
}
fn strip_scheme(url: &str) -> Option<&str> {
for scheme in ["https://", "http://", "ssh://", "git://"] {
if let Some(rest) = url.strip_prefix(scheme) {
return Some(rest);
}
}
None
}
fn assemble(host: &str, path: &str) -> Option<String> {
let host = host.trim().trim_matches('/');
let path = path.trim().trim_matches('/');
let path = path.strip_suffix(".git").unwrap_or(path);
if host.is_empty() || path.is_empty() {
return None;
}
Some(format!("{host}/{path}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalizes_scp_syntax() {
assert_eq!(
RepoIdentity::from_remote_url("git@github.com:dpep/rq.git"),
Some(RepoIdentity::Remote("github.com/dpep/rq".into()))
);
}
#[test]
fn normalizes_https() {
assert_eq!(
RepoIdentity::from_remote_url("https://github.com/dpep/rq.git"),
Some(RepoIdentity::Remote("github.com/dpep/rq".into()))
);
}
#[test]
fn normalizes_ssh_with_user_and_port() {
assert_eq!(
RepoIdentity::from_remote_url("ssh://git@github.com:22/dpep/rq"),
Some(RepoIdentity::Remote("github.com/dpep/rq".into()))
);
}
#[test]
fn forks_and_clones_share_identity() {
let a = RepoIdentity::from_remote_url("git@github.com:dpep/rq.git");
let b = RepoIdentity::from_remote_url("https://github.com/dpep/rq");
assert_eq!(a, b);
}
#[test]
fn empty_and_garbage_return_none() {
assert_eq!(RepoIdentity::from_remote_url(""), None);
assert_eq!(RepoIdentity::from_remote_url("not-a-url"), None);
}
#[test]
fn display_renders_each_variant() {
assert_eq!(
RepoIdentity::Remote("github.com/dpep/rq".into()).to_string(),
"github.com/dpep/rq"
);
assert_eq!(
RepoIdentity::local("/home/dpep/rq").to_string(),
"local:/home/dpep/rq"
);
}
}