cargo_release/steps/
tag.rs

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