use std::ffi::OsStr;
use std::path::Path;
use crate::error::CliError;
use crate::ops::cmd;
use crate::ops::git;
use crate::ops::replace::{Template, NOW};
use crate::steps::plan;
#[derive(Debug, Clone, clap::Args)]
pub struct HookStep {
#[command(flatten)]
manifest: clap_cargo::Manifest,
#[command(flatten)]
workspace: clap_cargo::Workspace,
#[arg(long)]
unpublished: bool,
#[arg(short, long = "config", value_name = "PATH")]
custom_config: Option<std::path::PathBuf>,
#[arg(long)]
isolated: bool,
#[arg(long, value_delimiter = ',')]
allow_branch: Option<Vec<String>>,
#[arg(short = 'x', long)]
execute: bool,
#[arg(short = 'n', long, conflicts_with = "execute", hide = true)]
dry_run: bool,
#[arg(long)]
no_confirm: bool,
}
impl HookStep {
pub fn run(&self) -> Result<(), CliError> {
git::git_version()?;
let mut index = crate::ops::index::CratesIoIndex::new();
if self.dry_run {
let _ =
crate::ops::shell::warn("`--dry-run` is superfluous, dry-run is done by default");
}
let ws_meta = self
.manifest
.metadata()
.features(cargo_metadata::CargoOpt::AllFeatures)
.exec()?;
let config = self.to_config();
let ws_config = crate::config::load_workspace_config(&config, &ws_meta)?;
let mut pkgs = plan::load(&config, &ws_meta)?;
let (_selected_pkgs, excluded_pkgs) = self.workspace.partition_packages(&ws_meta);
for excluded_pkg in excluded_pkgs {
let pkg = if let Some(pkg) = pkgs.get_mut(&excluded_pkg.id) {
pkg
} else {
continue;
};
if !pkg.config.release() {
continue;
}
let crate_name = pkg.meta.name.as_str();
let explicitly_excluded = self.workspace.exclude.contains(&excluded_pkg.name);
if pkg.config.release()
&& pkg.config.publish()
&& self.unpublished
&& !explicitly_excluded
{
let version = &pkg.initial_version;
if !crate::ops::cargo::is_published(
&mut index,
pkg.config.registry(),
crate_name,
&version.full_version_string,
) {
log::debug!(
"enabled {}, v{} is unpublished",
crate_name,
version.full_version_string
);
continue;
}
}
pkg.config.pre_release_replacements = Some(vec![]);
pkg.config.release = Some(false);
}
let pkgs = plan::plan(pkgs)?;
let (selected_pkgs, _excluded_pkgs): (Vec<_>, Vec<_>) = pkgs
.into_iter()
.map(|(_, pkg)| pkg)
.partition(|p| p.config.release());
if selected_pkgs.is_empty() {
let _ = crate::ops::shell::error("no packages selected");
return Err(2.into());
}
let dry_run = !self.execute;
let mut failed = false;
failed |= !super::verify_git_is_clean(
ws_meta.workspace_root.as_std_path(),
dry_run,
log::Level::Warn,
)?;
super::warn_changed(&ws_meta, &selected_pkgs)?;
failed |= !super::verify_git_branch(
ws_meta.workspace_root.as_std_path(),
&ws_config,
dry_run,
log::Level::Warn,
)?;
failed |= !super::verify_if_behind(
ws_meta.workspace_root.as_std_path(),
&ws_config,
dry_run,
log::Level::Warn,
)?;
super::confirm("Bump", &selected_pkgs, self.no_confirm, dry_run)?;
for pkg in &selected_pkgs {
hook(&ws_meta, pkg, dry_run)?;
}
super::finish(failed, dry_run)
}
fn to_config(&self) -> crate::config::ConfigArgs {
crate::config::ConfigArgs {
custom_config: self.custom_config.clone(),
isolated: self.isolated,
allow_branch: self.allow_branch.clone(),
..Default::default()
}
}
}
pub fn hook(
ws_meta: &cargo_metadata::Metadata,
pkg: &plan::PackageRelease,
dry_run: bool,
) -> Result<(), CliError> {
if let Some(pre_rel_hook) = pkg.config.pre_release_hook() {
let cwd = &pkg.package_root;
let crate_name = pkg.meta.name.as_str();
let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
let prev_version_var = pkg.initial_version.bare_version_string.as_str();
let prev_metadata_var = pkg.initial_version.full_version.build.as_str();
let version_var = version.bare_version_string.as_str();
let metadata_var = version.full_version.build.as_str();
let template = Template {
prev_version: Some(prev_version_var),
prev_metadata: Some(prev_metadata_var),
version: Some(version_var),
metadata: Some(metadata_var),
crate_name: Some(crate_name),
date: Some(NOW.as_str()),
tag_name: pkg.planned_tag.as_deref(),
..Default::default()
};
let pre_rel_hook = pre_rel_hook
.args()
.into_iter()
.map(|arg| template.render(arg))
.collect::<Vec<_>>();
log::debug!("calling pre-release hook: {:?}", pre_rel_hook);
let envs = maplit::btreemap! {
OsStr::new("PREV_VERSION") => prev_version_var.as_ref(),
OsStr::new("PREV_METADATA") => prev_metadata_var.as_ref(),
OsStr::new("NEW_VERSION") => version_var.as_ref(),
OsStr::new("NEW_METADATA") => metadata_var.as_ref(),
OsStr::new("DRY_RUN") => OsStr::new(if dry_run { "true" } else { "false" }),
OsStr::new("CRATE_NAME") => OsStr::new(crate_name),
OsStr::new("WORKSPACE_ROOT") => ws_meta.workspace_root.as_os_str(),
OsStr::new("CRATE_ROOT") => pkg.manifest_path.parent().unwrap_or_else(|| Path::new(".")).as_os_str(),
};
if !cmd::call_with_env(pre_rel_hook, envs, cwd, false)? {
let _ = crate::ops::shell::error(format!(
"release of {} aborted by non-zero return of prerelease hook.",
crate_name
));
return Err(101.into());
}
}
Ok(())
}