pub mod rollback;
mod bump_detect;
mod crate_info;
mod per_crate;
mod repo_shape;
mod run;
mod version_plan;
mod workspace_bump;
pub(crate) use bump_detect::*;
pub(crate) use crate_info::*;
pub(crate) use per_crate::*;
pub(crate) use repo_shape::*;
pub use run::run;
pub(crate) use version_plan::*;
pub(crate) use workspace_bump::*;
#[cfg(test)]
mod tests;
use anodizer_core::config::{CrateConfig, GitConfig, TagConfig};
use anodizer_core::git;
use anodizer_core::hooks::{HookRunContext, run_hooks};
use anodizer_core::log::{StageLogger, Verbosity};
use anodizer_core::template::TemplateVars;
use anyhow::{Result, bail};
use regex::Regex;
use std::path::{Path, PathBuf};
use crate::commands::bump::cargo_edit::{WorkspaceInfo, apply_plan, load_workspace};
use crate::commands::bump::plan::{BumpLevel, PlanRow};
use crate::commands::changelog_sync::{
ChangelogRouting, ChangelogTarget, render_and_stage_changelogs, resolve_changelog_enabled,
};
use crate::commands::version_files_resolve::resolve_version_files;
pub struct TagOpts {
pub dry_run: bool,
pub custom_tag: Option<String>,
pub version_override: Option<String>,
pub default_bump: Option<String>,
pub crate_name: Option<String>,
pub push: bool,
pub no_push: bool,
pub push_tags_only: bool,
pub sign: bool,
pub no_sign: bool,
pub push_remote: Option<String>,
pub push_dry_run: bool,
pub changelog: bool,
pub config_override: Option<std::path::PathBuf>,
pub verbose: bool,
pub debug: bool,
pub quiet: bool,
pub strict: bool,
}
fn resolve_effective_push(opts: &TagOpts, config_push: Option<bool>) -> bool {
if opts.no_push {
false
} else {
opts.push || config_push == Some(true)
}
}
fn resolve_effective_sign(opts: &TagOpts, config_sign: Option<bool>) -> bool {
if opts.no_sign {
false
} else {
opts.sign || config_sign == Some(true)
}
}
#[derive(Clone)]
pub(crate) struct ResolvedConfig {
default_bump: String,
bump_minor_pre_major: bool,
bump_patch_for_minor_pre_major: bool,
tag_prefix: String,
release_branches: Vec<String>,
custom_tag: Option<String>,
tag_context: String,
branch_history: String,
initial_version: String,
prerelease: bool,
prerelease_suffix: String,
force_without_changes: bool,
force_without_changes_pre: bool,
major_string_token: String,
minor_string_token: String,
patch_string_token: String,
none_string_token: String,
git_api_tagging: bool,
skip_ci_on_bump: bool,
}
impl ResolvedConfig {
fn from_tag_config(cfg: &TagConfig, opts: &TagOpts) -> Self {
ResolvedConfig {
default_bump: opts
.default_bump
.clone()
.or_else(|| cfg.default_bump.clone())
.unwrap_or_else(|| "none".to_string()),
bump_minor_pre_major: cfg.bump_minor_pre_major.unwrap_or(false),
bump_patch_for_minor_pre_major: cfg.bump_patch_for_minor_pre_major.unwrap_or(false),
tag_prefix: cfg.tag_prefix.clone().unwrap_or_else(|| "v".to_string()),
release_branches: cfg.release_branches.clone().unwrap_or_default(),
custom_tag: opts.custom_tag.clone().or_else(|| cfg.custom_tag.clone()),
tag_context: cfg
.tag_context
.clone()
.unwrap_or_else(|| "repo".to_string()),
branch_history: cfg
.branch_history
.clone()
.unwrap_or_else(|| "compare".to_string()),
initial_version: cfg
.initial_version
.clone()
.unwrap_or_else(|| "0.0.0".to_string()),
prerelease: cfg.prerelease.unwrap_or(false),
prerelease_suffix: cfg
.prerelease_suffix
.clone()
.unwrap_or_else(|| "beta".to_string()),
force_without_changes: cfg.force_without_changes.unwrap_or(false),
force_without_changes_pre: cfg.force_without_changes_pre.unwrap_or(false),
major_string_token: cfg
.major_string_token
.clone()
.unwrap_or_else(|| "#major".to_string()),
minor_string_token: cfg
.minor_string_token
.clone()
.unwrap_or_else(|| "#minor".to_string()),
patch_string_token: cfg
.patch_string_token
.clone()
.unwrap_or_else(|| "#patch".to_string()),
none_string_token: cfg
.none_string_token
.clone()
.unwrap_or_else(|| "#none".to_string()),
git_api_tagging: cfg.git_api_tagging.unwrap_or(false),
skip_ci_on_bump: cfg.skip_ci_on_bump.unwrap_or(false),
}
}
}
fn warn_cargo_lock_stale(log: &StageLogger, cause: &str) {
log.warn(&format!(
"{cause}; Cargo.lock is now stale relative to the bumped Cargo.toml, and \
`release` (publish / determinism) will fail on it later — run \
`cargo update --workspace` and fold Cargo.lock into the bump commit \
before releasing"
));
}
fn skip_ci_suffix(skip_ci_on_bump: bool) -> &'static str {
if skip_ci_on_bump { " [skip ci]" } else { "" }
}
pub(crate) struct VersionFilesBump<'a> {
old: Option<&'a str>,
files: &'a [String],
}
pub(crate) struct ChangelogBump<'a> {
enabled: bool,
from_tag: Option<&'a str>,
full_tag: &'a str,
routing: &'a ChangelogRouting<'a>,
}
pub(crate) struct WorkspaceBumpEdits<'a> {
vf: VersionFilesBump<'a>,
cl: ChangelogBump<'a>,
}
fn resolve_config_path(opts: &TagOpts) -> Option<std::path::PathBuf> {
opts.config_override
.as_deref()
.filter(|p| p.exists())
.map(|p| p.to_path_buf())
.or_else(|| crate::pipeline::find_config(None).ok())
}
fn load_config_at(opts: &TagOpts, root: &Path) -> Result<anodizer_core::config::Config> {
match opts.config_override.as_deref() {
Some(p) => {
if !p.exists() {
anyhow::bail!("--config path does not exist: {}", p.display());
}
crate::pipeline::load_config(p)
}
None => crate::pipeline::load_repo_config(root),
}
}
pub(crate) struct GroupTagResult {
crate_names: Vec<String>,
new_tags: Vec<(String, String)>,
version_updates: Vec<(String, String)>,
old_version: Option<String>,
prev_tag: Option<String>,
crate_version_files: Vec<Vec<String>>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct PushControls<'a> {
remote: &'a str,
config_push: Option<bool>,
sign: bool,
changelog_enabled: bool,
pre_hooks: &'a [anodizer_core::config::HookEntry],
post_hooks: &'a [anodizer_core::config::HookEntry],
remote_tags: Option<&'a std::collections::HashSet<String>>,
}
pub(crate) struct PerCrateDispatch {
groups: Vec<Vec<CrateConfig>>,
is_flat_aggregate: bool,
workspace_root: PathBuf,
}
pub(crate) struct CrateTagInfo {
tag_prefix: String,
path: String,
version_sync: bool,
version_files: Vec<String>,
}
#[derive(Debug, PartialEq)]
pub(crate) struct VersionFileRewrite {
file: String,
old: String,
new: String,
}