cargo_release/
config.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::Context as _;
4use serde::{Deserialize, Serialize};
5
6use crate::error::CargoResult;
7use crate::ops::cargo;
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10#[serde(deny_unknown_fields, default)]
11#[serde(rename_all = "kebab-case")]
12pub struct Config {
13    #[serde(skip)]
14    pub is_workspace: bool,
15    pub unstable: Unstable,
16    pub allow_branch: Option<Vec<String>>,
17    pub sign_commit: Option<bool>,
18    pub sign_tag: Option<bool>,
19    pub push_remote: Option<String>,
20    pub registry: Option<String>,
21    pub release: Option<bool>,
22    pub publish: Option<bool>,
23    pub verify: Option<bool>,
24    pub owners: Option<Vec<String>>,
25    pub push: Option<bool>,
26    pub push_options: Option<Vec<String>>,
27    pub shared_version: Option<SharedVersion>,
28    pub consolidate_commits: Option<bool>,
29    pub pre_release_commit_message: Option<String>,
30    pub pre_release_replacements: Option<Vec<Replace>>,
31    pub pre_release_hook: Option<Command>,
32    pub tag_message: Option<String>,
33    pub tag_prefix: Option<String>,
34    pub tag_name: Option<String>,
35    pub tag: Option<bool>,
36    pub enable_features: Option<Vec<String>>,
37    pub enable_all_features: Option<bool>,
38    pub dependent_version: Option<DependentVersion>,
39    pub metadata: Option<MetadataPolicy>,
40    pub target: Option<String>,
41    pub rate_limit: RateLimit,
42    pub certs_source: Option<CertsSource>,
43}
44
45impl Config {
46    pub fn new() -> Self {
47        Default::default()
48    }
49
50    pub fn from_defaults() -> Self {
51        let empty = Config::new();
52        Config {
53            is_workspace: true,
54            unstable: Unstable::from_defaults(),
55            allow_branch: Some(
56                empty
57                    .allow_branch()
58                    .map(|s| s.to_owned())
59                    .collect::<Vec<String>>(),
60            ),
61            sign_commit: Some(empty.sign_commit()),
62            sign_tag: Some(empty.sign_tag()),
63            push_remote: Some(empty.push_remote().to_owned()),
64            registry: empty.registry().map(|s| s.to_owned()),
65            release: Some(empty.release()),
66            publish: Some(empty.publish()),
67            verify: Some(empty.verify()),
68            owners: Some(empty.owners().to_vec()),
69            push: Some(empty.push()),
70            push_options: Some(
71                empty
72                    .push_options()
73                    .map(|s| s.to_owned())
74                    .collect::<Vec<String>>(),
75            ),
76            shared_version: empty
77                .shared_version()
78                .map(|s| SharedVersion::Name(s.to_owned())),
79            consolidate_commits: Some(empty.consolidate_commits()),
80            pre_release_commit_message: Some(empty.pre_release_commit_message().to_owned()),
81            pre_release_replacements: Some(empty.pre_release_replacements().to_vec()),
82            pre_release_hook: empty.pre_release_hook().cloned(),
83            tag_message: Some(empty.tag_message().to_owned()),
84            tag_prefix: None, // Skipping, its location dependent
85            tag_name: Some(empty.tag_name().to_owned()),
86            tag: Some(empty.tag()),
87            enable_features: Some(empty.enable_features().to_vec()),
88            enable_all_features: Some(empty.enable_all_features()),
89            dependent_version: Some(empty.dependent_version()),
90            metadata: Some(empty.metadata()),
91            target: None,
92            rate_limit: RateLimit::from_defaults(),
93            certs_source: Some(empty.certs_source()),
94        }
95    }
96
97    pub fn update(&mut self, source: &Config) {
98        self.unstable.update(&source.unstable);
99        if let Some(allow_branch) = source.allow_branch.as_deref() {
100            self.allow_branch = Some(allow_branch.to_owned());
101        }
102        if let Some(sign_commit) = source.sign_commit {
103            self.sign_commit = Some(sign_commit);
104        }
105        if let Some(sign_tag) = source.sign_tag {
106            self.sign_tag = Some(sign_tag);
107        }
108        if let Some(push_remote) = source.push_remote.as_deref() {
109            self.push_remote = Some(push_remote.to_owned());
110        }
111        if let Some(registry) = source.registry.as_deref() {
112            self.registry = Some(registry.to_owned());
113        }
114        if let Some(release) = source.release {
115            self.release = Some(release);
116        }
117        if let Some(publish) = source.publish {
118            self.publish = Some(publish);
119        }
120        if let Some(verify) = source.verify {
121            self.verify = Some(verify);
122        }
123        if let Some(owners) = source.owners.as_deref() {
124            self.owners = Some(owners.to_owned());
125        }
126        if let Some(push) = source.push {
127            self.push = Some(push);
128        }
129        if let Some(push_options) = source.push_options.as_deref() {
130            self.push_options = Some(push_options.to_owned());
131        }
132        if let Some(shared_version) = source.shared_version.clone() {
133            self.shared_version = Some(shared_version);
134        }
135        if let Some(consolidate_commits) = source.consolidate_commits {
136            self.consolidate_commits = Some(consolidate_commits);
137        }
138        if let Some(pre_release_commit_message) = source.pre_release_commit_message.as_deref() {
139            self.pre_release_commit_message = Some(pre_release_commit_message.to_owned());
140        }
141        if let Some(pre_release_replacements) = source.pre_release_replacements.as_deref() {
142            self.pre_release_replacements = Some(pre_release_replacements.to_owned());
143        }
144        if let Some(pre_release_hook) = source.pre_release_hook.as_ref() {
145            self.pre_release_hook = Some(pre_release_hook.to_owned());
146        }
147        if let Some(tag_message) = source.tag_message.as_deref() {
148            self.tag_message = Some(tag_message.to_owned());
149        }
150        if let Some(tag_prefix) = source.tag_prefix.as_deref() {
151            self.tag_prefix = Some(tag_prefix.to_owned());
152        }
153        if let Some(tag_name) = source.tag_name.as_deref() {
154            self.tag_name = Some(tag_name.to_owned());
155        }
156        if let Some(tag) = source.tag {
157            self.tag = Some(tag);
158        }
159        if let Some(enable_features) = source.enable_features.as_deref() {
160            self.enable_features = Some(enable_features.to_owned());
161        }
162        if let Some(enable_all_features) = source.enable_all_features {
163            self.enable_all_features = Some(enable_all_features);
164        }
165        if let Some(dependent_version) = source.dependent_version {
166            self.dependent_version = Some(dependent_version);
167        }
168        if let Some(metadata) = source.metadata {
169            self.metadata = Some(metadata);
170        }
171        if let Some(target) = source.target.as_deref() {
172            self.target = Some(target.to_owned());
173        }
174        self.rate_limit.update(&source.rate_limit);
175        if let Some(certs) = source.certs_source {
176            self.certs_source = Some(certs);
177        }
178    }
179
180    pub fn unstable(&self) -> &Unstable {
181        &self.unstable
182    }
183
184    pub fn allow_branch(&self) -> impl Iterator<Item = &str> {
185        self.allow_branch
186            .as_deref()
187            .map(|a| itertools::Either::Left(a.iter().map(|s| s.as_str())))
188            .unwrap_or_else(|| itertools::Either::Right(IntoIterator::into_iter(["*", "!HEAD"])))
189    }
190
191    pub fn sign_commit(&self) -> bool {
192        self.sign_commit.unwrap_or(false)
193    }
194
195    pub fn sign_tag(&self) -> bool {
196        self.sign_tag.unwrap_or(false)
197    }
198
199    pub fn push_remote(&self) -> &str {
200        self.push_remote.as_deref().unwrap_or("origin")
201    }
202
203    pub fn registry(&self) -> Option<&str> {
204        self.registry.as_deref()
205    }
206
207    pub fn release(&self) -> bool {
208        self.release.unwrap_or(true)
209    }
210
211    pub fn publish(&self) -> bool {
212        self.publish.unwrap_or(true)
213    }
214
215    pub fn verify(&self) -> bool {
216        self.verify.unwrap_or(true)
217    }
218
219    pub fn owners(&self) -> &[String] {
220        self.owners.as_ref().map(|v| v.as_ref()).unwrap_or(&[])
221    }
222
223    pub fn push(&self) -> bool {
224        self.push.unwrap_or(true)
225    }
226
227    pub fn push_options(&self) -> impl Iterator<Item = &str> {
228        self.push_options
229            .as_ref()
230            .into_iter()
231            .flat_map(|v| v.iter().map(|s| s.as_str()))
232    }
233
234    pub fn shared_version(&self) -> Option<&str> {
235        self.shared_version.as_ref().and_then(|s| s.as_name())
236    }
237
238    pub fn consolidate_commits(&self) -> bool {
239        self.consolidate_commits.unwrap_or(self.is_workspace)
240    }
241
242    pub fn pre_release_commit_message(&self) -> &str {
243        self.pre_release_commit_message
244            .as_deref()
245            .unwrap_or_else(|| {
246                if self.consolidate_commits() {
247                    "chore: Release"
248                } else {
249                    "chore: Release {{crate_name}} version {{version}}"
250                }
251            })
252    }
253
254    pub fn pre_release_replacements(&self) -> &[Replace] {
255        self.pre_release_replacements
256            .as_ref()
257            .map(|v| v.as_ref())
258            .unwrap_or(&[])
259    }
260
261    pub fn pre_release_hook(&self) -> Option<&Command> {
262        self.pre_release_hook.as_ref()
263    }
264
265    pub fn tag_message(&self) -> &str {
266        self.tag_message
267            .as_deref()
268            .unwrap_or("chore: Release {{crate_name}} version {{version}}")
269    }
270
271    pub fn tag_prefix(&self, is_root: bool) -> &str {
272        // crate_name as default tag prefix for multi-crate project
273        self.tag_prefix
274            .as_deref()
275            .unwrap_or(if !is_root { "{{crate_name}}-" } else { "" })
276    }
277
278    pub fn tag_name(&self) -> &str {
279        self.tag_name.as_deref().unwrap_or("{{prefix}}v{{version}}")
280    }
281
282    pub fn tag(&self) -> bool {
283        self.tag.unwrap_or(true)
284    }
285
286    pub fn enable_features(&self) -> &[String] {
287        self.enable_features
288            .as_ref()
289            .map(|v| v.as_ref())
290            .unwrap_or(&[])
291    }
292
293    pub fn enable_all_features(&self) -> bool {
294        self.enable_all_features.unwrap_or(false)
295    }
296
297    pub fn features(&self) -> cargo::Features {
298        if self.enable_all_features() {
299            cargo::Features::All
300        } else {
301            let features = self.enable_features();
302            cargo::Features::Selective(features.to_owned())
303        }
304    }
305
306    pub fn dependent_version(&self) -> DependentVersion {
307        self.dependent_version.unwrap_or_default()
308    }
309
310    pub fn metadata(&self) -> MetadataPolicy {
311        self.metadata.unwrap_or_default()
312    }
313
314    pub fn certs_source(&self) -> CertsSource {
315        self.certs_source.unwrap_or_default()
316    }
317}
318
319#[derive(Debug, Clone, Default, Serialize, Deserialize)]
320#[serde(deny_unknown_fields, default)]
321#[serde(rename_all = "kebab-case")]
322pub struct Unstable {
323    workspace_publish: Option<bool>,
324}
325
326impl Unstable {
327    pub fn new() -> Self {
328        Default::default()
329    }
330
331    pub fn from_defaults() -> Self {
332        let empty = Self::new();
333        Self {
334            workspace_publish: Some(empty.workspace_publish()),
335        }
336    }
337    pub fn update(&mut self, source: &Self) {
338        if let Some(workspace_publish) = source.workspace_publish {
339            self.workspace_publish = Some(workspace_publish);
340        }
341    }
342
343    pub fn workspace_publish(&self) -> bool {
344        self.workspace_publish.unwrap_or(false)
345    }
346}
347
348impl From<Vec<UnstableValues>> for Unstable {
349    fn from(values: Vec<UnstableValues>) -> Self {
350        let mut unstable = Unstable::new();
351        for value in values {
352            match value {
353                UnstableValues::WorkspacePublish(value) => unstable.workspace_publish = Some(value),
354            }
355        }
356        unstable
357    }
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361#[serde(deny_unknown_fields)]
362pub struct Replace {
363    pub file: PathBuf,
364    pub search: String,
365    pub replace: String,
366    pub min: Option<usize>,
367    pub max: Option<usize>,
368    pub exactly: Option<usize>,
369    #[serde(default)]
370    pub prerelease: bool,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
374#[serde(untagged)]
375pub enum Command {
376    Line(String),
377    Args(Vec<String>),
378}
379
380impl Command {
381    pub fn args(&self) -> Vec<&str> {
382        match self {
383            Command::Line(s) => vec![s.as_str()],
384            Command::Args(a) => a.iter().map(|s| s.as_str()).collect(),
385        }
386    }
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
390#[serde(rename_all = "kebab-case")]
391#[value(rename_all = "kebab-case")]
392#[derive(Default)]
393pub enum DependentVersion {
394    /// Always upgrade dependents
395    #[default] // This is the safest option as its hard to test `Fix`
396    Upgrade,
397    /// Upgrade when the old version requirement no longer applies
398    Fix,
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
402#[serde(rename_all = "kebab-case")]
403#[value(rename_all = "kebab-case")]
404#[derive(Default)]
405pub enum CertsSource {
406    /// Use certs from Mozilla's root certificate store.
407    #[default]
408    Webpki,
409    /// Use certs from the system root certificate store.
410    Native,
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
414#[serde(rename_all = "kebab-case")]
415#[value(rename_all = "kebab-case")]
416#[derive(Default)]
417pub enum MetadataPolicy {
418    /// Apply when set, clear when not
419    #[default]
420    Optional,
421    /// Error if not set
422    Required,
423    /// Never apply the set metadata
424    Ignore,
425    /// Keep the prior metadata if not set
426    Persistent,
427}
428
429#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
430#[serde(untagged)]
431#[serde(rename_all = "kebab-case")]
432pub enum SharedVersion {
433    Enabled(bool),
434    Name(String),
435}
436
437impl SharedVersion {
438    pub const WORKSPACE: &'static str = "workspace";
439
440    pub fn as_name(&self) -> Option<&str> {
441        match self {
442            SharedVersion::Enabled(true) => Some("default"),
443            SharedVersion::Enabled(false) => None,
444            SharedVersion::Name(name) => Some(name.as_str()),
445        }
446    }
447}
448
449#[derive(Debug, Clone, Default, Serialize, Deserialize)]
450#[serde(default)]
451struct CargoManifest {
452    workspace: Option<CargoWorkspace>,
453    package: Option<CargoPackage>,
454}
455
456#[derive(Debug, Clone, Default, Serialize, Deserialize)]
457#[serde(default)]
458struct CargoWorkspace {
459    package: Option<CargoWorkspacePackage>,
460    metadata: Option<CargoMetadata>,
461}
462
463impl CargoWorkspace {
464    fn into_config(self) -> Option<Config> {
465        self.metadata?.release
466    }
467}
468
469#[derive(Debug, Clone, Default, Serialize, Deserialize)]
470#[serde(default)]
471struct CargoWorkspacePackage {
472    publish: Option<CargoPublishField>,
473}
474
475#[derive(Debug, Clone, Default, Serialize, Deserialize)]
476#[serde(default)]
477struct CargoPackage {
478    publish: Option<MaybeWorkspace<CargoPublishField>>,
479    version: Option<MaybeWorkspace<String>>,
480    metadata: Option<CargoMetadata>,
481}
482
483impl CargoPackage {
484    fn into_config(self) -> Option<Config> {
485        self.metadata?.release
486    }
487}
488
489#[derive(Clone, Debug, Serialize, Deserialize)]
490#[serde(untagged)]
491enum CargoPublishField {
492    Bool(bool),
493    Registries(Vec<String>),
494}
495
496impl CargoPublishField {
497    fn publishable(&self) -> bool {
498        match self {
499            Self::Bool(b) => *b,
500            Self::Registries(r) => !r.is_empty(),
501        }
502    }
503}
504
505#[derive(Clone, Debug, Serialize, Deserialize)]
506#[serde(untagged)]
507pub enum MaybeWorkspace<T> {
508    Workspace(TomlWorkspaceField),
509    Defined(T),
510}
511
512#[derive(Clone, Debug, Serialize, Deserialize)]
513pub struct TomlWorkspaceField {
514    workspace: bool,
515}
516
517#[derive(Debug, Clone, Default, Serialize, Deserialize)]
518#[serde(default)]
519struct CargoMetadata {
520    release: Option<Config>,
521}
522
523#[derive(Debug, Default, Clone, Serialize, Deserialize)]
524#[serde(rename_all = "kebab-case")]
525pub struct RateLimit {
526    #[serde(default)]
527    pub new_packages: Option<usize>,
528    #[serde(default)]
529    pub existing_packages: Option<usize>,
530}
531
532impl RateLimit {
533    pub fn new() -> Self {
534        Default::default()
535    }
536
537    pub fn from_defaults() -> Self {
538        Self {
539            new_packages: Some(5),
540            existing_packages: Some(30),
541        }
542    }
543
544    pub fn update(&mut self, source: &RateLimit) {
545        if source.new_packages.is_some() {
546            self.new_packages = source.new_packages;
547        }
548        if source.existing_packages.is_some() {
549            self.existing_packages = source.existing_packages;
550        }
551    }
552
553    pub fn new_packages(&self) -> usize {
554        self.new_packages.unwrap_or(5)
555    }
556
557    pub fn existing_packages(&self) -> usize {
558        self.existing_packages.unwrap_or(30)
559    }
560}
561
562pub fn load_workspace_config(
563    args: &ConfigArgs,
564    ws_meta: &cargo_metadata::Metadata,
565) -> CargoResult<Config> {
566    let mut release_config = Config {
567        is_workspace: 1 < ws_meta.workspace_members.len(),
568        ..Default::default()
569    };
570
571    if !args.isolated {
572        let is_workspace = 1 < ws_meta.workspace_members.len();
573        let cfg = if is_workspace {
574            resolve_workspace_config(ws_meta.workspace_root.as_std_path())?
575        } else {
576            // Outside of workspaces, go ahead and treat package config as workspace config so
577            // users don't have to specially configure workspace-specific fields
578            let pkg = ws_meta
579                .packages
580                .iter()
581                .find(|p| ws_meta.workspace_members.contains(&p.id))
582                .unwrap();
583            resolve_config(
584                ws_meta.workspace_root.as_std_path(),
585                pkg.manifest_path.as_std_path(),
586            )?
587        };
588        release_config.update(&cfg);
589    }
590
591    if let Some(custom_config_path) = args.custom_config.as_ref() {
592        // when calling with -c option
593        let cfg = resolve_custom_config(custom_config_path.as_ref())?.unwrap_or_default();
594        release_config.update(&cfg);
595    }
596
597    release_config.update(&args.to_config());
598    Ok(release_config)
599}
600
601pub fn load_package_config(
602    args: &ConfigArgs,
603    ws_meta: &cargo_metadata::Metadata,
604    pkg: &cargo_metadata::Package,
605) -> CargoResult<Config> {
606    let manifest_path = pkg.manifest_path.as_std_path();
607
608    let is_workspace = 1 < ws_meta.workspace_members.len();
609    let mut release_config = Config {
610        is_workspace,
611        ..Default::default()
612    };
613
614    if !args.isolated {
615        let cfg = resolve_config(ws_meta.workspace_root.as_std_path(), manifest_path)?;
616        release_config.update(&cfg);
617    }
618
619    if let Some(custom_config_path) = args.custom_config.as_ref() {
620        // when calling with -c option
621        let cfg = resolve_custom_config(Path::new(custom_config_path))?.unwrap_or_default();
622        release_config.update(&cfg);
623    }
624
625    release_config.update(&args.to_config());
626
627    let overrides = resolve_overrides(ws_meta.workspace_root.as_std_path(), manifest_path)?;
628    release_config.update(&overrides);
629
630    Ok(release_config)
631}
632
633#[derive(Clone, Default, Debug, clap::Args)]
634pub struct ConfigArgs {
635    /// Custom config file
636    #[arg(short, long = "config", value_name = "PATH")]
637    pub custom_config: Option<PathBuf>,
638
639    /// Ignore implicit configuration files.
640    #[arg(long)]
641    pub isolated: bool,
642
643    /// Unstable options
644    #[arg(short = 'Z', value_name = "FEATURE")]
645    pub z: Vec<UnstableValues>,
646
647    /// Sign both git commit and tag
648    #[arg(long, overrides_with("no_sign"))]
649    pub sign: bool,
650    #[arg(long, overrides_with("sign"), hide(true))]
651    pub no_sign: bool,
652
653    /// Specify how workspace dependencies on this crate should be handed.
654    #[arg(long, value_name = "ACTION", value_enum)]
655    pub dependent_version: Option<DependentVersion>,
656
657    /// Comma-separated globs of branch names a release can happen from
658    #[arg(long, value_delimiter = ',', value_name = "GLOB[,...]")]
659    pub allow_branch: Option<Vec<String>>,
660
661    /// Indicate what certificate store to use for web requests.
662    #[arg(long)]
663    pub certs_source: Option<CertsSource>,
664
665    #[command(flatten)]
666    pub commit: CommitArgs,
667
668    #[command(flatten)]
669    pub publish: PublishArgs,
670
671    #[command(flatten)]
672    pub tag: TagArgs,
673
674    #[command(flatten)]
675    pub push: PushArgs,
676}
677
678impl ConfigArgs {
679    pub fn to_config(&self) -> Config {
680        let mut config = Config {
681            unstable: Unstable::from(self.z.clone()),
682            allow_branch: self.allow_branch.clone(),
683            sign_commit: self.sign(),
684            sign_tag: self.sign(),
685            dependent_version: self.dependent_version,
686            certs_source: self.certs_source,
687            ..Default::default()
688        };
689        config.update(&self.commit.to_config());
690        config.update(&self.publish.to_config());
691        config.update(&self.tag.to_config());
692        config.update(&self.push.to_config());
693        config
694    }
695
696    fn sign(&self) -> Option<bool> {
697        resolve_bool_arg(self.sign, self.no_sign)
698    }
699}
700
701#[derive(Clone, Debug)]
702pub enum UnstableValues {
703    WorkspacePublish(bool),
704}
705
706impl std::str::FromStr for UnstableValues {
707    type Err = anyhow::Error;
708
709    fn from_str(value: &str) -> Result<Self, Self::Err> {
710        let (name, mut value) = value.split_once('=').unwrap_or((value, ""));
711        match name {
712            "workspace-publish" => {
713                if value.is_empty() {
714                    value = "true";
715                }
716                let value = match value {
717                    "true" => true,
718                    "false" => false,
719                    _ => anyhow::bail!(
720                        "unsupported value `{name}={value}`, expected one of `true`, `false`"
721                    ),
722                };
723                Ok(UnstableValues::WorkspacePublish(value))
724            }
725            _ => {
726                anyhow::bail!("unsupported unstable feature name `{name}` (value `{value}`)");
727            }
728        }
729    }
730}
731
732impl std::fmt::Display for UnstableValues {
733    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
734        match self {
735            Self::WorkspacePublish(true) => "workspace-publish".fmt(fmt),
736            Self::WorkspacePublish(false) => "".fmt(fmt),
737        }
738    }
739}
740
741#[derive(Clone, Default, Debug, clap::Args)]
742#[command(next_help_heading = "Commit")]
743pub struct CommitArgs {
744    /// Sign git commit
745    #[arg(long, overrides_with("no_sign_commit"))]
746    pub sign_commit: bool,
747    #[arg(long, overrides_with("sign_commit"), hide(true))]
748    pub no_sign_commit: bool,
749}
750
751impl CommitArgs {
752    pub fn to_config(&self) -> Config {
753        Config {
754            sign_commit: resolve_bool_arg(self.sign_commit, self.no_sign_commit),
755            ..Default::default()
756        }
757    }
758}
759
760#[derive(Clone, Default, Debug, clap::Args)]
761#[command(next_help_heading = "Publish")]
762pub struct PublishArgs {
763    #[arg(long, overrides_with("no_publish"), hide(true))]
764    publish: bool,
765    /// Do not run cargo publish on release
766    #[arg(long, overrides_with("publish"))]
767    no_publish: bool,
768
769    /// Cargo registry to upload to
770    #[arg(long, value_name = "NAME")]
771    registry: Option<String>,
772
773    #[arg(long, overrides_with("no_verify"), hide(true))]
774    verify: bool,
775    /// Don't verify the contents by building them
776    #[arg(long, overrides_with("verify"))]
777    no_verify: bool,
778
779    /// Provide a set of features that need to be enabled
780    #[arg(long)]
781    features: Vec<String>,
782
783    /// Enable all features via `all-features`. Overrides `features`
784    #[arg(long)]
785    all_features: bool,
786
787    /// Build for the target triple
788    #[arg(long, value_name = "TRIPLE")]
789    target: Option<String>,
790}
791
792impl PublishArgs {
793    pub fn to_config(&self) -> Config {
794        Config {
795            publish: resolve_bool_arg(self.publish, self.no_publish),
796            registry: self.registry.clone(),
797            verify: resolve_bool_arg(self.verify, self.no_verify),
798            enable_features: (!self.features.is_empty()).then(|| self.features.clone()),
799            enable_all_features: self.all_features.then_some(true),
800            target: self.target.clone(),
801            ..Default::default()
802        }
803    }
804}
805
806#[derive(Clone, Default, Debug, clap::Args)]
807#[command(next_help_heading = "Tag")]
808pub struct TagArgs {
809    #[arg(long, overrides_with("no_tag"), hide(true))]
810    tag: bool,
811    /// Do not create git tag
812    #[arg(long, overrides_with("tag"))]
813    no_tag: bool,
814
815    /// Sign git tag
816    #[arg(long, overrides_with("no_sign_tag"))]
817    sign_tag: bool,
818    #[arg(long, overrides_with("sign_tag"), hide(true))]
819    no_sign_tag: bool,
820
821    /// Prefix of git tag, note that this will override default prefix based on sub-directory
822    #[arg(long, value_name = "PREFIX")]
823    tag_prefix: Option<String>,
824
825    /// The name of the git tag.
826    #[arg(long, value_name = "NAME")]
827    tag_name: Option<String>,
828}
829
830impl TagArgs {
831    pub fn to_config(&self) -> Config {
832        Config {
833            tag: resolve_bool_arg(self.tag, self.no_tag),
834            sign_tag: resolve_bool_arg(self.sign_tag, self.no_sign_tag),
835            tag_prefix: self.tag_prefix.clone(),
836            tag_name: self.tag_name.clone(),
837            ..Default::default()
838        }
839    }
840}
841
842#[derive(Clone, Default, Debug, clap::Args)]
843#[command(next_help_heading = "Push")]
844pub struct PushArgs {
845    #[arg(long, overrides_with("no_push"), hide(true))]
846    push: bool,
847    /// Do not run git push in the last step
848    #[arg(long, overrides_with("push"))]
849    no_push: bool,
850
851    /// Git remote to push
852    #[arg(long, value_name = "NAME")]
853    push_remote: Option<String>,
854}
855
856impl PushArgs {
857    pub fn to_config(&self) -> Config {
858        Config {
859            push: resolve_bool_arg(self.push, self.no_push),
860            push_remote: self.push_remote.clone(),
861            ..Default::default()
862        }
863    }
864}
865
866fn get_pkg_config_from_manifest(manifest_path: &Path) -> CargoResult<Option<Config>> {
867    if manifest_path.exists() {
868        let m = std::fs::read_to_string(manifest_path)?;
869        let c: CargoManifest = toml::from_str(&m)
870            .with_context(|| format!("Failed to parse `{}`", manifest_path.display()))?;
871
872        Ok(c.package.and_then(|p| p.into_config()))
873    } else {
874        Ok(None)
875    }
876}
877
878fn get_ws_config_from_manifest(manifest_path: &Path) -> CargoResult<Option<Config>> {
879    if manifest_path.exists() {
880        let m = std::fs::read_to_string(manifest_path)?;
881        let c: CargoManifest = toml::from_str(&m)
882            .with_context(|| format!("Failed to parse `{}`", manifest_path.display()))?;
883
884        Ok(c.workspace.and_then(|p| p.into_config()))
885    } else {
886        Ok(None)
887    }
888}
889
890fn get_config_from_file(file_path: &Path) -> CargoResult<Option<Config>> {
891    if file_path.exists() {
892        let c = std::fs::read_to_string(file_path)?;
893        let config = toml::from_str(&c)
894            .with_context(|| format!("Failed to parse `{}`", file_path.display()))?;
895        Ok(Some(config))
896    } else {
897        Ok(None)
898    }
899}
900
901pub fn resolve_custom_config(file_path: &Path) -> CargoResult<Option<Config>> {
902    get_config_from_file(file_path)
903}
904
905/// Try to resolve workspace configuration source.
906///
907/// This tries the following sources in order, merging the results:
908/// 1. $HOME/.release.toml
909/// 2. $HOME/.config/cargo-release/release.toml
910/// 3. $(workspace)/release.toml
911/// 3. $(workspace)/Cargo.toml
912pub fn resolve_workspace_config(workspace_root: &Path) -> CargoResult<Config> {
913    let mut config = Config::default();
914
915    // User-local configuration from home directory.
916    let home_dir = dirs_next::home_dir();
917    if let Some(mut home) = home_dir {
918        home.push(".release.toml");
919        if let Some(cfg) = get_config_from_file(&home)? {
920            config.update(&cfg);
921        }
922    };
923
924    let config_dir = dirs_next::config_dir();
925    if let Some(mut config_path) = config_dir {
926        config_path.push("cargo-release/release.toml");
927        if let Some(cfg) = get_config_from_file(&config_path)? {
928            config.update(&cfg);
929        }
930    };
931
932    // Workspace config
933    let default_config = workspace_root.join("release.toml");
934    let current_dir_config = get_config_from_file(&default_config)?;
935    if let Some(cfg) = current_dir_config {
936        config.update(&cfg);
937    };
938
939    let manifest_path = workspace_root.join("Cargo.toml");
940    let current_dir_config = get_ws_config_from_manifest(&manifest_path)?;
941    if let Some(cfg) = current_dir_config {
942        config.update(&cfg);
943    };
944
945    Ok(config)
946}
947
948/// Try to resolve configuration source.
949///
950/// This tries the following sources in order, merging the results:
951/// 1. $HOME/.release.toml
952/// 2. $HOME/.config/cargo-release/release.toml
953/// 3. $(workspace)/release.toml
954/// 3. $(workspace)/Cargo.toml `workspace.metadata.release`
955/// 4. $(crate)/release.toml
956/// 5. $(crate)/Cargo.toml `package.metadata.release`
957///
958/// `$(crate)/Cargo.toml` is a way to differentiate configuration for the root crate and the
959/// workspace.
960pub fn resolve_config(workspace_root: &Path, manifest_path: &Path) -> CargoResult<Config> {
961    let mut config = resolve_workspace_config(workspace_root)?;
962
963    // Crate config
964    let crate_root = manifest_path.parent().unwrap_or_else(|| Path::new("."));
965    let default_config = crate_root.join("release.toml");
966    let current_dir_config = get_config_from_file(&default_config)?;
967    if let Some(cfg) = current_dir_config {
968        config.update(&cfg);
969    };
970
971    let current_dir_config = get_pkg_config_from_manifest(manifest_path)?;
972    if let Some(cfg) = current_dir_config {
973        config.update(&cfg);
974    };
975
976    Ok(config)
977}
978
979pub fn resolve_overrides(workspace_root: &Path, manifest_path: &Path) -> CargoResult<Config> {
980    fn load_workspace<'m, 'c: 'm>(
981        workspace_root: &Path,
982        workspace_cache: &'c mut Option<CargoManifest>,
983    ) -> CargoResult<&'m CargoManifest> {
984        if workspace_cache.is_none() {
985            let workspace_path = workspace_root.join("Cargo.toml");
986            let toml = std::fs::read_to_string(&workspace_path)?;
987            let manifest: CargoManifest = toml::from_str(&toml)
988                .with_context(|| format!("Failed to parse `{}`", workspace_path.display()))?;
989
990            *workspace_cache = Some(manifest);
991        }
992        Ok(workspace_cache.as_ref().unwrap())
993    }
994
995    let mut release_config = Config::default();
996
997    let mut workspace_cache = None;
998    // the publish flag in cargo file
999    let manifest = std::fs::read_to_string(manifest_path)?;
1000    let manifest: CargoManifest = toml::from_str(&manifest)
1001        .with_context(|| format!("Failed to parse `{}`", manifest_path.display()))?;
1002    if let Some(package) = manifest.package.as_ref() {
1003        let publish = match package.publish.as_ref() {
1004            Some(MaybeWorkspace::Defined(publish)) => publish.publishable(),
1005            Some(MaybeWorkspace::Workspace(workspace)) => {
1006                if workspace.workspace {
1007                    let workspace = load_workspace(workspace_root, &mut workspace_cache)?;
1008                    workspace
1009                        .workspace
1010                        .as_ref()
1011                        .and_then(|w| w.package.as_ref())
1012                        .and_then(|p| p.publish.as_ref())
1013                        .map(|p| p.publishable())
1014                        .unwrap_or(true)
1015                } else {
1016                    true
1017                }
1018            }
1019            None => true,
1020        };
1021        if !publish {
1022            release_config.publish = Some(false);
1023        }
1024
1025        if package.version.is_none() {
1026            // No point releasing if it can't be published and doesn't have a version to update
1027            release_config.release = Some(false);
1028        }
1029        if package
1030            .version
1031            .as_ref()
1032            .and_then(|v| match v {
1033                MaybeWorkspace::Defined(_) => None,
1034                MaybeWorkspace::Workspace(workspace) => Some(workspace.workspace),
1035            })
1036            .unwrap_or(false)
1037        {
1038            release_config.shared_version =
1039                Some(SharedVersion::Name(SharedVersion::WORKSPACE.to_owned()));
1040            // We can't isolate commits because by changing the version in one crate, we change it in all
1041            release_config.consolidate_commits = Some(true);
1042        }
1043    }
1044
1045    Ok(release_config)
1046}
1047
1048fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
1049    match (yes, no) {
1050        (true, false) => Some(true),
1051        (false, true) => Some(false),
1052        (false, false) => None,
1053        (_, _) => unreachable!("clap should make this impossible"),
1054    }
1055}
1056
1057#[cfg(test)]
1058mod test {
1059    use super::*;
1060
1061    mod resolve_config {
1062        use super::*;
1063
1064        #[test]
1065        fn doesnt_panic() {
1066            let release_config = resolve_config(Path::new("."), Path::new("Cargo.toml")).unwrap();
1067            assert!(!release_config.sign_commit());
1068        }
1069    }
1070}