use anyhow::{anyhow, bail, Result};
use url::Url;
pub fn validate_git_url(url: &str) -> Result<()> {
if url.is_empty() || url != url.trim() {
bail!("malformed git url: {}", url);
}
if is_git_ssh_shorthand(url) {
return Ok(());
}
let parsed = Url::parse(url)
.map_err(|_| anyhow!("unsupported or malformed git url: {}", url))?;
match parsed.scheme() {
"http" | "https" | "ssh" => {
if parsed.host_str().is_none() {
bail!("git url missing host: {}", url);
}
if !has_repo_path(&parsed) {
bail!("git url missing repository path: {}", url);
}
Ok(())
}
"file" => {
if !has_repo_path(&parsed) {
bail!("git url missing repository path: {}", url);
}
Ok(())
}
scheme => Err(anyhow!("unsupported git url scheme: {}", scheme)),
}
}
fn is_git_ssh_shorthand(url: &str) -> bool {
let Some(rest) = url.strip_prefix("git@") else {
return false;
};
let Some((host, path)) = rest.split_once(':') else {
return false;
};
!host.is_empty() && !host.contains('/') && !path.trim_matches('/').is_empty()
}
fn has_repo_path(url: &Url) -> bool {
let path = url.path().trim_matches('/');
!path.is_empty()
}
fn strip_git_suffix_once(url: &str) -> &str {
url.strip_suffix(".git").unwrap_or(url)
}
fn normalize_name_source(url: &str) -> &str {
strip_git_suffix_once(url.trim_end_matches('/'))
}
fn last_path_segment(url: &str) -> Option<&str> {
url.rsplit(|c: char| c == '/' || c == ':')
.next()
.filter(|s| !s.is_empty())
}
pub fn infer_marketplace_name_from_url(url: &str) -> Result<String> {
let trimmed = normalize_name_source(url);
let last = last_path_segment(trimmed)
.ok_or_else(|| anyhow!("cannot infer name from url: {}", url))?;
Ok(last.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_supported_schemes() {
for u in [
"https://x.com/r.git",
"http://x.com/r",
"ssh://git@x.com/r.git",
"git@x.com:o/r.git",
"file:///tmp/r",
] {
assert!(validate_git_url(u).is_ok(), "{}", u);
}
}
#[test]
fn rejects_unsupported_schemes() {
for u in ["ftp://x/r", "javascript:alert(1)", "../local"] {
assert!(validate_git_url(u).is_err(), "{}", u);
}
}
#[test]
fn rejects_malformed_urls_with_supported_schemes() {
for u in [
"",
" https://x.com/r.git",
"https://",
"https://x.com",
"ssh://git@/r.git",
"file:///",
] {
assert!(validate_git_url(u).is_err(), "{}", u);
}
}
#[test]
fn rejects_malformed_ssh_shorthand() {
for u in [
"git@",
"git@github.com",
"git@github.com:",
"git@:owner/repo.git",
"git@github.com:/",
] {
assert!(validate_git_url(u).is_err(), "{}", u);
}
}
#[test]
fn infers_name_from_https() {
assert_eq!(
infer_marketplace_name_from_url("https://gitcode.com/u/foo.git").unwrap(),
"foo"
);
}
#[test]
fn infers_name_from_ssh_shorthand() {
assert_eq!(
infer_marketplace_name_from_url("git@github.com:o/bar.git").unwrap(),
"bar"
);
}
#[test]
fn infers_name_without_dot_git() {
assert_eq!(
infer_marketplace_name_from_url("https://x.com/u/baz").unwrap(),
"baz"
);
}
#[test]
fn strips_only_one_dot_git_suffix_when_inferring_name() {
assert_eq!(
infer_marketplace_name_from_url("https://x.com/u/foo.git.git").unwrap(),
"foo.git"
);
}
}