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) = self.workspace.partition_packages(&ws_meta);
68        for excluded_pkg in excluded_pkgs {
69            let Some(pkg) = pkgs.get_mut(&excluded_pkg.id) else {
70                // Either not in workspace or marked as `release = false`.
71                continue;
72            };
73            if !pkg.config.release() {
74                continue;
75            }
76
77            let crate_name = pkg.meta.name.as_str();
78            let explicitly_excluded = self.workspace.exclude.contains(&excluded_pkg.name);
79            // 1. Don't show this message if already not releasing in config
80            // 2. Still respect `--exclude`
81            if pkg.config.release()
82                && pkg.config.publish()
83                && self.unpublished
84                && !explicitly_excluded
85            {
86                let version = &pkg.initial_version;
87                if !crate::ops::cargo::is_published(
88                    &mut index,
89                    pkg.config.registry(),
90                    crate_name,
91                    &version.full_version_string,
92                    pkg.config.certs_source(),
93                ) {
94                    log::debug!(
95                        "enabled {}, v{} is unpublished",
96                        crate_name,
97                        version.full_version_string
98                    );
99                    continue;
100                }
101            }
102
103            pkg.config.pre_release_replacements = Some(vec![]);
104            pkg.config.release = Some(false);
105        }
106
107        let pkgs = plan::plan(pkgs)?;
108
109        let (selected_pkgs, _excluded_pkgs): (Vec<_>, Vec<_>) = pkgs
110            .into_iter()
111            .map(|(_, pkg)| pkg)
112            .partition(|p| p.config.release());
113        if selected_pkgs.is_empty() {
114            let _ = crate::ops::shell::error("no packages selected");
115            return Err(2.into());
116        }
117
118        let dry_run = !self.execute;
119        let mut failed = false;
120
121        // STEP 0: Help the user make the right decisions.
122        failed |= !super::verify_git_is_clean(
123            ws_meta.workspace_root.as_std_path(),
124            dry_run,
125            log::Level::Warn,
126        )?;
127
128        super::warn_changed(&ws_meta, &selected_pkgs)?;
129
130        failed |= !super::verify_git_branch(
131            ws_meta.workspace_root.as_std_path(),
132            &ws_config,
133            dry_run,
134            log::Level::Warn,
135        )?;
136
137        failed |= !super::verify_if_behind(
138            ws_meta.workspace_root.as_std_path(),
139            &ws_config,
140            dry_run,
141            log::Level::Warn,
142        )?;
143
144        // STEP 1: Release Confirmation
145        super::confirm("Bump", &selected_pkgs, self.no_confirm, dry_run)?;
146
147        // STEP 2: update current version, save and commit
148        for pkg in &selected_pkgs {
149            replace(pkg, dry_run)?;
150        }
151
152        super::finish(failed, dry_run)
153    }
154
155    fn to_config(&self) -> crate::config::ConfigArgs {
156        crate::config::ConfigArgs {
157            custom_config: self.custom_config.clone(),
158            isolated: self.isolated,
159            z: self.z.clone(),
160            allow_branch: self.allow_branch.clone(),
161            ..Default::default()
162        }
163    }
164}
165
166pub fn replace(pkg: &plan::PackageRelease, dry_run: bool) -> Result<(), CliError> {
167    let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
168    if !pkg.config.pre_release_replacements().is_empty() {
169        let cwd = &pkg.package_root;
170        let crate_name = pkg.meta.name.as_str();
171        let prev_version_var = pkg.initial_version.bare_version_string.as_str();
172        let prev_metadata_var = pkg.initial_version.full_version.build.as_str();
173        let version_var = version.bare_version_string.as_str();
174        let metadata_var = version.full_version.build.as_str();
175        // try replacing text in configured files
176        let template = Template {
177            prev_version: Some(prev_version_var),
178            prev_metadata: Some(prev_metadata_var),
179            version: Some(version_var),
180            metadata: Some(metadata_var),
181            crate_name: Some(crate_name),
182            date: Some(NOW.as_str()),
183            tag_name: pkg.planned_tag.as_deref(),
184            ..Default::default()
185        };
186        let prerelease = version.is_prerelease();
187        let noisy = true;
188        do_file_replacements(
189            pkg.config.pre_release_replacements(),
190            &template,
191            cwd,
192            prerelease,
193            noisy,
194            dry_run,
195        )?;
196    }
197
198    Ok(())
199}