use rto_graph::{NodeKind, Repo, Store};
pub struct SpokePin {
pub rev: String,
pub via: String,
}
pub fn detect(
spoke_store: &Store,
hub_dir: &str,
hub_origin: Option<&str>,
hub_repo: &Repo,
templates: &std::collections::BTreeMap<String, String>,
) -> anyhow::Result<Option<SpokePin>> {
for n in spoke_store.nodes_by_kind(&NodeKind::Other("submodule".into()))? {
let (Some(url), Some(sha)) = (meta_str(&n, "url"), meta_str(&n, "sha")) else {
continue;
};
if url_matches_hub(url, hub_dir, hub_origin) {
let path = meta_str(&n, "path").unwrap_or("?");
return Ok(Some(SpokePin {
rev: sha.to_owned(),
via: format!("submodule {path}"),
}));
}
}
for n in spoke_store.nodes_by_kind(&NodeKind::Other("image_ref".into()))? {
let (Some(image), Some(tag)) = (meta_str(&n, "image"), meta_str(&n, "tag")) else {
continue;
};
if image_basename(image) != hub_dir {
continue;
}
let configured = templates
.get(hub_dir)
.map(|t| t.replace("{tag}", tag))
.into_iter();
for candidate in configured.chain([tag.to_owned(), format!("v{tag}")]) {
if hub_repo.tree_id_at(&candidate).is_ok() {
return Ok(Some(SpokePin {
rev: candidate,
via: format!("image {image}:{tag}"),
}));
}
}
}
Ok(None)
}
fn meta_str<'a>(node: &'a rto_graph::Node, key: &str) -> Option<&'a str> {
node.meta.get(key).and_then(serde_json::Value::as_str)
}
fn url_matches_hub(url: &str, hub_dir: &str, hub_origin: Option<&str>) -> bool {
hub_origin.is_some_and(|o| norm_url(o) == norm_url(url)) || repo_basename(url) == hub_dir
}
fn norm_url(url: &str) -> String {
url.trim_end_matches('/')
.trim_end_matches(".git")
.to_ascii_lowercase()
}
fn repo_basename(url: &str) -> &str {
url.trim_end_matches('/')
.trim_end_matches(".git")
.rsplit(['/', ':'])
.next()
.unwrap_or(url)
}
fn image_basename(image: &str) -> &str {
image.rsplit('/').next().unwrap_or(image)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn url_matches_hub_by_origin_or_basename() {
assert!(url_matches_hub(
"https://github.com/acme/app.git",
"anything",
Some("https://github.com/acme/app")
));
assert!(url_matches_hub("git@github.com:acme/app.git", "app", None));
assert!(!url_matches_hub(
"git@github.com:acme/other.git",
"app",
None
));
assert!(!url_matches_hub(
"git@github.com:acme/app.git",
"app-2",
None
));
}
#[test]
fn image_basename_strips_registry_and_org() {
assert_eq!(image_basename("registry.io/acme/app"), "app");
assert_eq!(image_basename("app"), "app");
}
}