cargo_release/steps/
hook.rs1use std::ffi::OsStr;
2use std::path::Path;
3
4use crate::error::CliError;
5use crate::ops::cmd;
6use crate::ops::git;
7use crate::ops::replace::{NOW, Template};
8use crate::steps::plan;
9
10#[derive(Debug, Clone, clap::Args)]
12pub struct HookStep {
13 #[command(flatten)]
14 manifest: clap_cargo::Manifest,
15
16 #[command(flatten)]
17 workspace: clap_cargo::Workspace,
18
19 #[arg(long)]
21 unpublished: bool,
22
23 #[arg(short, long = "config", value_name = "PATH")]
25 custom_config: Option<std::path::PathBuf>,
26
27 #[arg(long)]
29 isolated: bool,
30
31 #[arg(short = 'Z', value_name = "FEATURE")]
33 z: Vec<crate::config::UnstableValues>,
34
35 #[arg(long, value_delimiter = ',')]
37 allow_branch: Option<Vec<String>>,
38
39 #[arg(short = 'x', long)]
41 execute: bool,
42
43 #[arg(short = 'n', long, conflicts_with = "execute", hide = true)]
44 dry_run: bool,
45
46 #[arg(long)]
48 no_confirm: bool,
49}
50
51impl HookStep {
52 pub fn run(&self) -> Result<(), CliError> {
53 git::git_version()?;
54 let mut index = crate::ops::index::CratesIoIndex::new();
55
56 if self.dry_run {
57 let _ =
58 crate::ops::shell::warn("`--dry-run` is superfluous, dry-run is done by default");
59 }
60
61 let ws_meta = self
62 .manifest
63 .metadata()
64 .features(cargo_metadata::CargoOpt::AllFeatures)
66 .exec()?;
67 let config = self.to_config();
68 let ws_config = crate::config::load_workspace_config(&config, &ws_meta)?;
69 let mut pkgs = plan::load(&config, &ws_meta)?;
70
71 let (_selected_pkgs, excluded_pkgs) = self.workspace.partition_packages(&ws_meta);
72 for excluded_pkg in excluded_pkgs {
73 let Some(pkg) = pkgs.get_mut(&excluded_pkg.id) else {
74 continue;
76 };
77 if !pkg.config.release() {
78 continue;
79 }
80
81 let crate_name = pkg.meta.name.as_str();
82 let explicitly_excluded = self.workspace.exclude.contains(&excluded_pkg.name);
83 if pkg.config.release()
86 && pkg.config.publish()
87 && self.unpublished
88 && !explicitly_excluded
89 {
90 let version = &pkg.initial_version;
91 if !crate::ops::cargo::is_published(
92 &mut index,
93 pkg.config.registry(),
94 crate_name,
95 &version.full_version_string,
96 pkg.config.certs_source(),
97 ) {
98 log::debug!(
99 "enabled {}, v{} is unpublished",
100 crate_name,
101 version.full_version_string
102 );
103 continue;
104 }
105 }
106
107 pkg.config.pre_release_replacements = Some(vec![]);
108 pkg.config.release = Some(false);
109 }
110
111 let pkgs = plan::plan(pkgs)?;
112
113 let (selected_pkgs, _excluded_pkgs): (Vec<_>, Vec<_>) = pkgs
114 .into_iter()
115 .map(|(_, pkg)| pkg)
116 .partition(|p| p.config.release());
117 if selected_pkgs.is_empty() {
118 let _ = crate::ops::shell::error("no packages selected");
119 return Err(2.into());
120 }
121
122 let dry_run = !self.execute;
123 let mut failed = false;
124
125 failed |= !super::verify_git_is_clean(
127 ws_meta.workspace_root.as_std_path(),
128 dry_run,
129 log::Level::Warn,
130 )?;
131
132 super::warn_changed(&ws_meta, &selected_pkgs)?;
133
134 failed |= !super::verify_git_branch(
135 ws_meta.workspace_root.as_std_path(),
136 &ws_config,
137 dry_run,
138 log::Level::Warn,
139 )?;
140
141 failed |= !super::verify_if_behind(
142 ws_meta.workspace_root.as_std_path(),
143 &ws_config,
144 dry_run,
145 log::Level::Warn,
146 )?;
147
148 super::confirm("Bump", &selected_pkgs, self.no_confirm, dry_run)?;
150
151 for pkg in &selected_pkgs {
153 hook(&ws_meta, pkg, dry_run)?;
154 }
155
156 super::finish(failed, dry_run)
157 }
158
159 fn to_config(&self) -> crate::config::ConfigArgs {
160 crate::config::ConfigArgs {
161 custom_config: self.custom_config.clone(),
162 isolated: self.isolated,
163 z: self.z.clone(),
164 allow_branch: self.allow_branch.clone(),
165 ..Default::default()
166 }
167 }
168}
169
170pub fn hook(
171 ws_meta: &cargo_metadata::Metadata,
172 pkg: &plan::PackageRelease,
173 dry_run: bool,
174) -> Result<(), CliError> {
175 if let Some(pre_rel_hook) = pkg.config.pre_release_hook() {
176 let cwd = &pkg.package_root;
177 let crate_name = pkg.meta.name.as_str();
178 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
179 let prev_version_var = pkg.initial_version.bare_version_string.as_str();
180 let prev_metadata_var = pkg.initial_version.full_version.build.as_str();
181 let version_var = version.bare_version_string.as_str();
182 let metadata_var = version.full_version.build.as_str();
183 let template = Template {
184 prev_version: Some(prev_version_var),
185 prev_metadata: Some(prev_metadata_var),
186 version: Some(version_var),
187 metadata: Some(metadata_var),
188 crate_name: Some(crate_name),
189 date: Some(NOW.as_str()),
190 tag_name: pkg.planned_tag.as_deref(),
191 ..Default::default()
192 };
193 let pre_rel_hook = pre_rel_hook
194 .args()
195 .into_iter()
196 .map(|arg| template.render(arg))
197 .collect::<Vec<_>>();
198 log::debug!("calling pre-release hook: {:?}", pre_rel_hook);
199 let envs = maplit::btreemap! {
200 OsStr::new("PREV_VERSION") => prev_version_var.as_ref(),
201 OsStr::new("PREV_METADATA") => prev_metadata_var.as_ref(),
202 OsStr::new("NEW_VERSION") => version_var.as_ref(),
203 OsStr::new("NEW_METADATA") => metadata_var.as_ref(),
204 OsStr::new("DRY_RUN") => OsStr::new(if dry_run { "true" } else { "false" }),
205 OsStr::new("CRATE_NAME") => OsStr::new(crate_name),
206 OsStr::new("WORKSPACE_ROOT") => ws_meta.workspace_root.as_os_str(),
207 OsStr::new("CRATE_ROOT") => pkg.manifest_path.parent().unwrap_or_else(|| Path::new(".")).as_os_str(),
208 };
209 if !cmd::call_with_env(pre_rel_hook, envs, cwd, false)? {
212 let _ = crate::ops::shell::error(format!(
213 "release of {crate_name} aborted by non-zero return of prerelease hook."
214 ));
215 return Err(101.into());
216 }
217 }
218
219 Ok(())
220}