use crate::sanitize_key;
pub fn sanitize_remote_url(url: &str) -> Option<String> {
let mut s = url.trim().to_string();
if s.is_empty() {
return None;
}
if let Some(at) = s.find('@') {
if !s[..at].contains("://") {
if let Some(colon) = s[at..].find(':') {
let host = s[at + 1..at + colon].to_ascii_lowercase();
let mut path = s[at + colon + 1..].to_string();
if let Some(stripped) = path.strip_suffix(".git") {
path = stripped.to_string();
}
let key = format!("{host}/{path}").to_ascii_lowercase();
return if key.len() > 1 { Some(key) } else { None };
}
}
}
if let Some(scheme_end) = s.find("://") {
s = s[scheme_end + 3..].to_string();
if let Some(at) = s.find('@') {
let slash = s.find('/').unwrap_or(s.len());
if at < slash {
s = s[at + 1..].to_string();
}
}
if let Some(slash) = s.find('/') {
let (hostport, path) = s.split_at(slash);
let host = hostport.split(':').next().unwrap_or(hostport);
let mut p = path.trim_start_matches('/').to_string();
if let Some(stripped) = p.strip_suffix(".git") {
p = stripped.to_string();
}
let key = format!("{}/{p}", host.to_ascii_lowercase()).to_ascii_lowercase();
return if key.len() > 1 { Some(key) } else { None };
}
return None;
}
let mut p = s;
if let Some(stripped) = p.strip_suffix(".git") {
p = stripped.to_string();
}
let p = p.to_ascii_lowercase();
if p.is_empty() {
None
} else {
Some(p)
}
}
pub fn stable_repo_id(
explicit: Option<&str>,
remote_url: Option<&str>,
basename: &str,
) -> String {
if let Some(e) = explicit.map(str::trim).filter(|e| !e.is_empty()) {
return sanitize_key(e);
}
if let Some(url) = remote_url {
if let Some(key) = sanitize_remote_url(url) {
return sanitize_key(&key);
}
}
sanitize_key(basename)
}
pub fn ensure_repo_id_file(
state_dir: &std::path::Path,
explicit: Option<&str>,
remote_url: Option<&str>,
basename: &str,
) -> std::io::Result<String> {
let id = stable_repo_id(explicit, remote_url, basename);
let has_signal = explicit.map(str::trim).filter(|e| !e.is_empty()).is_some()
|| remote_url
.map(|u| sanitize_remote_url(u).is_some())
.unwrap_or(false);
let path = state_dir.join("repo-id");
if has_signal && !path.exists() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, format!("{id}\n"))?;
}
Ok(id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_credentials_from_https_remote() {
let got = sanitize_remote_url("https://user:s3cret@github.com/acme/billing.git")
.unwrap();
assert_eq!(got, "github.com/acme/billing");
assert!(!got.contains("s3cret"), "secret leaked into identity");
assert!(!got.contains("user"), "userinfo leaked into identity");
}
#[test]
fn parses_scp_syntax_and_strips_ports() {
assert_eq!(
sanitize_remote_url("git@github.com:acme/billing.git").unwrap(),
"github.com/acme/billing"
);
assert_eq!(
sanitize_remote_url("https://github.com:443/acme/billing").unwrap(),
"github.com/acme/billing"
);
}
#[test]
fn precedence_is_explicit_then_remote_then_basename() {
assert_eq!(
stable_repo_id(Some("Billing "), Some("https://x@h/o/r"), "checkout"),
"billing"
);
assert_eq!(
stable_repo_id(None, Some("git@github.com:acme/billing.git"), "checkout"),
"github.com/acme/billing"
);
assert_eq!(stable_repo_id(None, None, "checkout"), "checkout");
assert_eq!(stable_repo_id(Some(""), None, "checkout"), "checkout");
}
#[test]
fn empty_and_garbage_remotes_fall_back() {
assert!(sanitize_remote_url("").is_none());
assert_eq!(stable_repo_id(None, Some(""), "b"), "b");
}
#[test]
fn repo_id_file_pins_only_signalled_ids() {
let dir = std::env::temp_dir().join(format!("scc-ident-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let id = ensure_repo_id_file(&dir, None, Some("git@github.com:acme/billing.git"), "checkout").unwrap();
assert_eq!(id, "github.com/acme/billing");
assert_eq!(std::fs::read_to_string(dir.join("repo-id")).unwrap(), "github.com/acme/billing\n");
let id2 = ensure_repo_id_file(&dir, Some("other"), None, "checkout").unwrap();
assert_eq!(id2, "other");
assert_eq!(std::fs::read_to_string(dir.join("repo-id")).unwrap(), "github.com/acme/billing\n");
std::fs::remove_dir_all(&dir).unwrap();
let dir2 = std::env::temp_dir().join(format!("scc-ident2-{}", std::process::id()));
std::fs::create_dir_all(&dir2).unwrap();
let id3 = ensure_repo_id_file(&dir2, None, None, "checkout").unwrap();
assert_eq!(id3, "checkout");
assert!(!dir2.join("repo-id").exists());
std::fs::remove_dir_all(&dir2).unwrap();
}
}