cargo_release/steps/
push.rs

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