use anodizer_core::config::Config;
use anodizer_core::context::Context;
use anodizer_core::git;
use anodizer_core::log::StageLogger;
pub(crate) fn first_covered_crate<'a>(
ctx: &Context,
config: &'a Config,
) -> Option<&'a anodizer_core::config::CrateConfig> {
let selected = &ctx.options.selected_crates;
config
.crate_universe()
.into_iter()
.find(|c| anodizer_core::config::crate_is_selected(selected, &c.name))
}
pub(super) fn resolve_tag_override(
anodizer_current_tag: Option<String>,
goreleaser_current_tag: Option<String>,
github_ref_type: Option<String>,
github_ref_name: Option<String>,
) -> Option<String> {
anodizer_current_tag
.filter(|s| !s.is_empty())
.or_else(|| goreleaser_current_tag.filter(|s| !s.is_empty()))
.or_else(|| {
let is_tag = github_ref_type.as_deref().filter(|s| *s == "tag").is_some();
if is_tag {
github_ref_name.filter(|s| !s.is_empty())
} else {
None
}
})
}
fn newest_tag_across_crates(
ctx: &Context,
config: &Config,
monorepo_prefix: Option<&str>,
log: &StageLogger,
) -> Option<(String, String)> {
let selected = &ctx.options.selected_crates;
let covered = config.selected_crates(selected);
let mut seen_templates: Vec<String> = Vec::new();
let mut best: Option<(git::SemVer, String, String)> = None;
for crate_cfg in covered {
let template = crate_cfg.tag_family_template();
if seen_templates.contains(&template) {
continue;
}
seen_templates.push(template.clone());
let found = match git::find_latest_tag_matching_with_prefix(
&template,
config.git.as_ref(),
Some(ctx.template_vars()),
monorepo_prefix,
) {
Ok(found) => found,
Err(e) => {
log.warn(&format!("error finding tags matching template: {e}"));
continue;
}
};
let Some(tag) = found else { continue };
let stripped = match monorepo_prefix {
Some(prefix) => git::strip_monorepo_prefix(&tag, prefix),
None => tag.as_str(),
};
let Ok(semver) = git::parse_semver_tag(stripped) else {
continue;
};
if best.as_ref().is_none_or(|(bv, _, _)| semver > *bv) {
best = Some((semver, tag, template));
}
}
if let Some((_, ref tag, _)) = best
&& seen_templates.len() > 1
{
log.verbose(&format!(
"synthesized version base '{tag}' — newest across {} tag families",
seen_templates.len()
));
}
best.map(|(_, tag, template)| (tag, template))
}
pub fn resolve_git_context(
ctx: &mut Context,
config: &Config,
log: &StageLogger,
) -> anyhow::Result<()> {
if git::is_shallow_clone() {
log.warn(
"shallow clone detected; tag discovery may be incomplete. \
Use `git fetch --unshallow` in CI.",
);
}
let anodizer_current_tag = ctx.env_var("ANODIZER_CURRENT_TAG");
let goreleaser_current_tag = ctx.env_var("GORELEASER_CURRENT_TAG");
let github_ref_type = ctx.env_var("GITHUB_REF_TYPE");
let github_ref_name = ctx.env_var("GITHUB_REF_NAME");
tracing::debug!(
anodizer_current_tag = ?anodizer_current_tag,
goreleaser_current_tag = ?goreleaser_current_tag,
github_ref_type = ?github_ref_type,
github_ref_name = ?github_ref_name,
"tag_override resolution: env var snapshot"
);
let tag_override = resolve_tag_override(
anodizer_current_tag,
goreleaser_current_tag,
github_ref_type,
github_ref_name,
);
let first_crate =
first_covered_crate(ctx, config).or_else(|| config.crate_universe().into_iter().next());
if let Some(crate_cfg) = first_crate {
let crate_tag_template = crate_cfg.tag_family_template();
let mut base_tag_template = crate_tag_template.clone();
let tag_source = if tag_override.is_some() {
git::TagSource::Declared
} else {
git::TagSource::Inferred
};
let tag = if let Some(ref override_tag) = tag_override {
log.verbose(&format!(
"using ANODIZER_CURRENT_TAG override '{}'",
override_tag
));
override_tag.clone()
} else {
let monorepo_prefix = config.monorepo_tag_prefix();
let latest_tag = if ctx.is_nightly() || ctx.is_snapshot() || ctx.options.observe {
newest_tag_across_crates(ctx, config, monorepo_prefix, log).map(|(tag, tmpl)| {
base_tag_template = tmpl;
tag
})
} else {
match git::find_latest_tag_matching_with_prefix(
&crate_tag_template,
config.git.as_ref(),
Some(ctx.template_vars()),
monorepo_prefix,
) {
Ok(found) => found,
Err(e) => {
log.warn(&format!("error finding tags matching template: {e}"));
None
}
}
};
match latest_tag {
Some(t) => t,
None => {
if ctx.options.snapshot || ctx.options.nightly {
let mode = if ctx.options.nightly {
"nightly"
} else {
"snapshot"
};
log.warn(&format!(
"no git tags found, defaulting to v0.0.0 ({mode} mode)."
));
"v0.0.0".to_string()
} else if ctx.options.dry_run {
log.warn("no git tags found, defaulting to v0.0.0 (dry-run mode).");
"v0.0.0".to_string()
} else if ctx.options.observe {
log.verbose("no git tags found; the report targets the first version");
"v0.0.0".to_string()
} else if ctx.options.notify {
"v0.0.0".to_string()
} else {
anyhow::bail!("no git tag found; create a tag or use --snapshot");
}
}
}
};
let is_synthetic_tag = tag == "v0.0.0" && tag_override.is_none();
if !is_synthetic_tag
&& let Ok(false) = git::tag_points_at_head(&tag)
&& !ctx.options.snapshot
&& !ctx.options.nightly
&& !ctx.options.changelog_preview
&& !ctx.options.observe
&& !ctx.options.notify
{
let head = git::get_short_commit().unwrap_or_else(|_| "unknown".to_string());
anyhow::bail!(
"tag {} does not point at HEAD ({}). Check out the tag or use --snapshot to skip this check.",
tag,
head
);
}
match git::detect_git_info(&tag, ctx.skip_validate()) {
Ok(mut git_info) => {
git_info.tag_source = tag_source;
if git_info.dirty
&& !ctx.options.snapshot
&& !ctx.options.nightly
&& !ctx.options.changelog_preview
&& !ctx.options.observe
&& !ctx.options.notify
{
if ctx.options.dry_run {
log.warn("git is in a dirty state; run `git status` to see what changed.");
} else {
anyhow::bail!(
"git is in a dirty state; run `git status` to see what changed. \
Use --snapshot to force."
);
}
}
let prev_override = ctx
.env_var("ANODIZER_PREVIOUS_TAG")
.filter(|s| !s.is_empty())
.or_else(|| {
ctx.env_var("GORELEASER_PREVIOUS_TAG")
.filter(|s| !s.is_empty())
});
if let Some(prev_override) = prev_override {
log.verbose(&format!(
"using ANODIZER_PREVIOUS_TAG override '{}'",
prev_override
));
git_info.previous_tag = Some(prev_override);
} else {
git_info.previous_tag = git::find_previous_tag_in_family(
&tag,
&base_tag_template,
config.git.as_ref(),
Some(ctx.template_vars()),
config.monorepo_tag_prefix(),
&config.sibling_tag_families_of(&base_tag_template),
)
.ok()
.flatten();
}
ctx.git_info = Some(git_info);
ctx.populate_git_vars();
}
Err(e) => {
let lenient_mode = if ctx.options.nightly {
Some("nightly")
} else if ctx.options.snapshot {
Some("snapshot")
} else if ctx.options.notify {
Some("notify")
} else if ctx.options.observe {
Some("preflight")
} else {
None
};
if let Some(mode) = lenient_mode {
log.warn(&format!(
"could not detect git info in {mode} mode, using defaults: {e}"
));
ctx.git_info = Some(git::GitInfo {
tag: tag.clone(),
tag_source,
commit: "none".to_string(),
short_commit: "none".to_string(),
branch: "none".to_string(),
dirty: true,
semver: git::SemVer {
major: 0,
minor: 0,
patch: 0,
prerelease: None,
build_metadata: None,
},
commit_date: String::new(),
commit_timestamp: String::new(),
previous_tag: None,
remote_url: String::new(),
summary: mode.to_string(),
tag_subject: String::new(),
tag_contents: String::new(),
tag_body: String::new(),
first_commit: None,
});
ctx.populate_git_vars();
} else {
return Err(anyhow::anyhow!("could not detect git info: {e}"));
}
}
}
} else {
ctx.populate_git_vars();
}
Ok(())
}