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, 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 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 #[default] Upgrade,
397 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 #[default]
408 Webpki,
409 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 #[default]
420 Optional,
421 Required,
423 Ignore,
425 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 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 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 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 #[arg(short, long = "config", value_name = "PATH")]
637 pub custom_config: Option<PathBuf>,
638
639 #[arg(long)]
641 pub isolated: bool,
642
643 #[arg(short = 'Z', value_name = "FEATURE")]
645 pub z: Vec<UnstableValues>,
646
647 #[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 #[arg(long, value_name = "ACTION", value_enum)]
655 pub dependent_version: Option<DependentVersion>,
656
657 #[arg(long, value_delimiter = ',', value_name = "GLOB[,...]")]
659 pub allow_branch: Option<Vec<String>>,
660
661 #[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 #[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 #[arg(long, overrides_with("publish"))]
767 no_publish: bool,
768
769 #[arg(long, value_name = "NAME")]
771 registry: Option<String>,
772
773 #[arg(long, overrides_with("no_verify"), hide(true))]
774 verify: bool,
775 #[arg(long, overrides_with("verify"))]
777 no_verify: bool,
778
779 #[arg(long)]
781 features: Vec<String>,
782
783 #[arg(long)]
785 all_features: bool,
786
787 #[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 #[arg(long, overrides_with("tag"))]
813 no_tag: bool,
814
815 #[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 #[arg(long, value_name = "PREFIX")]
823 tag_prefix: Option<String>,
824
825 #[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 #[arg(long, overrides_with("push"))]
849 no_push: bool,
850
851 #[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
905pub fn resolve_workspace_config(workspace_root: &Path) -> CargoResult<Config> {
913 let mut config = Config::default();
914
915 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 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
948pub fn resolve_config(workspace_root: &Path, manifest_path: &Path) -> CargoResult<Config> {
961 let mut config = resolve_workspace_config(workspace_root)?;
962
963 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 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 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 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}