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) =
72 if self.unpublished && self.workspace == clap_cargo::Workspace::default() {
73 ws_meta.packages.iter().partition(|_| false)
74 } else {
75 self.workspace.partition_packages(&ws_meta)
76 };
77 for excluded_pkg in excluded_pkgs {
78 let Some(pkg) = pkgs.get_mut(&excluded_pkg.id) else {
79 continue;
81 };
82 if !pkg.config.release() {
83 continue;
84 }
85
86 let crate_name = pkg.meta.name.as_str();
87 let explicitly_excluded = self.workspace.exclude.contains(&excluded_pkg.name);
88 if pkg.config.release()
91 && pkg.config.publish()
92 && self.unpublished
93 && !explicitly_excluded
94 {
95 let version = &pkg.initial_version;
96 if !crate::ops::cargo::is_published(
97 &mut index,
98 pkg.config.registry(),
99 crate_name,
100 &version.full_version_string,
101 pkg.config.certs_source(),
102 ) {
103 log::debug!(
104 "enabled {}, v{} is unpublished",
105 crate_name,
106 version.full_version_string
107 );
108 continue;
109 }
110 }
111
112 pkg.config.pre_release_replacements = Some(vec![]);
113 pkg.config.release = Some(false);
114 }
115
116 let pkgs = plan::plan(pkgs)?;
117
118 let (selected_pkgs, _excluded_pkgs): (Vec<_>, Vec<_>) = pkgs
119 .into_iter()
120 .map(|(_, pkg)| pkg)
121 .partition(|p| p.config.release());
122 if selected_pkgs.is_empty() {
123 let _ = crate::ops::shell::error("no packages selected");
124 return Err(2.into());
125 }
126
127 let dry_run = !self.execute;
128 let mut failed = false;
129
130 failed |= !super::verify_git_is_clean(
132 ws_meta.workspace_root.as_std_path(),
133 dry_run,
134 log::Level::Warn,
135 )?;
136
137 super::warn_changed(&ws_meta, &selected_pkgs)?;
138
139 failed |= !super::verify_git_branch(
140 ws_meta.workspace_root.as_std_path(),
141 &ws_config,
142 dry_run,
143 log::Level::Warn,
144 )?;
145
146 failed |= !super::verify_if_behind(
147 ws_meta.workspace_root.as_std_path(),
148 &ws_config,
149 dry_run,
150 log::Level::Warn,
151 )?;
152
153 super::confirm("Bump", &selected_pkgs, self.no_confirm, dry_run)?;
155
156 for pkg in &selected_pkgs {
158 hook(&ws_meta, pkg, dry_run)?;
159 }
160
161 super::finish(failed, dry_run)
162 }
163
164 fn to_config(&self) -> crate::config::ConfigArgs {
165 crate::config::ConfigArgs {
166 custom_config: self.custom_config.clone(),
167 isolated: self.isolated,
168 z: self.z.clone(),
169 allow_branch: self.allow_branch.clone(),
170 ..Default::default()
171 }
172 }
173}
174
175pub fn hook(
176 ws_meta: &cargo_metadata::Metadata,
177 pkg: &plan::PackageRelease,
178 dry_run: bool,
179) -> Result<(), CliError> {
180 if let Some(pre_rel_hook) = pkg.config.pre_release_hook() {
181 let cwd = &pkg.package_root;
182 let crate_name = pkg.meta.name.as_str();
183 let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
184 let prev_version_var = pkg.initial_version.bare_version_string.as_str();
185 let prev_metadata_var = pkg.initial_version.full_version.build.as_str();
186 let version_var = version.bare_version_string.as_str();
187 let metadata_var = version.full_version.build.as_str();
188 let template = Template {
189 prev_version: Some(prev_version_var),
190 prev_metadata: Some(prev_metadata_var),
191 version: Some(version_var),
192 metadata: Some(metadata_var),
193 crate_name: Some(crate_name),
194 date: Some(NOW.as_str()),
195 tag_name: pkg.planned_tag.as_deref(),
196 ..Default::default()
197 };
198 let pre_rel_hook = pre_rel_hook
199 .args()
200 .into_iter()
201 .map(|arg| template.render(arg))
202 .collect::<Vec<_>>();
203 log::debug!("calling pre-release hook: {pre_rel_hook:?}");
204 let envs = maplit::btreemap! {
205 OsStr::new("PREV_VERSION") => prev_version_var.as_ref(),
206 OsStr::new("PREV_METADATA") => prev_metadata_var.as_ref(),
207 OsStr::new("NEW_VERSION") => version_var.as_ref(),
208 OsStr::new("NEW_METADATA") => metadata_var.as_ref(),
209 OsStr::new("DRY_RUN") => OsStr::new(if dry_run { "true" } else { "false" }),
210 OsStr::new("CRATE_NAME") => OsStr::new(crate_name),
211 OsStr::new("WORKSPACE_ROOT") => ws_meta.workspace_root.as_os_str(),
212 OsStr::new("CRATE_ROOT") => pkg.manifest_path.parent().unwrap_or_else(|| Path::new(".")).as_os_str(),
213 };
214 if !cmd::call_with_env(pre_rel_hook, envs, cwd, false)? {
217 let _ = crate::ops::shell::error(format!(
218 "release of {crate_name} aborted by non-zero return of prerelease hook."
219 ));
220 return Err(101.into());
221 }
222 }
223
224 Ok(())
225}