use std::path::Path;
use url::Url;
const TRANSPORT_SCHEMES: [&str; 6] = ["file", "git", "git+ssh", "http", "https", "ssh"];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GitRemoteKind {
Url,
Scp,
Path,
}
pub fn git_remote_kind(remote: &str) -> GitRemoteKind {
if transport_url(remote).is_some() {
GitRemoteKind::Url
} else if scp_parts(remote).is_some() {
GitRemoteKind::Scp
} else {
GitRemoteKind::Path
}
}
pub fn normalize_git_remote(remote: &str) -> Option<Url> {
let path = Path::new(remote);
if path.is_absolute() {
return Url::from_file_path(path).ok();
}
if let Some(url) = transport_url(remote) {
return Some(url);
}
if let Some((host, path)) = scp_parts(remote) {
let path = path.strip_prefix('/').unwrap_or(path);
return Url::parse(&format!("ssh://{host}/{path}")).ok();
}
Url::from_file_path(path.canonicalize().ok()?).ok()
}
fn transport_url(remote: &str) -> Option<Url> {
let url = Url::parse(remote).ok()?;
TRANSPORT_SCHEMES.contains(&url.scheme()).then_some(url)
}
fn scp_parts(remote: &str) -> Option<(&str, &str)> {
if starts_with_windows_drive(remote) {
return None;
}
let (host, path) = remote.split_once(':')?;
(!host.is_empty() && !host.contains(['/', '\\']) && !path.is_empty() && !path.starts_with("//"))
.then_some((host, path))
}
fn starts_with_windows_drive(remote: &str) -> bool {
let mut bytes = remote.bytes();
matches!(
(bytes.next(), bytes.next()),
(Some(b'A'..=b'Z' | b'a'..=b'z'), Some(b':'))
)
}
#[cfg(test)]
mod tests {
use super::*;
fn normalized(remote: &str) -> Option<String> {
normalize_git_remote(remote).map(|url| url.to_string())
}
#[test]
fn scp_syntax_becomes_an_ssh_url() {
assert_eq!(
normalized("git@github.com:stjudecloud/workflows.git").as_deref(),
Some("ssh://git@github.com/stjudecloud/workflows.git")
);
}
#[test]
fn scp_syntax_without_a_user_becomes_an_ssh_url() {
assert_eq!(
normalized("github.com:stjudecloud/workflows.git").as_deref(),
Some("ssh://github.com/stjudecloud/workflows.git")
);
}
#[test]
fn a_server_absolute_scp_path_keeps_one_separator() {
assert_eq!(
normalized("git@example.com:/srv/git/workflows.git").as_deref(),
Some("ssh://git@example.com/srv/git/workflows.git")
);
}
#[test]
fn transport_urls_pass_through_unchanged() {
assert_eq!(
normalized("https://github.com/stjudecloud/workflows.git").as_deref(),
Some("https://github.com/stjudecloud/workflows.git")
);
}
#[test]
fn absolute_paths_become_file_urls() {
let remote = if cfg!(windows) {
"C:\\repos\\workflows"
} else {
"/repos/workflows"
};
let normalized = normalized(remote);
assert!(
normalized
.as_deref()
.is_some_and(|url| url.starts_with("file://")),
"expected a file URL, got {normalized:?}"
);
}
#[test]
fn relative_paths_that_do_not_exist_are_rejected() {
assert_eq!(normalized("relative/path-that-does-not-exist"), None);
}
#[test]
fn scp_syntax_does_not_swallow_a_windows_drive_path() {
assert_eq!(git_remote_kind("C:\\repos\\workflows"), GitRemoteKind::Path);
}
#[test]
fn a_bare_host_and_path_is_scp_syntax_rather_than_a_url() {
assert_eq!(
git_remote_kind("github.com:stjudecloud/workflows.git"),
GitRemoteKind::Scp
);
}
}