cargo_release/steps/
replace.rs

1use crate::error::CliError;
2use crate::ops::git;
3use crate::ops::replace::{NOW, Template, do_file_replacements};
4use crate::steps::plan;
5
6/// Perform pre-release replacements
7#[derive(Debug, Clone, clap::Args)]
8pub struct ReplaceStep {
9    #[command(flatten)]
10    manifest: clap_cargo::Manifest,
11
12    #[command(flatten)]
13    workspace: clap_cargo::Workspace,
14
15    /// Process all packages whose current version is unpublished
16    #[arg(long)]
17    unpublished: bool,
18
19    /// Custom config file
20    #[arg(short, long = "config")]
21    custom_config: Option<std::path::PathBuf>,
22
23    /// Ignore implicit configuration files.
24    #[arg(long)]
25    isolated: bool,
26
27    /// Unstable options
28    #[arg(short = 'Z', value_name = "FEATURE")]
29    z: Vec<crate::config::UnstableValues>,
30
31    /// Comma-separated globs of branch names a release can happen from
32    #[arg(long, value_delimiter = ',')]
33    allow_branch: Option<Vec<String>>,
34
35    /// Actually perform a release. Dry-run mode is the default
36    #[arg(short = 'x', long)]
37    execute: bool,
38
39    #[arg(short = 'n', long, conflicts_with = "execute", hide = true)]
40    dry_run: bool,
41
42    /// Skip release confirmation and version preview
43    #[arg(long)]
44    no_confirm: bool,
45}
46
47impl ReplaceStep {
48    pub fn run(&self) -> Result<(), CliError> {
49        git::git_version()?;
50        let mut index = crate::ops::index::CratesIoIndex::new();
51
52        if self.dry_run {
53            let _ =
54                crate::ops::shell::warn("`--dry-run` is superfluous, dry-run is done by default");
55        }
56
57        let ws_meta = self
58            .manifest
59            .metadata()
60            // When evaluating dependency ordering, we need to consider optional dependencies
61            .features(cargo_metadata::CargoOpt::AllFeatures)
62            .exec()?;
63        let config = self.to_config();
64        let ws_config = crate::config::load_workspace_config(&config, &ws_meta)?;
65        let mut pkgs = plan::load(&config, &ws_meta)?;
66
67        let (_selected_pkgs, excluded_pkgs) =
68            if self.unpublished && self.workspace == clap_cargo::Workspace::default() {
69                ws_meta.packages.iter().partition(|_| false)
70            } else {
71                self.workspace.partition_packages(&ws_meta)
72            };
73        for excluded_pkg in excluded_pkgs {
74            let Some(pkg) = pkgs.get_mut(&excluded_pkg.id) else {
75                // Either not in workspace or marked as `release = false`.
76                continue;
77            };
78            if !pkg.config.release() {
79                continue;
80            }
81
82            let crate_name = pkg.meta.name.as_str();
83            let explicitly_excluded = self.workspace.exclude.contains(&excluded_pkg.name);
84            // 1. Don't show this message if already not releasing in config
85            // 2. Still respect `--exclude`
86            if pkg.config.release()
87                && pkg.config.publish()
88                && self.unpublished
89                && !explicitly_excluded
90            {
91                let version = &pkg.initial_version;
92                if !crate::ops::cargo::is_published(
93                    &mut index,
94                    pkg.config.registry(),
95                    crate_name,
96                    &version.full_version_string,
97                    pkg.config.certs_source(),
98                ) {
99                    log::debug!(
100                        "enabled {}, v{} is unpublished",
101                        crate_name,
102                        version.full_version_string
103                    );
104                    continue;
105                }
106            }
107
108            pkg.config.pre_release_replacements = Some(vec![]);
109            pkg.config.release = Some(false);
110        }
111
112        let pkgs = plan::plan(pkgs)?;
113
114        let (selected_pkgs, _excluded_pkgs): (Vec<_>, Vec<_>) = pkgs
115            .into_iter()
116            .map(|(_, pkg)| pkg)
117            .partition(|p| p.config.release());
118        if selected_pkgs.is_empty() {
119            let _ = crate::ops::shell::error("no packages selected");
120            return Err(2.into());
121        }
122
123        let dry_run = !self.execute;
124        let mut failed = false;
125
126        // STEP 0: Help the user make the right decisions.
127        failed |= !super::verify_git_is_clean(
128            ws_meta.workspace_root.as_std_path(),
129            dry_run,
130            log::Level::Warn,
131        )?;
132
133        super::warn_changed(&ws_meta, &selected_pkgs)?;
134
135        failed |= !super::verify_git_branch(
136            ws_meta.workspace_root.as_std_path(),
137            &ws_config,
138            dry_run,
139            log::Level::Warn,
140        )?;
141
142        failed |= !super::verify_if_behind(
143            ws_meta.workspace_root.as_std_path(),
144            &ws_config,
145            dry_run,
146            log::Level::Warn,
147        )?;
148
149        // STEP 1: Release Confirmation
150        super::confirm("Bump", &selected_pkgs, self.no_confirm, dry_run)?;
151
152        // STEP 2: update current version, save and commit
153        for pkg in &selected_pkgs {
154            replace(pkg, dry_run)?;
155        }
156
157        super::finish(failed, dry_run)
158    }
159
160    fn to_config(&self) -> crate::config::ConfigArgs {
161        crate::config::ConfigArgs {
162            custom_config: self.custom_config.clone(),
163            isolated: self.isolated,
164            z: self.z.clone(),
165            allow_branch: self.allow_branch.clone(),
166            ..Default::default()
167        }
168    }
169}
170
171pub fn replace(pkg: &plan::PackageRelease, dry_run: bool) -> Result<(), CliError> {
172    let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
173    if !pkg.config.pre_release_replacements().is_empty() {
174        let cwd = &pkg.package_root;
175        let crate_name = pkg.meta.name.as_str();
176        let prev_version_var = pkg.initial_version.bare_version_string.as_str();
177        let prev_metadata_var = pkg.initial_version.full_version.build.as_str();
178        let version_var = version.bare_version_string.as_str();
179        let metadata_var = version.full_version.build.as_str();
180        // try replacing text in configured files
181        let template = Template {
182            prev_version: Some(prev_version_var),
183            prev_metadata: Some(prev_metadata_var),
184            version: Some(version_var),
185            metadata: Some(metadata_var),
186            crate_name: Some(crate_name),
187            date: Some(NOW.as_str()),
188            tag_name: pkg.planned_tag.as_deref(),
189            ..Default::default()
190        };
191        let prerelease = version.is_prerelease();
192        let noisy = true;
193        do_file_replacements(
194            pkg.config.pre_release_replacements(),
195            &template,
196            cwd,
197            prerelease,
198            noisy,
199            dry_run,
200        )?;
201    }
202
203    Ok(())
204}