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}