1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use changeset_changelog::{ChangelogConfig, ChangelogLocation, ComparisonLinksSetting};
5use changeset_core::ZeroVersionBehavior;
6use changeset_git::DEFAULT_BASE_BRANCH;
7use globset::{Glob, GlobSet, GlobSetBuilder};
8
9use crate::error::ProjectError;
10use crate::manifest::{ChangesetMetadata, TagFormatValue, read_manifest};
11use crate::project::{CargoProject, ProjectKind};
12
13const DEFAULT_DEPENDENCY_BUMP_CHANGELOG_TEMPLATE: &str =
14 "Updated dependency `{dependency}` to v{version}";
15const DEFAULT_NONE_BUMP_PROMOTE_MESSAGE_TEMPLATE: &str = "Internal architectural changes";
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum TagFormat {
19 #[default]
20 VersionOnly,
21 CratePrefixed,
22}
23
24#[derive(Debug, Clone)]
25#[allow(clippy::struct_excessive_bools)]
26pub struct GitConfig {
27 commit: bool,
28 tags: bool,
29 keep_changesets: bool,
30 tag_format: TagFormat,
31 commit_title_template: String,
32 changes_in_body: bool,
33}
34
35impl GitConfig {
36 #[must_use]
37 pub fn commit(&self) -> bool {
38 self.commit
39 }
40
41 #[must_use]
42 pub fn tags(&self) -> bool {
43 self.tags
44 }
45
46 #[must_use]
47 pub fn keep_changesets(&self) -> bool {
48 self.keep_changesets
49 }
50
51 #[must_use]
52 pub fn tag_format(&self) -> TagFormat {
53 self.tag_format
54 }
55
56 #[must_use]
57 pub fn commit_title_template(&self) -> &str {
58 &self.commit_title_template
59 }
60
61 #[must_use]
62 pub fn changes_in_body(&self) -> bool {
63 self.changes_in_body
64 }
65
66 #[cfg(any(test, feature = "testing"))]
67 #[must_use]
68 pub fn with_changes_in_body(mut self, changes_in_body: bool) -> Self {
69 self.changes_in_body = changes_in_body;
70 self
71 }
72}
73
74impl Default for GitConfig {
75 fn default() -> Self {
76 Self {
77 commit: true,
78 tags: true,
79 keep_changesets: false,
80 tag_format: TagFormat::default(),
81 commit_title_template: String::from("{new-version}"),
82 changes_in_body: true,
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
88pub struct RootChangesetConfig {
89 ignored_files: GlobSet,
90 changeset_dir: PathBuf,
91 changelog_config: ChangelogConfig,
92 git_config: GitConfig,
93 zero_version_behavior: ZeroVersionBehavior,
94 dependency_bump_changelog_template: String,
95 base_branch: String,
96 none_bump_behavior: changeset_core::NoneBumpBehavior,
97 none_bump_promote_message_template: String,
98}
99
100impl RootChangesetConfig {
101 #[must_use]
102 pub fn ignored_files(&self) -> &GlobSet {
103 &self.ignored_files
104 }
105
106 #[must_use]
107 pub fn is_ignored(&self, path: &Path) -> bool {
108 self.ignored_files.is_match(path)
109 }
110
111 #[must_use]
112 pub fn changeset_dir(&self) -> &Path {
113 &self.changeset_dir
114 }
115
116 #[must_use]
117 pub fn changelog_config(&self) -> &ChangelogConfig {
118 &self.changelog_config
119 }
120
121 #[must_use]
122 pub fn git_config(&self) -> &GitConfig {
123 &self.git_config
124 }
125
126 #[must_use]
127 pub fn zero_version_behavior(&self) -> ZeroVersionBehavior {
128 self.zero_version_behavior
129 }
130
131 #[must_use]
132 pub fn dependency_bump_changelog_template(&self) -> &str {
133 &self.dependency_bump_changelog_template
134 }
135
136 #[must_use]
137 pub fn base_branch(&self) -> &str {
138 &self.base_branch
139 }
140
141 #[must_use]
142 pub fn none_bump_behavior(&self) -> changeset_core::NoneBumpBehavior {
143 self.none_bump_behavior
144 }
145
146 #[must_use]
147 pub fn none_bump_promote_message_template(&self) -> &str {
148 &self.none_bump_promote_message_template
149 }
150
151 #[cfg(any(test, feature = "testing"))]
152 #[must_use]
153 pub fn with_git_config(mut self, git_config: GitConfig) -> Self {
154 self.git_config = git_config;
155 self
156 }
157
158 #[cfg(any(test, feature = "testing"))]
159 #[must_use]
160 pub fn with_none_bump_behavior(mut self, behavior: changeset_core::NoneBumpBehavior) -> Self {
161 self.none_bump_behavior = behavior;
162 self
163 }
164}
165
166impl Default for RootChangesetConfig {
167 fn default() -> Self {
168 Self {
169 ignored_files: GlobSet::empty(),
170 changeset_dir: PathBuf::from(crate::DEFAULT_CHANGESET_DIR),
171 changelog_config: ChangelogConfig::default(),
172 git_config: GitConfig::default(),
173 zero_version_behavior: ZeroVersionBehavior::default(),
174 dependency_bump_changelog_template: String::from(
175 DEFAULT_DEPENDENCY_BUMP_CHANGELOG_TEMPLATE,
176 ),
177 base_branch: String::from(DEFAULT_BASE_BRANCH),
178 none_bump_behavior: changeset_core::NoneBumpBehavior::default(),
179 none_bump_promote_message_template: String::from(
180 DEFAULT_NONE_BUMP_PROMOTE_MESSAGE_TEMPLATE,
181 ),
182 }
183 }
184}
185
186#[derive(Debug, Default)]
187pub struct PackageChangesetConfig {
188 ignored_files: GlobSet,
189}
190
191impl PackageChangesetConfig {
192 #[must_use]
193 pub fn ignored_files(&self) -> &GlobSet {
194 &self.ignored_files
195 }
196
197 #[must_use]
198 pub fn is_ignored(&self, path: &Path) -> bool {
199 self.ignored_files.is_match(path)
200 }
201}
202
203enum CargoRootConfigType {
204 Workspace,
205 Package,
206}
207
208pub fn parse_root_config(project: &CargoProject) -> Result<RootChangesetConfig, ProjectError> {
217 match project.kind() {
218 ProjectKind::SinglePackage => {
219 parse_cargo_root_config(project.root(), CargoRootConfigType::Package)
220 }
221 ProjectKind::VirtualWorkspace | ProjectKind::WorkspaceWithRoot => {
222 parse_cargo_root_config(project.root(), CargoRootConfigType::Workspace)
223 }
224 }
225}
226
227pub fn parse_package_config(package_path: &Path) -> Result<PackageChangesetConfig, ProjectError> {
231 let manifest_path = package_path.join("Cargo.toml");
232 let manifest = read_manifest(&manifest_path)?;
233
234 let patterns = manifest
235 .package
236 .and_then(|pkg| pkg.metadata)
237 .and_then(|meta| meta.changeset)
238 .map(|cs| cs.ignored_files)
239 .unwrap_or_default();
240
241 let ignored_files = build_glob_set(&patterns)?;
242
243 Ok(PackageChangesetConfig { ignored_files })
244}
245
246pub fn load_changeset_configs(
250 project: &CargoProject,
251) -> Result<(RootChangesetConfig, HashMap<String, PackageChangesetConfig>), ProjectError> {
252 let root_config = parse_root_config(project)?;
253
254 let mut package_configs = HashMap::new();
255 for package in project.packages() {
256 let config = parse_package_config(package.path())?;
257 package_configs.insert(package.name().clone(), config);
258 }
259
260 Ok((root_config, package_configs))
261}
262
263fn build_glob_set(patterns: &[String]) -> Result<GlobSet, ProjectError> {
264 let mut builder = GlobSetBuilder::new();
265 for pattern in patterns {
266 let glob = Glob::new(pattern).map_err(|source| ProjectError::GlobPattern {
267 pattern: pattern.clone(),
268 source,
269 })?;
270 builder.add(glob);
271 }
272 builder.build().map_err(|source| ProjectError::GlobPattern {
273 pattern: patterns.join(", "),
274 source,
275 })
276}
277
278fn build_changelog_config(
279 changelog: Option<ChangelogLocation>,
280 comparison_links: Option<ComparisonLinksSetting>,
281 comparison_links_template: Option<String>,
282) -> ChangelogConfig {
283 ChangelogConfig::new(
284 changelog.unwrap_or_default(),
285 comparison_links.unwrap_or_default(),
286 comparison_links_template,
287 )
288}
289
290fn build_git_config(metadata: Option<&ChangesetMetadata>) -> GitConfig {
291 let defaults = GitConfig::default();
292 match metadata {
293 None => defaults,
294 Some(cs) => GitConfig {
295 commit: cs.commit.unwrap_or(defaults.commit),
296 tags: cs.tags.unwrap_or(defaults.tags),
297 keep_changesets: cs.keep_changesets.unwrap_or(defaults.keep_changesets),
298 tag_format: cs.tag_format.map_or(defaults.tag_format, |tf| match tf {
299 TagFormatValue::VersionOnly => TagFormat::VersionOnly,
300 TagFormatValue::CratePrefixed => TagFormat::CratePrefixed,
301 }),
302 commit_title_template: cs
303 .commit_title_template
304 .clone()
305 .unwrap_or(defaults.commit_title_template),
306 changes_in_body: cs.changes_in_body.unwrap_or(defaults.changes_in_body),
307 },
308 }
309}
310
311fn parse_cargo_root_config(
312 project_root: &Path,
313 config_type: CargoRootConfigType,
314) -> Result<RootChangesetConfig, ProjectError> {
315 let manifest_path = project_root.join("Cargo.toml");
316 let manifest = read_manifest(&manifest_path)?;
317
318 let changeset_metadata = match config_type {
319 CargoRootConfigType::Workspace => manifest
320 .workspace
321 .and_then(|ws| ws.metadata)
322 .and_then(|meta| meta.changeset),
323 CargoRootConfigType::Package => manifest
324 .package
325 .and_then(|pkg| pkg.metadata)
326 .and_then(|meta| meta.changeset),
327 };
328
329 let patterns = changeset_metadata
330 .as_ref()
331 .map(|cs| cs.ignored_files.clone())
332 .unwrap_or_default();
333
334 let changeset_dir = changeset_metadata
335 .as_ref()
336 .and_then(|cs| cs.changeset_dir.clone())
337 .unwrap_or_else(|| crate::DEFAULT_CHANGESET_DIR.to_string());
338
339 let ignored_files = build_glob_set(&patterns)?;
340
341 let changelog_config = build_changelog_config(
342 changeset_metadata.as_ref().and_then(|cs| cs.changelog),
343 changeset_metadata
344 .as_ref()
345 .and_then(|cs| cs.comparison_links),
346 changeset_metadata
347 .as_ref()
348 .and_then(|cs| cs.comparison_links_template.clone()),
349 );
350
351 let git_config = build_git_config(changeset_metadata.as_ref());
352
353 let zero_version_behavior = changeset_metadata
354 .as_ref()
355 .and_then(|cs| cs.zero_version_behavior)
356 .unwrap_or_default();
357
358 let dependency_bump_changelog_template = changeset_metadata
359 .as_ref()
360 .and_then(|cs| cs.dependency_bump_changelog_template.clone())
361 .unwrap_or_else(|| String::from(DEFAULT_DEPENDENCY_BUMP_CHANGELOG_TEMPLATE));
362
363 let base_branch = changeset_metadata
364 .as_ref()
365 .and_then(|cs| cs.base_branch.clone())
366 .unwrap_or_else(|| String::from(DEFAULT_BASE_BRANCH));
367
368 let none_bump_behavior = changeset_metadata
369 .as_ref()
370 .and_then(|cs| cs.none_bump_behavior)
371 .unwrap_or_default();
372
373 let none_bump_promote_message_template = changeset_metadata
374 .as_ref()
375 .and_then(|cs| cs.none_bump_promote_message_template.clone())
376 .unwrap_or_else(|| String::from(DEFAULT_NONE_BUMP_PROMOTE_MESSAGE_TEMPLATE));
377
378 Ok(RootChangesetConfig {
379 ignored_files,
380 changeset_dir: PathBuf::from(changeset_dir),
381 changelog_config,
382 git_config,
383 zero_version_behavior,
384 dependency_bump_changelog_template,
385 base_branch,
386 none_bump_behavior,
387 none_bump_promote_message_template,
388 })
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use std::fs;
395 use tempfile::TempDir;
396
397 fn setup_with_config(toml_content: &str) -> anyhow::Result<TempDir> {
398 let dir = TempDir::new()?;
399 fs::write(dir.path().join("Cargo.toml"), toml_content)?;
400 Ok(dir)
401 }
402
403 #[test]
404 fn parse_workspace_root_config_with_ignored_files() -> anyhow::Result<()> {
405 let toml = r#"
406[workspace]
407members = ["crates/*"]
408
409[workspace.metadata.changeset]
410ignored-files = ["*.md", "docs/**"]
411"#;
412 let dir = setup_with_config(toml)?;
413
414 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
415
416 assert!(config.is_ignored(Path::new("README.md")));
417 assert!(config.is_ignored(Path::new("docs/guide.md")));
418 assert!(!config.is_ignored(Path::new("src/lib.rs")));
419
420 Ok(())
421 }
422
423 #[test]
424 fn parse_workspace_root_config_without_metadata() -> anyhow::Result<()> {
425 let toml = r#"
426[workspace]
427members = ["crates/*"]
428"#;
429 let dir = setup_with_config(toml)?;
430
431 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
432
433 assert!(!config.is_ignored(Path::new("README.md")));
434 assert!(!config.is_ignored(Path::new("src/lib.rs")));
435
436 Ok(())
437 }
438
439 #[test]
440 fn parse_workspace_root_config_with_custom_changeset_dir() -> anyhow::Result<()> {
441 let toml = r#"
442[workspace]
443members = ["crates/*"]
444
445[workspace.metadata.changeset]
446changeset-dir = "changes"
447"#;
448 let dir = setup_with_config(toml)?;
449
450 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
451
452 assert_eq!(config.changeset_dir(), Path::new("changes"));
453
454 Ok(())
455 }
456
457 #[test]
458 fn parse_workspace_root_config_default_changeset_dir() -> anyhow::Result<()> {
459 let toml = r#"
460[workspace]
461members = ["crates/*"]
462"#;
463 let dir = setup_with_config(toml)?;
464
465 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
466
467 assert_eq!(config.changeset_dir(), Path::new(".changeset"));
468
469 Ok(())
470 }
471
472 #[test]
473 fn parse_package_config_with_ignored_files() -> anyhow::Result<()> {
474 let toml = r#"
475[package]
476name = "my-crate"
477version = "0.1.0"
478
479[package.metadata.changeset]
480ignored-files = ["benches/**", "examples/**"]
481"#;
482 let dir = setup_with_config(toml)?;
483
484 let config = parse_package_config(dir.path())?;
485
486 assert!(config.is_ignored(Path::new("benches/bench.rs")));
487 assert!(config.is_ignored(Path::new("examples/demo.rs")));
488 assert!(!config.is_ignored(Path::new("src/lib.rs")));
489
490 Ok(())
491 }
492
493 #[test]
494 fn parse_package_config_without_metadata() -> anyhow::Result<()> {
495 let toml = r#"
496[package]
497name = "my-crate"
498version = "0.1.0"
499"#;
500 let dir = setup_with_config(toml)?;
501
502 let config = parse_package_config(dir.path())?;
503
504 assert!(!config.is_ignored(Path::new("benches/bench.rs")));
505
506 Ok(())
507 }
508
509 #[test]
510 fn parse_single_package_root_config() -> anyhow::Result<()> {
511 let toml = r#"
512[package]
513name = "my-crate"
514version = "0.1.0"
515
516[package.metadata.changeset]
517ignored-files = ["*.md"]
518changeset-dir = "my-changesets"
519"#;
520 let dir = setup_with_config(toml)?;
521
522 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Package)?;
523
524 assert!(config.is_ignored(Path::new("README.md")));
525 assert_eq!(config.changeset_dir(), Path::new("my-changesets"));
526
527 Ok(())
528 }
529
530 #[test]
531 fn invalid_glob_pattern_returns_error() -> anyhow::Result<()> {
532 let toml = r#"
533[workspace]
534members = ["crates/*"]
535
536[workspace.metadata.changeset]
537ignored-files = ["[invalid"]
538"#;
539 let dir = setup_with_config(toml)?;
540
541 let result = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace);
542
543 assert!(result.is_err());
544 let err = result.expect_err("should fail on invalid glob");
545 assert!(matches!(err, ProjectError::GlobPattern { .. }));
546
547 Ok(())
548 }
549
550 #[test]
551 fn empty_ignored_files_list() -> anyhow::Result<()> {
552 let toml = r#"
553[workspace]
554members = ["crates/*"]
555
556[workspace.metadata.changeset]
557ignored-files = []
558"#;
559 let dir = setup_with_config(toml)?;
560
561 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
562
563 assert!(!config.is_ignored(Path::new("anything.txt")));
564
565 Ok(())
566 }
567
568 #[test]
569 fn parse_workspace_changelog_config() -> anyhow::Result<()> {
570 let toml = r#"
571[workspace]
572members = ["crates/*"]
573
574[workspace.metadata.changeset]
575changelog = "per-package"
576comparison-links = "enabled"
577comparison-links-template = "https://example.com/{repository}/compare/{base}...{target}"
578"#;
579 let dir = setup_with_config(toml)?;
580
581 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
582 let changelog_config = config.changelog_config();
583
584 assert_eq!(changelog_config.changelog(), ChangelogLocation::PerPackage);
585 assert_eq!(
586 changelog_config.comparison_links(),
587 ComparisonLinksSetting::Enabled
588 );
589 assert_eq!(
590 changelog_config.comparison_links_template(),
591 Some("https://example.com/{repository}/compare/{base}...{target}")
592 );
593
594 Ok(())
595 }
596
597 #[test]
598 fn parse_changelog_config_defaults() -> anyhow::Result<()> {
599 let toml = r#"
600[workspace]
601members = ["crates/*"]
602"#;
603 let dir = setup_with_config(toml)?;
604
605 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
606 let changelog_config = config.changelog_config();
607
608 assert_eq!(changelog_config.changelog(), ChangelogLocation::Root);
609 assert_eq!(
610 changelog_config.comparison_links(),
611 ComparisonLinksSetting::Auto
612 );
613 assert!(changelog_config.comparison_links_template().is_none());
614
615 Ok(())
616 }
617
618 #[test]
619 fn parse_single_package_changelog_config() -> anyhow::Result<()> {
620 let toml = r#"
621[package]
622name = "my-crate"
623version = "0.1.0"
624
625[package.metadata.changeset]
626changelog = "root"
627comparison-links = "disabled"
628"#;
629 let dir = setup_with_config(toml)?;
630
631 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Package)?;
632 let changelog_config = config.changelog_config();
633
634 assert_eq!(changelog_config.changelog(), ChangelogLocation::Root);
635 assert_eq!(
636 changelog_config.comparison_links(),
637 ComparisonLinksSetting::Disabled
638 );
639
640 Ok(())
641 }
642
643 #[test]
644 fn parse_git_config_defaults() -> anyhow::Result<()> {
645 let toml = r#"
646[workspace]
647members = ["crates/*"]
648"#;
649 let dir = setup_with_config(toml)?;
650
651 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
652 let git_config = config.git_config();
653
654 assert!(git_config.commit());
655 assert!(git_config.tags());
656 assert!(!git_config.keep_changesets());
657 assert_eq!(git_config.tag_format(), TagFormat::VersionOnly);
658 assert_eq!(git_config.commit_title_template(), "{new-version}");
659 assert!(git_config.changes_in_body());
660
661 Ok(())
662 }
663
664 #[test]
665 fn parse_git_config_all_options() -> anyhow::Result<()> {
666 let toml = r#"
667[workspace]
668members = ["crates/*"]
669
670[workspace.metadata.changeset]
671commit = false
672tags = false
673keep-changesets = true
674tag-format = "crate-prefixed"
675commit-title-template = "chore(release): {new-version}"
676changes-in-body = false
677"#;
678 let dir = setup_with_config(toml)?;
679
680 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
681 let git_config = config.git_config();
682
683 assert!(!git_config.commit());
684 assert!(!git_config.tags());
685 assert!(git_config.keep_changesets());
686 assert_eq!(git_config.tag_format(), TagFormat::CratePrefixed);
687 assert_eq!(
688 git_config.commit_title_template(),
689 "chore(release): {new-version}"
690 );
691 assert!(!git_config.changes_in_body());
692
693 Ok(())
694 }
695
696 #[test]
697 fn parse_git_config_version_only_format() -> anyhow::Result<()> {
698 let toml = r#"
699[workspace]
700members = ["crates/*"]
701
702[workspace.metadata.changeset]
703tag-format = "version-only"
704"#;
705 let dir = setup_with_config(toml)?;
706
707 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
708 let git_config = config.git_config();
709
710 assert_eq!(git_config.tag_format(), TagFormat::VersionOnly);
711
712 Ok(())
713 }
714
715 #[test]
716 fn parse_single_package_git_config() -> anyhow::Result<()> {
717 let toml = r#"
718[package]
719name = "my-crate"
720version = "0.1.0"
721
722[package.metadata.changeset]
723commit = false
724tags = true
725keep-changesets = true
726"#;
727 let dir = setup_with_config(toml)?;
728
729 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Package)?;
730 let git_config = config.git_config();
731
732 assert!(!git_config.commit());
733 assert!(git_config.tags());
734 assert!(git_config.keep_changesets());
735
736 Ok(())
737 }
738
739 #[test]
740 fn parse_zero_version_behavior_default() -> anyhow::Result<()> {
741 let toml = r#"
742[workspace]
743members = ["crates/*"]
744"#;
745 let dir = setup_with_config(toml)?;
746
747 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
748
749 assert_eq!(
750 config.zero_version_behavior(),
751 ZeroVersionBehavior::EffectiveMinor
752 );
753
754 Ok(())
755 }
756
757 #[test]
758 fn parse_zero_version_behavior_effective_minor() -> anyhow::Result<()> {
759 let toml = r#"
760[workspace]
761members = ["crates/*"]
762
763[workspace.metadata.changeset]
764zero-version-behavior = "effective-minor"
765"#;
766 let dir = setup_with_config(toml)?;
767
768 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
769
770 assert_eq!(
771 config.zero_version_behavior(),
772 ZeroVersionBehavior::EffectiveMinor
773 );
774
775 Ok(())
776 }
777
778 #[test]
779 fn parse_zero_version_behavior_auto_promote() -> anyhow::Result<()> {
780 let toml = r#"
781[workspace]
782members = ["crates/*"]
783
784[workspace.metadata.changeset]
785zero-version-behavior = "auto-promote-on-major"
786"#;
787 let dir = setup_with_config(toml)?;
788
789 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
790
791 assert_eq!(
792 config.zero_version_behavior(),
793 ZeroVersionBehavior::AutoPromoteOnMajor
794 );
795
796 Ok(())
797 }
798
799 #[test]
800 fn parse_single_package_zero_version_behavior() -> anyhow::Result<()> {
801 let toml = r#"
802[package]
803name = "my-crate"
804version = "0.1.0"
805
806[package.metadata.changeset]
807zero-version-behavior = "auto-promote-on-major"
808"#;
809 let dir = setup_with_config(toml)?;
810
811 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Package)?;
812
813 assert_eq!(
814 config.zero_version_behavior(),
815 ZeroVersionBehavior::AutoPromoteOnMajor
816 );
817
818 Ok(())
819 }
820
821 #[test]
822 fn parse_dependency_bump_changelog_template_from_workspace_metadata() -> anyhow::Result<()> {
823 let toml = r#"
824[workspace]
825members = ["crates/*"]
826
827[workspace.metadata.changeset]
828dependency-bump-changelog-template = "Upgraded `{dependency}` to {version}"
829"#;
830 let dir = setup_with_config(toml)?;
831
832 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
833
834 assert_eq!(
835 config.dependency_bump_changelog_template(),
836 "Upgraded `{dependency}` to {version}"
837 );
838
839 Ok(())
840 }
841
842 #[test]
843 fn parse_dependency_bump_changelog_template_from_package_metadata() -> anyhow::Result<()> {
844 let toml = r#"
845[package]
846name = "my-crate"
847version = "0.1.0"
848
849[package.metadata.changeset]
850dependency-bump-changelog-template = "Bumped `{dependency}` to {version}"
851"#;
852 let dir = setup_with_config(toml)?;
853
854 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Package)?;
855
856 assert_eq!(
857 config.dependency_bump_changelog_template(),
858 "Bumped `{dependency}` to {version}"
859 );
860
861 Ok(())
862 }
863
864 #[test]
865 fn dependency_bump_changelog_template_defaults_when_not_specified() -> anyhow::Result<()> {
866 let toml = r#"
867[workspace]
868members = ["crates/*"]
869"#;
870 let dir = setup_with_config(toml)?;
871
872 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
873
874 assert_eq!(
875 config.dependency_bump_changelog_template(),
876 "Updated dependency `{dependency}` to v{version}"
877 );
878
879 Ok(())
880 }
881
882 #[test]
883 fn parse_workspace_root_config_base_branch_defaults_to_main() -> anyhow::Result<()> {
884 let toml = r#"
885[workspace]
886members = ["crates/*"]
887"#;
888 let dir = setup_with_config(toml)?;
889
890 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
891
892 assert_eq!(config.base_branch(), "main");
893
894 Ok(())
895 }
896
897 #[test]
898 fn parse_workspace_root_config_with_base_branch() -> anyhow::Result<()> {
899 let toml = r#"
900[workspace]
901members = ["crates/*"]
902
903[workspace.metadata.changeset]
904base-branch = "develop"
905"#;
906 let dir = setup_with_config(toml)?;
907
908 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
909
910 assert_eq!(config.base_branch(), "develop");
911
912 Ok(())
913 }
914
915 #[test]
916 fn parse_single_package_root_config_with_base_branch() -> anyhow::Result<()> {
917 let toml = r#"
918[package]
919name = "my-crate"
920version = "0.1.0"
921
922[package.metadata.changeset]
923base-branch = "master"
924"#;
925 let dir = setup_with_config(toml)?;
926
927 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Package)?;
928
929 assert_eq!(config.base_branch(), "master");
930
931 Ok(())
932 }
933
934 #[test]
935 fn parse_none_bump_behavior_default() -> anyhow::Result<()> {
936 let toml = r#"
937[workspace]
938members = ["crates/*"]
939"#;
940 let dir = setup_with_config(toml)?;
941
942 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
943
944 assert_eq!(
945 config.none_bump_behavior(),
946 changeset_core::NoneBumpBehavior::PromoteToPatch
947 );
948
949 Ok(())
950 }
951
952 #[test]
953 fn parse_none_bump_behavior_allow() -> anyhow::Result<()> {
954 let toml = r#"
955[workspace]
956members = ["crates/*"]
957
958[workspace.metadata.changeset]
959none-bump-behavior = "allow"
960"#;
961 let dir = setup_with_config(toml)?;
962
963 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
964
965 assert_eq!(
966 config.none_bump_behavior(),
967 changeset_core::NoneBumpBehavior::Allow
968 );
969
970 Ok(())
971 }
972
973 #[test]
974 fn parse_none_bump_behavior_disallow() -> anyhow::Result<()> {
975 let toml = r#"
976[workspace]
977members = ["crates/*"]
978
979[workspace.metadata.changeset]
980none-bump-behavior = "disallow"
981"#;
982 let dir = setup_with_config(toml)?;
983
984 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
985
986 assert_eq!(
987 config.none_bump_behavior(),
988 changeset_core::NoneBumpBehavior::Disallow
989 );
990
991 Ok(())
992 }
993
994 #[test]
995 fn parse_none_bump_promote_message_template_custom() -> anyhow::Result<()> {
996 let toml = r#"
997[workspace]
998members = ["crates/*"]
999
1000[workspace.metadata.changeset]
1001none-bump-promote-message-template = "chore: internal refactor"
1002"#;
1003 let dir = setup_with_config(toml)?;
1004
1005 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
1006
1007 assert_eq!(
1008 config.none_bump_promote_message_template(),
1009 "chore: internal refactor"
1010 );
1011
1012 Ok(())
1013 }
1014
1015 #[test]
1016 fn parse_none_bump_promote_message_template_default() -> anyhow::Result<()> {
1017 let toml = r#"
1018[workspace]
1019members = ["crates/*"]
1020"#;
1021 let dir = setup_with_config(toml)?;
1022
1023 let config = parse_cargo_root_config(dir.path(), CargoRootConfigType::Workspace)?;
1024
1025 assert_eq!(
1026 config.none_bump_promote_message_template(),
1027 "Internal architectural changes"
1028 );
1029
1030 Ok(())
1031 }
1032}