cargo_release/steps/
version.rs

1use crate::error::CargoResult;
2use crate::error::CliError;
3use crate::ops::git;
4use crate::steps::plan;
5
6/// Bump crate versions
7#[derive(Debug, Clone, clap::Args)]
8pub struct VersionStep {
9    #[command(flatten)]
10    manifest: clap_cargo::Manifest,
11
12    #[command(flatten)]
13    workspace: clap_cargo::Workspace,
14
15    /// Custom config file
16    #[arg(short, long = "config")]
17    custom_config: Option<std::path::PathBuf>,
18
19    /// Ignore implicit configuration files.
20    #[arg(long)]
21    isolated: bool,
22
23    /// Unstable options
24    #[arg(short, value_name = "FEATURE")]
25    z: Vec<crate::config::UnstableValues>,
26
27    /// Comma-separated globs of branch names a release can happen from
28    #[arg(long, value_delimiter = ',')]
29    allow_branch: Option<Vec<String>>,
30
31    /// Actually perform a release. Dry-run mode is the default
32    #[arg(short = 'x', long)]
33    execute: bool,
34
35    #[arg(short = 'n', long, conflicts_with = "execute", hide = true)]
36    dry_run: bool,
37
38    /// Skip release confirmation and version preview
39    #[arg(long)]
40    no_confirm: bool,
41
42    /// Either bump by LEVEL or set the VERSION for all selected packages
43    #[arg(value_name = "LEVEL|VERSION", help_heading = "Version")]
44    level_or_version: super::TargetVersion,
45
46    /// Semver metadata
47    #[arg(short, long, help_heading = "Version")]
48    metadata: Option<String>,
49
50    /// The name of tag for the previous release.
51    #[arg(long, value_name = "NAME", help_heading = "Version")]
52    prev_tag_name: Option<String>,
53}
54
55impl VersionStep {
56    pub fn run(&self) -> Result<(), CliError> {
57        git::git_version()?;
58
59        if self.dry_run {
60            let _ =
61                crate::ops::shell::warn("`--dry-run` is superfluous, dry-run is done by default");
62        }
63
64        let ws_meta = self
65            .manifest
66            .metadata()
67            // When evaluating dependency ordering, we need to consider optional dependencies
68            .features(cargo_metadata::CargoOpt::AllFeatures)
69            .exec()?;
70        let config = self.to_config();
71        let ws_config = crate::config::load_workspace_config(&config, &ws_meta)?;
72        let mut pkgs = plan::load(&config, &ws_meta)?;
73
74        for pkg in pkgs.values_mut() {
75            if let Some(prev_tag) = self.prev_tag_name.as_ref() {
76                // Trust the user that the tag passed in is the latest tag for the workspace and that
77                // they don't care about any changes from before this tag.
78                pkg.set_prior_tag(prev_tag.to_owned());
79            }
80            if pkg.config.release() {
81                pkg.bump(&self.level_or_version, self.metadata.as_deref())?;
82            }
83        }
84
85        let (_selected_pkgs, excluded_pkgs) = self.workspace.partition_packages(&ws_meta);
86        for excluded_pkg in excluded_pkgs {
87            let Some(pkg) = pkgs.get_mut(&excluded_pkg.id) else {
88                // Either not in workspace or marked as `release = false`.
89                continue;
90            };
91            if !pkg.config.release() {
92                continue;
93            }
94
95            pkg.planned_version = None;
96            pkg.config.release = Some(false);
97        }
98
99        let pkgs = plan::plan(pkgs)?;
100
101        let (selected_pkgs, excluded_pkgs): (Vec<_>, Vec<_>) = pkgs
102            .into_iter()
103            .map(|(_, pkg)| pkg)
104            .partition(|p| p.config.release());
105        if selected_pkgs.is_empty() {
106            let _ = crate::ops::shell::error("no packages selected");
107            return Err(2.into());
108        }
109
110        let dry_run = !self.execute;
111        let mut failed = false;
112
113        // STEP 0: Help the user make the right decisions.
114        failed |= !super::verify_git_is_clean(
115            ws_meta.workspace_root.as_std_path(),
116            dry_run,
117            log::Level::Warn,
118        )?;
119
120        failed |=
121            !super::verify_monotonically_increasing(&selected_pkgs, dry_run, log::Level::Error)?;
122
123        super::warn_changed(&ws_meta, &selected_pkgs)?;
124
125        failed |= !super::verify_git_branch(
126            ws_meta.workspace_root.as_std_path(),
127            &ws_config,
128            dry_run,
129            log::Level::Warn,
130        )?;
131
132        failed |= !super::verify_if_behind(
133            ws_meta.workspace_root.as_std_path(),
134            &ws_config,
135            dry_run,
136            log::Level::Warn,
137        )?;
138
139        // STEP 1: Release Confirmation
140        super::confirm("Bump", &selected_pkgs, self.no_confirm, dry_run)?;
141
142        // STEP 2: update current version, save and commit
143        let update_lock = update_versions(&ws_meta, &selected_pkgs, &excluded_pkgs, dry_run)?;
144        if update_lock {
145            log::debug!("Updating lock file");
146            if !dry_run {
147                let workspace_path = ws_meta.workspace_root.as_std_path().join("Cargo.toml");
148                crate::ops::cargo::update_lock(&workspace_path)?;
149            }
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 changed_since(
167    ws_meta: &cargo_metadata::Metadata,
168    pkg: &plan::PackageRelease,
169    since_ref: &str,
170) -> Option<Vec<std::path::PathBuf>> {
171    let changed_root = if pkg.bin {
172        ws_meta.workspace_root.as_std_path()
173    } else {
174        // Limit our lookup since we don't need to check for `Cargo.lock`
175        &pkg.package_root
176    };
177    let changed = git::changed_files(changed_root, since_ref).ok().flatten()?;
178    let changed: Vec<_> = changed
179        .into_iter()
180        .filter(|p| pkg.package_content.contains(p))
181        .collect();
182
183    Some(changed)
184}
185
186pub fn update_versions(
187    ws_meta: &cargo_metadata::Metadata,
188    selected_pkgs: &[plan::PackageRelease],
189    excluded_pkgs: &[plan::PackageRelease],
190    dry_run: bool,
191) -> CargoResult<bool> {
192    let mut changed = false;
193
194    let workspace_version = selected_pkgs
195        .iter()
196        .filter(|p| p.config.shared_version() == Some(crate::config::SharedVersion::WORKSPACE))
197        .find_map(|p| p.planned_version.clone());
198
199    if let Some(workspace_version) = &workspace_version {
200        let _ = crate::ops::shell::status(
201            "Upgrading",
202            format!(
203                "workspace to version {}",
204                workspace_version.full_version_string
205            ),
206        );
207        let workspace_path = ws_meta.workspace_root.as_std_path().join("Cargo.toml");
208        crate::ops::cargo::set_workspace_version(
209            &workspace_path,
210            workspace_version.full_version_string.as_str(),
211            dry_run,
212        )?;
213        // Deferring `update_dependent_versions` to the per-package logic
214        changed = true;
215    }
216
217    for (selected, pkg) in selected_pkgs
218        .iter()
219        .map(|s| (true, s))
220        .chain(excluded_pkgs.iter().map(|s| (false, s)))
221    {
222        let is_inherited =
223            pkg.config.shared_version() == Some(crate::config::SharedVersion::WORKSPACE);
224        let planned_version = if is_inherited {
225            workspace_version.as_ref()
226        } else if let Some(version) = pkg.planned_version.as_ref() {
227            assert!(selected);
228            Some(version)
229        } else {
230            None
231        };
232
233        if let Some(version) = planned_version {
234            if is_inherited {
235                let crate_name = pkg.meta.name.as_str();
236                let _ = crate::ops::shell::status(
237                    "Upgrading",
238                    format!(
239                        "{} from {} to {} (inherited from workspace)",
240                        crate_name,
241                        pkg.initial_version.full_version_string,
242                        version.full_version_string
243                    ),
244                );
245            } else {
246                let crate_name = pkg.meta.name.as_str();
247                let _ = crate::ops::shell::status(
248                    "Upgrading",
249                    format!(
250                        "{} from {} to {}",
251                        crate_name,
252                        pkg.initial_version.full_version_string,
253                        version.full_version_string
254                    ),
255                );
256                crate::ops::cargo::set_package_version(
257                    &pkg.manifest_path,
258                    version.full_version_string.as_str(),
259                    dry_run,
260                )?;
261            }
262            update_dependent_versions(ws_meta, pkg, version, dry_run)?;
263            changed = true;
264        }
265    }
266
267    Ok(changed)
268}
269
270pub fn update_dependent_versions(
271    ws_meta: &cargo_metadata::Metadata,
272    pkg: &plan::PackageRelease,
273    version: &plan::Version,
274    dry_run: bool,
275) -> CargoResult<()> {
276    // This is redundant with iterating over `workspace_members`
277    // - As `find_dependency_tables` returns workspace dependencies
278    // - If there is a root package
279    //
280    // But split this out for
281    // - Virtual manifests
282    // - Nicer message to the user
283    {
284        let workspace_path = ws_meta.workspace_root.as_std_path().join("Cargo.toml");
285        crate::ops::cargo::upgrade_dependency_req(
286            "workspace",
287            &workspace_path,
288            &pkg.package_root,
289            &pkg.meta.name,
290            &version.full_version,
291            pkg.config.dependent_version(),
292            dry_run,
293        )?;
294    }
295
296    for dep in find_ws_members(ws_meta) {
297        crate::ops::cargo::upgrade_dependency_req(
298            &dep.name,
299            dep.manifest_path.as_std_path(),
300            &pkg.package_root,
301            &pkg.meta.name,
302            &version.full_version,
303            pkg.config.dependent_version(),
304            dry_run,
305        )?;
306    }
307
308    Ok(())
309}
310
311fn find_ws_members(
312    ws_meta: &cargo_metadata::Metadata,
313) -> impl Iterator<Item = &cargo_metadata::Package> {
314    let workspace_members: std::collections::HashSet<_> =
315        ws_meta.workspace_members.iter().collect();
316    ws_meta
317        .packages
318        .iter()
319        .filter(move |p| workspace_members.contains(&p.id))
320}