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                && git::tag_exists(ws_meta.workspace_root.as_std_path(), tag_name)?
93            {
94                let crate_name = pkg.meta.name.as_str();
95                let _ = crate::ops::shell::warn(format!(
96                    "disabled due to existing tag ({tag_name}), skipping {crate_name}"
97                ));
98                pkg.planned_tag = None;
99                pkg.config.tag = Some(false);
100                pkg.config.release = Some(false);
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            && seen_tags.insert(tag_name)
163        {
164            let cwd = &pkg.package_root;
165            let crate_name = pkg.meta.name.as_str();
166
167            let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
168            let prev_version_var = pkg.initial_version.bare_version_string.as_str();
169            let prev_metadata_var = pkg.initial_version.full_version.build.as_str();
170            let version_var = version.bare_version_string.as_str();
171            let metadata_var = version.full_version.build.as_str();
172            let template = Template {
173                prev_version: Some(prev_version_var),
174                prev_metadata: Some(prev_metadata_var),
175                version: Some(version_var),
176                metadata: Some(metadata_var),
177                crate_name: Some(crate_name),
178                tag_name: Some(tag_name),
179                date: Some(NOW.as_str()),
180                ..Default::default()
181            };
182            let tag_message = template.render(pkg.config.tag_message());
183
184            log::debug!("creating git tag {tag_name}");
185            if !git::tag(cwd, tag_name, &tag_message, pkg.config.sign_tag(), dry_run)? {
186                // tag failed, abort release
187                return Err(101.into());
188            }
189        }
190    }
191
192    Ok(())
193}