1use std::fs;
2use std::path::{Path, PathBuf};
3
4use changeset_git::DEFAULT_BASE_BRANCH;
5use changeset_manifest::{InitConfig, MetadataSection};
6use changeset_project::{CargoProject, ProjectKind, RootChangesetConfig};
7
8use derive_builder::Builder;
9use gset::Getset;
10
11use crate::Result;
12use crate::traits::{
13 ChangelogSettingsInput, FilteringSettingsInput, GitSettingsInput, InitInteractionProvider,
14 ManifestMetadataWriter, ProjectContext, ProjectProvider, VersionSettingsInput,
15};
16
17#[derive(Debug, Default, Builder, Getset)]
18#[builder(default)]
19pub struct InitInput {
20 #[getset(get_copy, vis = "pub")]
21 defaults: bool,
22 #[getset(get_as_ref, vis = "pub", ty = "Option<&GitSettingsInput>")]
23 git_config: Option<GitSettingsInput>,
24 #[getset(get_as_ref, vis = "pub", ty = "Option<&ChangelogSettingsInput>")]
25 changelog_config: Option<ChangelogSettingsInput>,
26 #[getset(get_as_ref, vis = "pub", ty = "Option<&VersionSettingsInput>")]
27 version_config: Option<VersionSettingsInput>,
28 #[getset(get_as_ref, vis = "pub", ty = "Option<&FilteringSettingsInput>")]
29 filtering_config: Option<FilteringSettingsInput>,
30}
31
32#[derive(Debug, Getset)]
33pub struct InitPlan {
34 #[getset(get, vis = "pub")]
35 changeset_dir: PathBuf,
36 #[getset(get_copy, vis = "pub")]
37 dir_exists: bool,
38 #[getset(get_copy, vis = "pub")]
39 gitkeep_exists: bool,
40 #[getset(get_copy, vis = "pub")]
41 metadata_section: MetadataSection,
42 #[getset(get, vis = "pub")]
43 config: InitConfig,
44}
45
46impl InitPlan {
47 #[must_use]
48 pub fn new(
49 changeset_dir: PathBuf,
50 dir_exists: bool,
51 gitkeep_exists: bool,
52 metadata_section: MetadataSection,
53 config: InitConfig,
54 ) -> Self {
55 Self {
56 changeset_dir,
57 dir_exists,
58 gitkeep_exists,
59 metadata_section,
60 config,
61 }
62 }
63}
64
65#[derive(Debug, Getset)]
66#[must_use]
67pub struct InitOutput {
68 #[getset(get, vis = "pub")]
69 changeset_dir: PathBuf,
70 #[getset(get_copy, vis = "pub")]
71 created_dir: bool,
72 #[getset(get_copy, vis = "pub")]
73 created_gitkeep: bool,
74 #[getset(get_copy, vis = "pub")]
75 wrote_config: bool,
76 #[getset(get_copy, vis = "pub")]
77 config_location: Option<MetadataSection>,
78}
79
80impl InitOutput {
81 pub(crate) fn new(
82 changeset_dir: PathBuf,
83 created_dir: bool,
84 created_gitkeep: bool,
85 wrote_config: bool,
86 config_location: Option<MetadataSection>,
87 ) -> Self {
88 Self {
89 changeset_dir,
90 created_dir,
91 created_gitkeep,
92 wrote_config,
93 config_location,
94 }
95 }
96}
97
98pub struct InitOperation<P, M = (), I = ()> {
99 project_provider: P,
100 manifest_writer: Option<M>,
101 interaction_provider: Option<I>,
102}
103
104impl<P> InitOperation<P, (), ()>
105where
106 P: ProjectProvider,
107{
108 pub fn new(project_provider: P) -> Self {
109 Self {
110 project_provider,
111 manifest_writer: None,
112 interaction_provider: None,
113 }
114 }
115
116 pub fn prepare_simple(&self, start_path: &Path) -> Result<InitPlan> {
120 let project = self.project_provider.discover_project(start_path)?;
121 let (root_config, _) = self.project_provider.load_configs(&project)?;
122
123 Ok(build_init_plan(
124 &project,
125 &root_config,
126 InitConfig::default(),
127 ))
128 }
129
130 pub fn execute_simple_plan(&self, start_path: &Path, plan: &InitPlan) -> Result<InitOutput> {
134 let project = self.project_provider.discover_project(start_path)?;
135 let (root_config, _) = self.project_provider.load_configs(&project)?;
136
137 let changeset_dir = self
138 .project_provider
139 .ensure_changeset_dir(&project, &root_config)?;
140
141 let gitkeep_path = changeset_dir.join(".gitkeep");
142 if !plan.gitkeep_exists() {
143 fs::write(&gitkeep_path, "")?;
144 }
145
146 Ok(InitOutput::new(
147 changeset_dir,
148 !plan.dir_exists(),
149 !plan.gitkeep_exists(),
150 false,
151 None,
152 ))
153 }
154
155 pub fn execute_simple(&self, start_path: &Path) -> Result<InitOutput> {
160 let plan = self.prepare_simple(start_path)?;
161 self.execute_simple_plan(start_path, &plan)
162 }
163}
164
165impl<P, M, I> InitOperation<P, M, I>
166where
167 P: ProjectProvider,
168{
169 #[must_use]
170 pub fn with_manifest_writer<M2>(self, writer: M2) -> InitOperation<P, M2, I> {
171 InitOperation {
172 project_provider: self.project_provider,
173 manifest_writer: Some(writer),
174 interaction_provider: self.interaction_provider,
175 }
176 }
177
178 #[must_use]
179 pub fn with_interaction_provider<I2>(self, provider: I2) -> InitOperation<P, M, I2> {
180 InitOperation {
181 project_provider: self.project_provider,
182 manifest_writer: self.manifest_writer,
183 interaction_provider: Some(provider),
184 }
185 }
186}
187
188impl<P, M, I> InitOperation<P, M, I>
189where
190 P: ProjectProvider,
191 M: ManifestMetadataWriter,
192 I: InitInteractionProvider,
193{
194 pub fn prepare(&self, start_path: &Path, input: &InitInput) -> Result<InitPlan> {
199 let project = self.project_provider.discover_project(start_path)?;
200 let (root_config, _) = self.project_provider.load_configs(&project)?;
201
202 let context = ProjectContext {
203 is_single_package: *project.kind() == ProjectKind::SinglePackage,
204 };
205 let config = self.build_config(input, context)?;
206
207 Ok(build_init_plan(&project, &root_config, config))
208 }
209
210 pub fn execute_plan(&self, start_path: &Path, plan: &InitPlan) -> Result<InitOutput> {
215 let project = self.project_provider.discover_project(start_path)?;
216 let (root_config, _) = self.project_provider.load_configs(&project)?;
217
218 let changeset_dir = self
219 .project_provider
220 .ensure_changeset_dir(&project, &root_config)?;
221
222 let gitkeep_path = changeset_dir.join(".gitkeep");
223 if !plan.gitkeep_exists() {
224 fs::write(&gitkeep_path, "")?;
225 }
226
227 let wrote_config = if let Some(ref writer) = self.manifest_writer {
228 if plan.config().is_empty() {
229 false
230 } else {
231 let manifest_path = project.root().join("Cargo.toml");
232 writer.write_metadata(&manifest_path, plan.metadata_section(), plan.config())?;
233 true
234 }
235 } else {
236 false
237 };
238
239 Ok(InitOutput::new(
240 changeset_dir,
241 !plan.dir_exists(),
242 !plan.gitkeep_exists(),
243 wrote_config,
244 if wrote_config {
245 Some(plan.metadata_section())
246 } else {
247 None
248 },
249 ))
250 }
251
252 pub fn execute(&self, start_path: &Path, input: &InitInput) -> Result<InitOutput> {
257 let plan = self.prepare(start_path, input)?;
258 self.execute_plan(start_path, &plan)
259 }
260
261 fn build_config(&self, input: &InitInput, context: ProjectContext) -> Result<InitConfig> {
262 let mut config = build_config_from_input(input, context);
263
264 if config.is_empty() {
265 if let Some(ref provider) = self.interaction_provider {
266 let interactive_input = InitInputBuilder::default()
267 .git_config(provider.configure_git_settings(context)?)
268 .changelog_config(provider.configure_changelog_settings(context)?)
269 .version_config(provider.configure_version_settings()?)
270 .filtering_config(provider.configure_filtering_settings()?)
271 .build()
272 .expect("all fields have defaults");
273 apply_settings_to_config(&mut config, &interactive_input);
274 }
275 }
276
277 Ok(config)
278 }
279}
280
281fn build_init_plan(
282 project: &CargoProject,
283 root_config: &RootChangesetConfig,
284 config: InitConfig,
285) -> InitPlan {
286 let changeset_dir_path = root_config.changeset_dir();
287 let full_changeset_dir = project.root().join(changeset_dir_path);
288 let dir_exists = full_changeset_dir.exists();
289 let gitkeep_exists = full_changeset_dir.join(".gitkeep").exists();
290
291 let metadata_section = match project.kind() {
292 ProjectKind::VirtualWorkspace | ProjectKind::WorkspaceWithRoot => {
293 MetadataSection::Workspace
294 }
295 ProjectKind::SinglePackage => MetadataSection::Package,
296 };
297
298 InitPlan::new(
299 full_changeset_dir,
300 dir_exists,
301 gitkeep_exists,
302 metadata_section,
303 config,
304 )
305}
306
307#[must_use]
311pub(crate) fn build_default_config(context: ProjectContext) -> InitConfig {
312 let tag_format = if context.is_single_package {
313 changeset_manifest::TagFormat::VersionOnly
314 } else {
315 changeset_manifest::TagFormat::CratePrefixed
316 };
317
318 InitConfig {
319 commit: Some(true),
320 tags: Some(true),
321 keep_changesets: Some(false),
322 tag_format: Some(tag_format),
323 changelog: Some(changeset_manifest::ChangelogLocation::default()),
324 comparison_links: Some(changeset_manifest::ComparisonLinks::default()),
325 zero_version_behavior: Some(changeset_manifest::ZeroVersionBehavior::default()),
326 dependency_bump_changelog_template: Some(String::from(
327 "Updated dependency `{dependency}` to v{version}",
328 )),
329 base_branch: Some(String::from(DEFAULT_BASE_BRANCH)),
330 none_bump_behavior: Some(changeset_manifest::NoneBumpBehavior::default()),
331 none_bump_promote_message_template: None,
332 commit_title_template: Some(String::from("{new-version}")),
333 changes_in_body: Some(true),
334 comparison_links_template: None,
335 ignored_files: None,
336 }
337}
338
339#[must_use]
340pub fn build_config_from_input(input: &InitInput, context: ProjectContext) -> InitConfig {
341 if input.defaults() {
342 return build_default_config(context);
343 }
344
345 let mut config = InitConfig::default();
346 apply_settings_to_config(&mut config, input);
347 config
348}
349
350fn apply_settings_to_config(config: &mut InitConfig, input: &InitInput) {
351 if let Some(git) = input.git_config() {
352 config.commit = Some(git.commit);
353 config.tags = Some(git.tags);
354 config.keep_changesets = Some(git.keep_changesets);
355 config.tag_format = Some(git.tag_format);
356 config.base_branch = Some(git.base_branch.clone());
357 config
358 .commit_title_template
359 .clone_from(&git.commit_title_template);
360 config.changes_in_body = git.changes_in_body;
361 }
362
363 if let Some(changelog) = input.changelog_config() {
364 config.changelog = Some(changelog.changelog);
365 config.comparison_links = Some(changelog.comparison_links);
366 config
367 .comparison_links_template
368 .clone_from(&changelog.comparison_links_template);
369 config
370 .dependency_bump_changelog_template
371 .clone_from(&changelog.dependency_bump_changelog_template);
372 }
373
374 if let Some(version) = input.version_config() {
375 config.zero_version_behavior = version.zero_version_behavior;
376 config.none_bump_behavior = version.none_bump_behavior;
377 config
378 .none_bump_promote_message_template
379 .clone_from(&version.none_bump_promote_message_template);
380 }
381
382 if let Some(filtering) = input.filtering_config() {
383 if !filtering.ignored_files.is_empty() {
384 config.ignored_files = Some(filtering.ignored_files.clone());
385 }
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use std::sync::Arc;
392
393 use changeset_manifest::{ChangelogLocation, ComparisonLinks, TagFormat, ZeroVersionBehavior};
394
395 use super::*;
396 use crate::mocks::{MockInitInteractionProvider, MockManifestWriter, MockProjectProvider};
397
398 #[test]
399 fn returns_changeset_dir_path() {
400 let dir = tempfile::tempdir().expect("create temp dir");
401 let changeset_dir = dir.path().join(".changeset");
402 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
403
404 let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
405 .with_changeset_dir(changeset_dir.clone());
406
407 let operation = InitOperation::new(project_provider);
408
409 let result = operation
410 .execute_simple(Path::new("/any"))
411 .expect("InitOperation failed for single-package project");
412
413 assert_eq!(result.changeset_dir(), &changeset_dir);
414 }
415
416 #[test]
417 fn works_with_workspace_projects() {
418 let dir = tempfile::tempdir().expect("create temp dir");
419 let changeset_dir = dir.path().join(".changeset");
420 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
421
422 let project_provider =
423 MockProjectProvider::workspace(vec![("crate-a", "1.0.0"), ("crate-b", "2.0.0")])
424 .with_changeset_dir(changeset_dir.clone());
425
426 let operation = InitOperation::new(project_provider);
427
428 let result = operation
429 .execute_simple(Path::new("/any"))
430 .expect("InitOperation failed for workspace project");
431
432 assert!(
433 result
434 .changeset_dir()
435 .to_string_lossy()
436 .contains(".changeset")
437 );
438 }
439
440 #[test]
441 fn creates_gitkeep_file() {
442 let dir = tempfile::tempdir().expect("create temp dir");
443 let changeset_dir = dir.path().join(".changeset");
444 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
445
446 let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
447 .with_changeset_dir(changeset_dir.clone());
448
449 let operation = InitOperation::new(project_provider);
450
451 let result = operation
452 .execute_simple(Path::new("/any"))
453 .expect("InitOperation failed");
454
455 assert!(result.created_gitkeep());
456 assert!(changeset_dir.join(".gitkeep").exists());
457 }
458
459 #[test]
460 fn creates_gitkeep_even_when_dir_exists() {
461 let dir = tempfile::tempdir().expect("create temp dir");
462 let changeset_dir = dir.path().join(".changeset");
463 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
464
465 let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
466 .with_changeset_dir(changeset_dir.clone());
467
468 let operation = InitOperation::new(project_provider);
469 let result = operation
470 .execute_simple(Path::new("/any"))
471 .expect("InitOperation failed");
472
473 assert!(!result.created_dir());
474 assert!(result.created_gitkeep());
475 assert!(changeset_dir.join(".gitkeep").exists());
476 }
477
478 #[test]
479 fn writes_config_with_defaults() {
480 let dir = tempfile::tempdir().expect("create temp dir");
481 let changeset_dir = dir.path().join(".changeset");
482 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
483
484 let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
485 .with_changeset_dir(changeset_dir.clone());
486 let manifest_writer = Arc::new(MockManifestWriter::new());
487 let interaction_provider = Arc::new(MockInitInteractionProvider::new());
488
489 let operation = InitOperation::new(project_provider)
490 .with_manifest_writer(Arc::clone(&manifest_writer))
491 .with_interaction_provider(Arc::clone(&interaction_provider));
492
493 let input = InitInputBuilder::default()
494 .defaults(true)
495 .build()
496 .expect("all fields have defaults");
497
498 let result = operation
499 .execute(Path::new("/any"), &input)
500 .expect("InitOperation failed");
501
502 assert!(result.wrote_config());
503 assert_eq!(result.config_location(), Some(MetadataSection::Package));
504
505 let written = manifest_writer.written_metadata();
506 assert_eq!(written.len(), 1);
507 let (_, section, config) = &written[0];
508 assert_eq!(*section, MetadataSection::Package);
509 assert_eq!(config.commit, Some(true));
510 assert_eq!(config.tags, Some(true));
511 assert_eq!(config.keep_changesets, Some(false));
512 }
513
514 #[test]
515 fn writes_config_from_input() {
516 let dir = tempfile::tempdir().expect("create temp dir");
517 let changeset_dir = dir.path().join(".changeset");
518 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
519
520 let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
521 .with_changeset_dir(changeset_dir.clone());
522 let manifest_writer = Arc::new(MockManifestWriter::new());
523 let interaction_provider = Arc::new(MockInitInteractionProvider::new());
524
525 let operation = InitOperation::new(project_provider)
526 .with_manifest_writer(Arc::clone(&manifest_writer))
527 .with_interaction_provider(Arc::clone(&interaction_provider));
528
529 let input = InitInputBuilder::default()
530 .git_config(Some(GitSettingsInput {
531 commit: false,
532 tags: true,
533 keep_changesets: true,
534 tag_format: TagFormat::CratePrefixed,
535 base_branch: String::from("main"),
536 ..Default::default()
537 }))
538 .changelog_config(Some(ChangelogSettingsInput {
539 changelog: ChangelogLocation::PerPackage,
540 comparison_links: ComparisonLinks::Enabled,
541 ..Default::default()
542 }))
543 .version_config(Some(VersionSettingsInput {
544 zero_version_behavior: Some(ZeroVersionBehavior::AutoPromoteOnMajor),
545 ..Default::default()
546 }))
547 .build()
548 .expect("all fields have defaults");
549
550 let result = operation
551 .execute(Path::new("/any"), &input)
552 .expect("InitOperation failed");
553
554 assert!(result.wrote_config());
555
556 let written = manifest_writer.written_metadata();
557 assert_eq!(written.len(), 1);
558 let (_, _, config) = &written[0];
559 assert_eq!(config.commit, Some(false));
560 assert_eq!(config.tags, Some(true));
561 assert_eq!(config.keep_changesets, Some(true));
562 assert_eq!(config.tag_format, Some(TagFormat::CratePrefixed));
563 assert_eq!(config.changelog, Some(ChangelogLocation::PerPackage));
564 assert_eq!(config.comparison_links, Some(ComparisonLinks::Enabled));
565 assert_eq!(
566 config.zero_version_behavior,
567 Some(ZeroVersionBehavior::AutoPromoteOnMajor)
568 );
569 }
570
571 #[test]
572 fn writes_partial_config() {
573 let dir = tempfile::tempdir().expect("create temp dir");
574 let changeset_dir = dir.path().join(".changeset");
575 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
576
577 let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
578 .with_changeset_dir(changeset_dir.clone());
579 let manifest_writer = Arc::new(MockManifestWriter::new());
580 let interaction_provider = Arc::new(MockInitInteractionProvider::new());
581
582 let operation = InitOperation::new(project_provider)
583 .with_manifest_writer(Arc::clone(&manifest_writer))
584 .with_interaction_provider(Arc::clone(&interaction_provider));
585
586 let input = InitInputBuilder::default()
587 .git_config(Some(GitSettingsInput {
588 commit: true,
589 tags: false,
590 keep_changesets: false,
591 tag_format: TagFormat::VersionOnly,
592 base_branch: String::from("main"),
593 ..Default::default()
594 }))
595 .build()
596 .expect("all fields have defaults");
597
598 let result = operation
599 .execute(Path::new("/any"), &input)
600 .expect("InitOperation failed");
601
602 assert!(result.wrote_config());
603
604 let written = manifest_writer.written_metadata();
605 assert_eq!(written.len(), 1);
606 let (_, _, config) = &written[0];
607 assert_eq!(config.commit, Some(true));
608 assert_eq!(config.tags, Some(false));
609 assert!(config.changelog.is_none());
610 assert!(config.zero_version_behavior.is_none());
611 }
612
613 #[test]
614 fn interactive_mode_collects_all_groups() {
615 let dir = tempfile::tempdir().expect("create temp dir");
616 let changeset_dir = dir.path().join(".changeset");
617 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
618
619 let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
620 .with_changeset_dir(changeset_dir.clone());
621 let manifest_writer = Arc::new(MockManifestWriter::new());
622 let interaction_provider = Arc::new(MockInitInteractionProvider::all_defaults());
623
624 let operation = InitOperation::new(project_provider)
625 .with_manifest_writer(Arc::clone(&manifest_writer))
626 .with_interaction_provider(Arc::clone(&interaction_provider));
627
628 let input = InitInput::default();
629
630 let result = operation
631 .execute(Path::new("/any"), &input)
632 .expect("InitOperation failed");
633
634 assert!(result.wrote_config());
635
636 let written = manifest_writer.written_metadata();
637 assert_eq!(written.len(), 1);
638 let (_, _, config) = &written[0];
639 assert!(config.commit.is_some());
640 assert!(config.tags.is_some());
641 assert!(config.changelog.is_some());
642 assert!(config.zero_version_behavior.is_some());
643 }
644
645 #[test]
646 fn interactive_mode_skips_declined_groups() {
647 let dir = tempfile::tempdir().expect("create temp dir");
648 let changeset_dir = dir.path().join(".changeset");
649 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
650
651 let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
652 .with_changeset_dir(changeset_dir.clone());
653 let manifest_writer = Arc::new(MockManifestWriter::new());
654 let interaction_provider = Arc::new(
655 MockInitInteractionProvider::new()
656 .with_git_settings(Some(GitSettingsInput::default()))
657 .with_changelog_settings(None)
658 .with_version_settings(None),
659 );
660
661 let operation = InitOperation::new(project_provider)
662 .with_manifest_writer(Arc::clone(&manifest_writer))
663 .with_interaction_provider(Arc::clone(&interaction_provider));
664
665 let input = InitInput::default();
666
667 let result = operation
668 .execute(Path::new("/any"), &input)
669 .expect("InitOperation failed");
670
671 assert!(result.wrote_config());
672
673 let written = manifest_writer.written_metadata();
674 assert_eq!(written.len(), 1);
675 let (_, _, config) = &written[0];
676 assert!(config.commit.is_some());
677 assert!(config.changelog.is_none());
678 assert!(config.zero_version_behavior.is_none());
679 }
680
681 #[test]
682 fn skips_config_write_when_empty() {
683 let dir = tempfile::tempdir().expect("create temp dir");
684 let changeset_dir = dir.path().join(".changeset");
685 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
686
687 let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
688 .with_changeset_dir(changeset_dir.clone());
689 let manifest_writer = Arc::new(MockManifestWriter::new());
690 let interaction_provider = Arc::new(MockInitInteractionProvider::all_skipped());
691
692 let operation = InitOperation::new(project_provider)
693 .with_manifest_writer(Arc::clone(&manifest_writer))
694 .with_interaction_provider(Arc::clone(&interaction_provider));
695
696 let input = InitInput::default();
697
698 let result = operation
699 .execute(Path::new("/any"), &input)
700 .expect("InitOperation failed");
701
702 assert!(!result.wrote_config());
703 assert!(result.config_location().is_none());
704
705 let written = manifest_writer.written_metadata();
706 assert!(written.is_empty());
707 }
708
709 #[test]
710 fn workspace_uses_workspace_metadata() {
711 let dir = tempfile::tempdir().expect("create temp dir");
712 let changeset_dir = dir.path().join(".changeset");
713 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
714
715 let project_provider =
716 MockProjectProvider::workspace(vec![("crate-a", "1.0.0"), ("crate-b", "2.0.0")])
717 .with_changeset_dir(changeset_dir.clone());
718 let manifest_writer = Arc::new(MockManifestWriter::new());
719 let interaction_provider = Arc::new(MockInitInteractionProvider::new());
720
721 let operation = InitOperation::new(project_provider)
722 .with_manifest_writer(Arc::clone(&manifest_writer))
723 .with_interaction_provider(Arc::clone(&interaction_provider));
724
725 let input = InitInputBuilder::default()
726 .defaults(true)
727 .build()
728 .expect("all fields have defaults");
729
730 let result = operation
731 .execute(Path::new("/any"), &input)
732 .expect("InitOperation failed");
733
734 assert!(result.wrote_config());
735 assert_eq!(result.config_location(), Some(MetadataSection::Workspace));
736
737 let written = manifest_writer.written_metadata();
738 assert_eq!(written.len(), 1);
739 let (_, section, _) = &written[0];
740 assert_eq!(*section, MetadataSection::Workspace);
741 }
742
743 #[test]
744 fn single_package_uses_package_metadata() {
745 let dir = tempfile::tempdir().expect("create temp dir");
746 let changeset_dir = dir.path().join(".changeset");
747 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
748
749 let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
750 .with_changeset_dir(changeset_dir.clone());
751 let manifest_writer = Arc::new(MockManifestWriter::new());
752 let interaction_provider = Arc::new(MockInitInteractionProvider::new());
753
754 let operation = InitOperation::new(project_provider)
755 .with_manifest_writer(Arc::clone(&manifest_writer))
756 .with_interaction_provider(Arc::clone(&interaction_provider));
757
758 let input = InitInputBuilder::default()
759 .defaults(true)
760 .build()
761 .expect("all fields have defaults");
762
763 let result = operation
764 .execute(Path::new("/any"), &input)
765 .expect("InitOperation failed");
766
767 assert!(result.wrote_config());
768 assert_eq!(result.config_location(), Some(MetadataSection::Package));
769
770 let written = manifest_writer.written_metadata();
771 assert_eq!(written.len(), 1);
772 let (_, section, _) = &written[0];
773 assert_eq!(*section, MetadataSection::Package);
774 }
775
776 #[test]
777 fn default_config_includes_dependency_bump_changelog_template() {
778 let context = ProjectContext {
779 is_single_package: true,
780 };
781 let config = build_default_config(context);
782 assert_eq!(
783 config.dependency_bump_changelog_template,
784 Some("Updated dependency `{dependency}` to v{version}".to_string())
785 );
786 }
787
788 #[test]
789 fn default_config_includes_none_bump_behavior() {
790 use changeset_manifest::NoneBumpBehavior;
791
792 let context = ProjectContext {
793 is_single_package: true,
794 };
795 let config = build_default_config(context);
796 assert_eq!(config.none_bump_behavior, Some(NoneBumpBehavior::default()));
797 assert!(config.none_bump_promote_message_template.is_none());
798 }
799
800 #[test]
801 fn none_bump_fields_propagate_from_version_settings_input() {
802 use changeset_manifest::NoneBumpBehavior;
803
804 let context = ProjectContext {
805 is_single_package: true,
806 };
807
808 let input = InitInputBuilder::default()
809 .version_config(Some(VersionSettingsInput {
810 zero_version_behavior: Some(ZeroVersionBehavior::default()),
811 none_bump_behavior: Some(NoneBumpBehavior::Disallow),
812 none_bump_promote_message_template: Some("Custom message".to_string()),
813 }))
814 .build()
815 .expect("all fields have defaults");
816
817 let config = build_config_from_input(&input, context);
818 assert_eq!(config.none_bump_behavior, Some(NoneBumpBehavior::Disallow));
819 assert_eq!(
820 config.none_bump_promote_message_template,
821 Some("Custom message".to_string())
822 );
823 }
824
825 #[test]
826 fn none_bump_fields_default_when_no_version_config() {
827 let context = ProjectContext {
828 is_single_package: true,
829 };
830
831 let input = InitInput::default();
832
833 let config = build_config_from_input(&input, context);
834 assert!(config.none_bump_behavior.is_none());
835 assert!(config.none_bump_promote_message_template.is_none());
836 }
837
838 #[test]
839 fn commit_title_template_propagates_from_git_settings() {
840 let context = ProjectContext {
841 is_single_package: true,
842 };
843
844 let input = InitInputBuilder::default()
845 .git_config(Some(GitSettingsInput {
846 commit_title_template: Some("Release {new-version}".to_string()),
847 ..Default::default()
848 }))
849 .build()
850 .expect("all fields have defaults");
851
852 let config = build_config_from_input(&input, context);
853 assert_eq!(
854 config.commit_title_template,
855 Some("Release {new-version}".to_string())
856 );
857 }
858
859 #[test]
860 fn changes_in_body_propagates_from_git_settings() {
861 let context = ProjectContext {
862 is_single_package: true,
863 };
864
865 let input = InitInputBuilder::default()
866 .git_config(Some(GitSettingsInput {
867 changes_in_body: Some(false),
868 ..Default::default()
869 }))
870 .build()
871 .expect("all fields have defaults");
872
873 let config = build_config_from_input(&input, context);
874 assert_eq!(config.changes_in_body, Some(false));
875 }
876
877 #[test]
878 fn comparison_links_template_propagates_from_changelog_settings() {
879 let context = ProjectContext {
880 is_single_package: true,
881 };
882
883 let input = InitInputBuilder::default()
884 .changelog_config(Some(ChangelogSettingsInput {
885 comparison_links_template: Some(
886 "https://github.com/org/repo/compare/{base}...{target}".to_string(),
887 ),
888 ..Default::default()
889 }))
890 .build()
891 .expect("all fields have defaults");
892
893 let config = build_config_from_input(&input, context);
894 assert_eq!(
895 config.comparison_links_template,
896 Some("https://github.com/org/repo/compare/{base}...{target}".to_string())
897 );
898 }
899
900 #[test]
901 fn dependency_bump_changelog_template_propagates_from_changelog_settings() {
902 let context = ProjectContext {
903 is_single_package: true,
904 };
905
906 let input = InitInputBuilder::default()
907 .changelog_config(Some(ChangelogSettingsInput {
908 dependency_bump_changelog_template: Some(
909 "Updated `{dependency}` to v{version}".to_string(),
910 ),
911 ..Default::default()
912 }))
913 .build()
914 .expect("all fields have defaults");
915
916 let config = build_config_from_input(&input, context);
917 assert_eq!(
918 config.dependency_bump_changelog_template,
919 Some("Updated `{dependency}` to v{version}".to_string())
920 );
921 }
922
923 #[test]
924 fn filtering_config_propagates_ignored_files() {
925 let context = ProjectContext {
926 is_single_package: true,
927 };
928
929 let input = InitInputBuilder::default()
930 .filtering_config(Some(FilteringSettingsInput {
931 ignored_files: vec!["*.lock".to_string(), "docs/**".to_string()],
932 }))
933 .build()
934 .expect("all fields have defaults");
935
936 let config = build_config_from_input(&input, context);
937 assert_eq!(
938 config.ignored_files,
939 Some(vec!["*.lock".to_string(), "docs/**".to_string()])
940 );
941 }
942
943 #[test]
944 fn filtering_config_skips_empty_ignored_files() {
945 let context = ProjectContext {
946 is_single_package: true,
947 };
948
949 let input = InitInputBuilder::default()
950 .filtering_config(Some(FilteringSettingsInput {
951 ignored_files: vec![],
952 }))
953 .build()
954 .expect("all fields have defaults");
955
956 let config = build_config_from_input(&input, context);
957 assert!(config.ignored_files.is_none());
958 }
959
960 #[test]
961 fn interactive_mode_collects_filtering_settings() {
962 let dir = tempfile::tempdir().expect("create temp dir");
963 let changeset_dir = dir.path().join(".changeset");
964 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
965
966 let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
967 .with_changeset_dir(changeset_dir.clone());
968 let manifest_writer = Arc::new(MockManifestWriter::new());
969 let interaction_provider = Arc::new(
970 MockInitInteractionProvider::new()
971 .with_git_settings(Some(GitSettingsInput::default()))
972 .with_changelog_settings(None)
973 .with_version_settings(None)
974 .with_filtering_settings(Some(FilteringSettingsInput {
975 ignored_files: vec!["*.lock".to_string()],
976 })),
977 );
978
979 let operation = InitOperation::new(project_provider)
980 .with_manifest_writer(Arc::clone(&manifest_writer))
981 .with_interaction_provider(Arc::clone(&interaction_provider));
982
983 let input = InitInput::default();
984
985 let result = operation
986 .execute(Path::new("/any"), &input)
987 .expect("InitOperation failed");
988
989 assert!(result.wrote_config());
990
991 let written = manifest_writer.written_metadata();
992 assert_eq!(written.len(), 1);
993 let (_, _, config) = &written[0];
994 assert_eq!(config.ignored_files, Some(vec!["*.lock".to_string()]));
995 }
996
997 #[test]
998 fn base_branch_propagates_from_git_settings_input() {
999 let dir = tempfile::tempdir().expect("create temp dir");
1000 let changeset_dir = dir.path().join(".changeset");
1001 std::fs::create_dir_all(&changeset_dir).expect("create changeset dir");
1002
1003 let project_provider = MockProjectProvider::single_package("my-crate", "1.0.0")
1004 .with_changeset_dir(changeset_dir.clone());
1005 let manifest_writer = Arc::new(MockManifestWriter::new());
1006 let interaction_provider = Arc::new(MockInitInteractionProvider::new());
1007
1008 let operation = InitOperation::new(project_provider)
1009 .with_manifest_writer(Arc::clone(&manifest_writer))
1010 .with_interaction_provider(Arc::clone(&interaction_provider));
1011
1012 let input = InitInputBuilder::default()
1013 .git_config(Some(GitSettingsInput {
1014 commit: true,
1015 tags: true,
1016 keep_changesets: false,
1017 tag_format: TagFormat::VersionOnly,
1018 base_branch: String::from("develop"),
1019 ..Default::default()
1020 }))
1021 .build()
1022 .expect("all fields have defaults");
1023
1024 let _ = operation
1025 .execute(Path::new("/any"), &input)
1026 .expect("InitOperation failed");
1027
1028 let written = manifest_writer.written_metadata();
1029 assert_eq!(written.len(), 1);
1030 let (_, _, config) = &written[0];
1031 assert_eq!(config.base_branch, Some(String::from("develop")));
1032 }
1033}