use super::Repo;
impl Repo {
pub fn remote_url(&self) -> Option<String> {
let local = self.local();
let remote = local.try_find_remote("origin")?.ok()?;
let url = remote.url(gix::remote::Direction::Fetch)?;
Some(url.to_bstring().to_string())
}
}
pub fn scope_key(repo: &Repo) -> String {
match repo.remote_url() {
Some(url) => normalize_remote_url(&url),
None => format!("path:{}", repo.workdir().display()),
}
}
pub fn normalize_remote_url(url: &str) -> String {
let mut s = url.trim().to_string();
while s.ends_with('/') {
s.pop();
}
for prefix in ["https://", "http://", "ssh://", "git://"] {
if let Some(rest) = s.strip_prefix(prefix) {
s = rest.to_string();
break;
}
}
if let Some(at) = s.find('@')
&& !s[..at].contains('/')
{
s = s[at + 1..].to_string();
}
if !s.starts_with('[') {
if let Some(colon) = s.find(':')
&& !s[..colon].contains('/')
{
let after = &s[colon + 1..];
let port_len = after.bytes().take_while(u8::is_ascii_digit).count();
if port_len > 0 && after[port_len..].starts_with('/') {
s.replace_range(colon..colon + 1 + port_len, "");
} else {
s.replace_range(colon..=colon, "/");
}
}
} else if let Some(bracket_end) = s.find(']') {
let after_bracket = &s[bracket_end + 1..];
if let Some(port_str) = after_bracket.strip_prefix(':') {
let port_len = port_str.bytes().take_while(u8::is_ascii_digit).count();
if port_len > 0 && port_str[port_len..].starts_with('/') {
let colon_pos = bracket_end + 1;
s.replace_range(colon_pos..colon_pos + 1 + port_len, "");
}
}
}
if let Some(stripped) = s.strip_suffix(".git") {
s = stripped.to_string();
}
while s.ends_with('/') {
s.pop();
}
if let Some(slash) = s.find('/') {
let host = s[..slash].to_ascii_lowercase();
let rest = s[slash..].to_string();
s = format!("{host}{rest}");
} else {
s = s.to_ascii_lowercase();
}
s
}
#[cfg(test)]
mod tests {
use super::normalize_remote_url;
#[test]
fn collapses_https_and_ssh_forms_to_same_key() {
assert_eq!(
normalize_remote_url("https://github.com/Foo/bar.git"),
"github.com/Foo/bar"
);
assert_eq!(
normalize_remote_url("git@github.com:Foo/bar.git"),
"github.com/Foo/bar"
);
assert_eq!(
normalize_remote_url("ssh://git@github.com/Foo/bar.git/"),
"github.com/Foo/bar"
);
}
#[test]
fn ssh_with_port_normalizes_to_same_scope_as_https_and_scp() {
let expected = "github.com/Foo/bar";
assert_eq!(
normalize_remote_url("ssh://git@github.com:22/Foo/bar"),
expected
);
assert_eq!(normalize_remote_url("git@github.com:Foo/bar"), expected);
assert_eq!(normalize_remote_url("https://github.com/Foo/bar"), expected);
assert_eq!(
normalize_remote_url("ssh://git@github.com:22/Foo/bar.git"),
expected
);
}
#[test]
fn lowercases_host_but_preserves_path_case() {
assert_eq!(
normalize_remote_url("https://GitHub.COM/Foo/Bar"),
"github.com/Foo/Bar"
);
}
#[test]
fn url_without_origin_remains_stable() {
assert_eq!(normalize_remote_url("local-only"), "local-only");
}
#[test]
fn bracketed_ipv6_host_normalizes_correctly() {
assert_eq!(
normalize_remote_url("ssh://git@[2001:db8::1]:22/Foo/bar"),
"[2001:db8::1]/Foo/bar"
);
assert_eq!(
normalize_remote_url("ssh://git@[2001:db8::1]/Foo/bar"),
"[2001:db8::1]/Foo/bar"
);
assert_eq!(
normalize_remote_url("ssh://git@[2001:db8::1]:2222/Foo/bar.git"),
"[2001:db8::1]/Foo/bar"
);
}
}