#[derive(Debug, PartialEq, Eq)]
pub struct GitUri {
pub user: String,
pub repo: String,
pub host: String,
}
fn take_while1(s: &str, pred: impl Fn(char) -> bool) -> Option<(&str, &str)> {
let end = s.find(|c| !pred(c)).unwrap_or(s.len());
(end > 0).then(|| s.split_at(end))
}
fn not_slash(c: char) -> bool {
c != '/'
}
#[must_use]
pub fn parse_git_uri(uri: &str) -> Option<GitUri> {
let uri = uri.strip_suffix(".git").unwrap_or(uri);
for scheme in ["https://", "http://"] {
if let Some(rest) = uri.strip_prefix(scheme) {
if let Some(rest) = rest.strip_prefix("github.com/") {
let (user, rest) = take_while1(rest, not_slash)?;
let rest = rest.strip_prefix('/')?;
let (repo, _) = take_while1(rest, not_slash)?;
return Some(GitUri {
user: user.into(),
repo: repo.into(),
host: "github.com".into(),
});
}
}
}
if let Some(rest) = uri.strip_prefix("git@github.com:") {
if let Some((user, rest)) = take_while1(rest, not_slash) {
if let Some(rest) = rest.strip_prefix('/') {
if let Some((repo, _)) = take_while1(rest, not_slash) {
return Some(GitUri {
user: user.into(),
repo: repo.into(),
host: "github.com".into(),
});
}
}
}
}
for scheme in ["https://", "http://"] {
if let Some(rest) = uri.strip_prefix(scheme) {
if let Some((host, rest)) = take_while1(rest, not_slash) {
if let Some(rest) = rest.strip_prefix('/') {
if let Some((user, rest)) = take_while1(rest, not_slash) {
if let Some(rest) = rest.strip_prefix('/') {
if let Some((repo, _)) = take_while1(rest, not_slash) {
return Some(GitUri {
user: user.into(),
repo: repo.into(),
host: host.into(),
});
}
}
}
}
}
}
}
if let Some(rest) = uri.strip_prefix("git@") {
if let Some((host, rest)) = take_while1(rest, |c| c != ':') {
if let Some(rest) = rest.strip_prefix(':') {
if let Some((user, rest)) = take_while1(rest, not_slash) {
if let Some(rest) = rest.strip_prefix('/') {
if let Some((repo, _)) = take_while1(rest, not_slash) {
return Some(GitUri {
user: user.into(),
repo: repo.into(),
host: host.into(),
});
}
}
}
}
}
}
None
}
#[must_use]
#[allow(
clippy::case_sensitive_file_extension_comparisons,
reason = "Ruby's end_with?('.git') is case-sensitive; case-folding would diverge from upstream"
)]
pub fn is_git_uri(arg: &str) -> bool {
arg.starts_with("https://")
|| arg.starts_with("http://")
|| arg.starts_with("git@")
|| arg.contains("github.com")
|| arg.contains("gitlab.com")
|| arg.ends_with(".git")
}
#[must_use]
pub fn generate_clone_directory_name(
git_uri: &str,
custom_name: Option<&str>,
date_prefix: &str,
) -> Option<String> {
if let Some(name) = custom_name {
if !name.is_empty() {
return Some(name.to_string());
}
}
let parsed = parse_git_uri(git_uri)?;
Some(format!("{date_prefix}-{}-{}", parsed.user, parsed.repo))
}
#[cfg(test)]
mod tests {
use super::*;
fn parts(uri: &str) -> (String, String, String) {
let g = parse_git_uri(uri).unwrap();
(g.user, g.repo, g.host)
}
#[test]
fn github_https_and_ssh() {
assert_eq!(
parts("https://github.com/user/repo"),
("user".into(), "repo".into(), "github.com".into())
);
assert_eq!(
parts("git@github.com:user/repo.git"),
("user".into(), "repo".into(), "github.com".into())
);
}
#[test]
fn generic_hosts() {
assert_eq!(
parts("https://gitlab.com/grp/proj"),
("grp".into(), "proj".into(), "gitlab.com".into())
);
assert_eq!(
parts("git@my.host.dev:owner/thing.git"),
("owner".into(), "thing".into(), "my.host.dev".into())
);
}
#[test]
fn unparseable_returns_none() {
assert_eq!(parse_git_uri("not-a-uri"), None);
assert_eq!(parse_git_uri("https://host-only"), None);
}
#[test]
fn trailing_path_segments_ignored_like_upstream_regex() {
assert_eq!(
parts("https://github.com/user/repo/tree/main"),
("user".into(), "repo".into(), "github.com".into())
);
}
#[test]
fn clone_name_custom_verbatim_no_date() {
assert_eq!(
generate_clone_directory_name("https://github.com/u/r", Some("my name"), "2026-07-10")
.unwrap(),
"my name"
);
assert_eq!(
generate_clone_directory_name("https://github.com/u/r", None, "2026-07-10").unwrap(),
"2026-07-10-u-r"
);
assert_eq!(
generate_clone_directory_name("nope", None, "2026-07-10"),
None
);
}
#[test]
fn is_git_uri_matches_upstream_predicate() {
assert!(is_git_uri("https://github.com/u/r"));
assert!(is_git_uri("git@host:u/r"));
assert!(is_git_uri("something.github.com-ish")); assert!(is_git_uri("local/path.git"));
assert!(!is_git_uri("plain-query"));
}
}