use super::*;
use std::collections::HashMap;
pub(crate) struct TagPlan {
pub new_tag: String,
pub new_version: String,
pub old_tag: String,
pub crate_versions: HashMap<String, String>,
}
pub(crate) struct PlanInputs<'a> {
pub cfg: &'a ResolvedConfig,
pub prev_tag: Option<&'a str>,
pub bump: BumpKind,
pub cargo_current_ver: Option<String>,
pub version_override: Option<String>,
}
pub(crate) fn planned_version(
inputs: PlanInputs<'_>,
log: &StageLogger,
) -> Result<Option<TagPlan>> {
let PlanInputs {
cfg,
prev_tag,
bump,
cargo_current_ver,
version_override,
} = inputs;
let cargo_ahead = manifest_version_ahead(
cargo_current_ver.as_deref(),
prev_tag
.and_then(|t| git::parse_semver_tag(t).ok())
.map(|p| (p.major, p.minor, p.patch)),
);
if bump == BumpKind::None && !cargo_ahead && version_override.is_none() {
return Ok(None);
}
let (new_major, new_minor, new_patch, old_tag_str) = if let Some(prev) = prev_tag {
let base = git::parse_semver_tag(prev)?;
let (maj, min, pat) = apply_bump(base.major, base.minor, base.patch, &bump);
(maj, min, pat, prev)
} else {
let base = git::parse_semver_tag(&format!("{}{}", cfg.tag_prefix, cfg.initial_version))
.unwrap_or(git::SemVer {
major: 0,
minor: 1,
patch: 0,
prerelease: None,
build_metadata: None,
});
(base.major, base.minor, base.patch, "")
};
let mut new_version = format!("{}.{}.{}", new_major, new_minor, new_patch);
if cfg.prerelease {
new_version = format!("{}-{}", new_version, cfg.prerelease_suffix);
}
if let Some(cargo_ver) = cargo_current_ver
&& manifest_version_ahead(Some(&cargo_ver), Some((new_major, new_minor, new_patch)))
{
if version_override.is_none() {
log.status(&format!(
"Cargo.toml version {} > tag-derived {}, using Cargo.toml version",
cargo_ver, new_version
));
}
new_version = cargo_ver;
}
if let Some(pinned) = version_override {
if pinned != new_version {
log.warn(&format!(
"--version {} overrides the derived version {} (autotag + Cargo.toml-ahead guard bypassed)",
pinned, new_version
));
}
new_version = pinned;
}
Ok(Some(TagPlan {
new_tag: format!("{}{}", cfg.tag_prefix, new_version),
new_version,
old_tag: old_tag_str.to_string(),
crate_versions: HashMap::new(),
}))
}
pub(crate) fn plan_next_version(
config_override: Option<&Path>,
log: &StageLogger,
) -> Result<Option<TagPlan>> {
let opts = TagOpts {
config_override: config_override.map(Path::to_path_buf),
..Default::default()
};
let root = crate::commands::helpers::discover_workspace_root(config_override)?;
let config = load_config_at(&opts, &root)?;
let workspace = load_workspace(&root)?;
let mut cfg = ResolvedConfig::from_config(&config, &opts);
let repo_shape = detect_repo_shape(&root, Some(&config), workspace.as_ref());
let remote = opts.push_remote.as_deref().unwrap_or("origin");
let remote_tag_names: Option<std::collections::HashSet<String>> =
if git::has_remote_in(&root, remote) {
match git::list_remote_tag_names_in(&root, remote) {
Ok(names) => Some(names.into_iter().collect()),
Err(e) => {
log.verbose(&format!(
"could not list tags on remote '{remote}' ({e}); the plan reads \
local tags, so a tag deleted on the remote may still count"
));
None
}
}
} else {
None
};
if matches!(repo_shape, RepoShape::Single)
&& let [only] = config.crate_universe().as_slice()
{
cfg.tag_prefix = git::per_crate_tag_prefix(&only.name, &only.tag_family_template());
}
let groups: Option<Vec<Vec<CrateConfig>>> = match repo_shape {
RepoShape::PerCrate(groups) => Some(groups),
RepoShape::FlatAggregate(crates) => Some(vec![crates]),
RepoShape::Single | RepoShape::Lockstep => None,
};
if let Some(groups) = groups {
let results = compute_per_crate_tags(
&root,
&groups,
&opts,
&cfg,
config.git.as_ref(),
Some(&config),
remote_tag_names.as_ref(),
log,
)?;
let planned: Vec<(git::SemVer, &str, &GroupTagResult)> = results
.iter()
.filter_map(|group| {
let (tag, _) = group.new_tags.first()?;
let semver = git::parse_semver_tag(tag).ok()?;
Some((semver, tag.as_str(), group))
})
.collect();
let crate_versions: HashMap<String, String> = planned
.iter()
.flat_map(|(semver, _, group)| {
group
.crate_names
.iter()
.map(move |name| (name.clone(), semver.version_string()))
})
.collect();
let newest = planned.iter().max_by(|a, b| a.0.cmp(&b.0));
return Ok(newest.map(|(semver, tag, group)| TagPlan {
new_tag: tag.to_string(),
new_version: semver.version_string(),
old_tag: group.prev_tag.clone().unwrap_or_default(),
crate_versions,
}));
}
let prev_tag = find_previous_tag(&cfg, config.git.as_ref(), remote_tag_names.as_ref())?;
if let Some(ref tag) = prev_tag
&& !git::has_commits_since_tag_in(&root, tag)?
{
let force = if cfg.prerelease {
cfg.force_without_changes_pre
} else {
cfg.force_without_changes
};
if !force {
return Ok(None);
}
}
let messages = get_messages_for_bump(&root, &cfg, prev_tag.as_deref(), None)?;
let plan = planned_version(
PlanInputs {
cfg: &cfg,
prev_tag: prev_tag.as_deref(),
bump: detect_bump_demoted(&messages, &cfg, prev_tag.as_deref()),
cargo_current_ver: workspace
.as_ref()
.and_then(|ws| ws.workspace_package_version.clone()),
version_override: None,
},
log,
)?;
Ok(plan.map(|mut plan| {
plan.crate_versions = config
.crate_universe()
.iter()
.map(|c| (c.name.clone(), plan.new_version.clone()))
.collect();
plan
}))
}