cargo_release/steps/
commit.rs

1use crate::config;
2use crate::error::CliError;
3use crate::ops::git;
4use crate::ops::replace::{NOW, Template};
5use crate::steps::plan;
6
7/// Commit the specified packages
8///
9/// Will automatically skip published versions
10#[derive(Debug, Clone, clap::Args)]
11pub struct CommitStep {
12    #[command(flatten)]
13    manifest: clap_cargo::Manifest,
14
15    /// Custom config file
16    #[arg(short, long = "config", value_name = "PATH")]
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 = 'Z', value_name = "FEATURE")]
25    z: Vec<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    #[command(flatten)]
43    commit: config::CommitArgs,
44}
45
46impl CommitStep {
47    pub fn run(&self) -> Result<(), CliError> {
48        git::git_version()?;
49
50        if self.dry_run {
51            let _ =
52                crate::ops::shell::warn("`--dry-run` is superfluous, dry-run is done by default");
53        }
54
55        let ws_meta = self
56            .manifest
57            .metadata()
58            // When evaluating dependency ordering, we need to consider optional dependencies
59            .features(cargo_metadata::CargoOpt::AllFeatures)
60            .exec()?;
61        let config = self.to_config();
62        let ws_config = config::load_workspace_config(&config, &ws_meta)?;
63        let pkgs = plan::load(&config, &ws_meta)?;
64
65        let pkgs = plan::plan(pkgs)?;
66
67        let (selected_pkgs, excluded_pkgs): (Vec<_>, Vec<_>) = pkgs
68            .into_iter()
69            .map(|(_, pkg)| pkg)
70            .partition(|p| p.config.release());
71        if git::is_dirty(ws_meta.workspace_root.as_std_path())?.is_none() {
72            let _ = crate::ops::shell::error("nothing to commit");
73            return Err(2.into());
74        }
75
76        let dry_run = !self.execute;
77        let mut failed = false;
78
79        // STEP 0: Help the user make the right decisions.
80        failed |= !super::verify_git_branch(
81            ws_meta.workspace_root.as_std_path(),
82            &ws_config,
83            dry_run,
84            log::Level::Warn,
85        )?;
86
87        failed |= !super::verify_if_behind(
88            ws_meta.workspace_root.as_std_path(),
89            &ws_config,
90            dry_run,
91            log::Level::Warn,
92        )?;
93
94        // STEP 1: Release Confirmation
95        super::confirm("Commit", &selected_pkgs, self.no_confirm, dry_run)?;
96
97        if ws_config.is_workspace {
98            let consolidate_commits = super::consolidate_commits(&selected_pkgs, &excluded_pkgs)?;
99            if !consolidate_commits {
100                let _ = crate::ops::shell::warn(
101                    "ignoring `consolidate-commits=false`; `cargo release commit` can effectively only do one commit",
102                );
103            }
104            workspace_commit(&ws_meta, &ws_config, &selected_pkgs, dry_run)?;
105        } else if !selected_pkgs.is_empty() {
106            let selected_pkg = selected_pkgs
107                .first()
108                .expect("non-workspace can have at most 1 package");
109            pkg_commit(selected_pkg, dry_run)?;
110        }
111
112        super::finish(failed, dry_run)
113    }
114
115    fn to_config(&self) -> config::ConfigArgs {
116        config::ConfigArgs {
117            custom_config: self.custom_config.clone(),
118            isolated: self.isolated,
119            z: self.z.clone(),
120            allow_branch: self.allow_branch.clone(),
121            commit: self.commit.clone(),
122            ..Default::default()
123        }
124    }
125}
126
127pub fn pkg_commit(pkg: &plan::PackageRelease, dry_run: bool) -> Result<(), CliError> {
128    let cwd = &pkg.package_root;
129    let crate_name = pkg.meta.name.as_str();
130    let version = pkg.planned_version.as_ref().unwrap_or(&pkg.initial_version);
131    let prev_version_var = pkg.initial_version.bare_version_string.as_str();
132    let prev_metadata_var = pkg.initial_version.full_version.build.as_str();
133    let version_var = version.bare_version_string.as_str();
134    let metadata_var = version.full_version.build.as_str();
135    let template = Template {
136        prev_version: Some(prev_version_var),
137        prev_metadata: Some(prev_metadata_var),
138        version: Some(version_var),
139        metadata: Some(metadata_var),
140        crate_name: Some(crate_name),
141        date: Some(NOW.as_str()),
142        ..Default::default()
143    };
144    let commit_msg = template.render(pkg.config.pre_release_commit_message());
145    let sign = pkg.config.sign_commit();
146    if !git::commit_all(cwd, &commit_msg, sign, dry_run)? {
147        // commit failed, abort release
148        return Err(101.into());
149    }
150
151    Ok(())
152}
153
154pub fn workspace_commit(
155    ws_meta: &cargo_metadata::Metadata,
156    ws_config: &config::Config,
157    pkgs: &[plan::PackageRelease],
158    dry_run: bool,
159) -> Result<(), CliError> {
160    let shared_version = super::find_shared_versions(pkgs)?;
161
162    let shared_commit_msg = {
163        let version_var = shared_version
164            .as_ref()
165            .map(|v| v.bare_version_string.as_str());
166        let metadata_var = shared_version
167            .as_ref()
168            .map(|v| v.full_version.build.as_str());
169        let template = Template {
170            version: version_var,
171            metadata: metadata_var,
172            date: Some(NOW.as_str()),
173            ..Default::default()
174        };
175        template.render(ws_config.pre_release_commit_message())
176    };
177    if !git::commit_all(
178        ws_meta.workspace_root.as_std_path(),
179        &shared_commit_msg,
180        ws_config.sign_commit(),
181        dry_run,
182    )? {
183        // commit failed, abort release
184        return Err(101.into());
185    }
186
187    Ok(())
188}