use anyhow::{Context as _, Result, bail};
use serde_json::json;
pub struct ResolveTagOpts {
pub tag: String,
pub json: bool,
pub config_override: Option<std::path::PathBuf>,
}
pub fn run(opts: ResolveTagOpts) -> Result<()> {
let config_path = opts
.config_override
.as_deref()
.filter(|p| p.exists())
.map(|p| p.to_path_buf())
.or_else(|| crate::pipeline::find_config(None).ok());
let config = match config_path {
Some(ref path) => crate::pipeline::load_config(path)?,
None => bail!("no anodizer config found"),
};
let all_crates: Vec<_> = config
.crates
.iter()
.chain(
config
.workspaces
.as_deref()
.unwrap_or_default()
.iter()
.flat_map(|w| &w.crates),
)
.collect();
let mut best: Option<(&anodizer_core::config::CrateConfig, usize)> = None;
for c in &all_crates {
if let Some(prefix) = anodizer_core::git::extract_tag_prefix(&c.tag_template)
&& opts.tag.starts_with(&prefix)
{
let remainder = &opts.tag[prefix.len()..];
let is_version = remainder
.split('.')
.next()
.is_some_and(|s| !s.is_empty() && s.chars().all(|ch| ch.is_ascii_digit()));
if is_version && best.as_ref().is_none_or(|(_, len)| prefix.len() > *len) {
best = Some((c, prefix.len()));
}
}
}
let crate_cfg = match best {
Some((c, _)) => c,
None => bail!("no crate matches tag '{}'", opts.tag),
};
let has_builds = crate_cfg
.builds
.as_ref()
.map(|b| !b.is_empty())
.unwrap_or(false);
if opts.json {
let out = serde_json::to_string(&json!({
"crate": crate_cfg.name,
"path": crate_cfg.path,
"has_builds": has_builds,
}))
.context("serialize resolve-tag JSON output")?;
println!("{}", out);
} else {
println!("crate={}", crate_cfg.name);
println!("path={}", crate_cfg.path);
println!("has-builds={}", has_builds);
}
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn test_resolve_tag_matches_simple_prefix() {
let tag = "v1.2.3";
let prefix = "v";
let remainder = tag.strip_prefix(prefix).unwrap();
assert!(
remainder
.split('.')
.next()
.unwrap()
.chars()
.all(|ch| ch.is_ascii_digit())
);
}
#[test]
fn test_resolve_tag_matches_monorepo_prefix() {
let tag = "core-v0.2.3";
let prefix = "core-v";
let remainder = tag.strip_prefix(prefix).unwrap();
assert!(
remainder
.split('.')
.next()
.unwrap()
.chars()
.all(|ch| ch.is_ascii_digit())
);
}
#[test]
fn test_resolve_tag_rejects_non_version_suffix() {
let tag = "v-something";
let prefix = "v";
let remainder = tag.strip_prefix(prefix).unwrap();
assert!(
!remainder
.split('.')
.next()
.unwrap()
.chars()
.all(|ch| ch.is_ascii_digit())
);
}
#[test]
fn test_resolve_tag_longer_prefix_wins() {
let tag = "core-v1.0.0";
let prefixes = [("v", "cfgd"), ("core-v", "cfgd-core")];
let matched = prefixes.iter().find(|(prefix, _)| {
if let Some(remainder) = tag.strip_prefix(prefix) {
remainder
.split('.')
.next()
.map(|s| s.chars().all(|ch| ch.is_ascii_digit()))
.unwrap_or(false)
} else {
false
}
});
assert!(matched.is_some());
}
}