commit_wizard/engine/config/base/
mod.rs1pub mod types;
2pub use types::*;
3
4use std::collections::{BTreeMap, HashMap};
5
6use crate::engine::{
7 constants::{
8 CONFIG_VERSION, defaults, full_base_config, minimal_base_config, standard_base_config,
9 },
10 models::policy::enforcement::AiProvider,
11};
12
13#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct BaseConfig {
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub commit: Option<CommitConfig>,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub branch: Option<BranchConfig>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub pr: Option<PrConfig>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub check: Option<CheckConfig>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub push: Option<PushConfig>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub versioning: Option<VersioningConfig>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub changelog: Option<ChangelogConfig>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub release: Option<ReleaseConfig>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub hooks: Option<HooksConfig>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub ai: Option<AiConfig>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub registry: Option<RegistryConfig>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub registries: Option<HashMap<String, NamedRegistryConfig>>,
40}
41
42impl BaseConfig {
43 pub fn empty() -> Self {
44 Self {
45 commit: None,
46 branch: None,
47 pr: None,
48 check: None,
49 push: None,
50 versioning: None,
51 changelog: None,
52 release: None,
53 hooks: None,
54 ai: None,
55 registry: None,
56 registries: None,
57 }
58 }
59
60 pub fn merge(self, lower: BaseConfig) -> BaseConfig {
63 BaseConfig {
64 commit: merge_commit_config(self.commit, lower.commit),
65 branch: self.branch.or(lower.branch),
66 pr: self.pr.or(lower.pr),
67 check: self.check.or(lower.check),
68 push: self.push.or(lower.push),
69 versioning: self.versioning.or(lower.versioning),
70 changelog: self.changelog.or(lower.changelog),
71 release: self.release.or(lower.release),
72 hooks: self.hooks.or(lower.hooks),
73 ai: self.ai.or(lower.ai),
74 registry: self.registry.or(lower.registry),
75 registries: self.registries.or(lower.registries),
76 }
77 }
78
79 pub fn minimal() -> Self {
80 minimal_base_config()
81 }
82
83 pub fn standard() -> Self {
84 standard_base_config()
85 }
86
87 pub fn full() -> Self {
88 full_base_config()
89 }
90
91 pub fn version(&self) -> u32 {
92 CONFIG_VERSION
93 }
94
95 pub fn commit_subject_max_length(&self) -> u32 {
100 self.commit
101 .as_ref()
102 .and_then(|c| c.subject_max_length)
103 .unwrap_or(defaults::COMMIT_SUBJECT_MAX_LENGTH)
104 }
105
106 pub fn commit_use_emojis(&self) -> bool {
107 self.commit
108 .as_ref()
109 .and_then(|c| c.use_emojis)
110 .unwrap_or(defaults::COMMIT_USE_EMOJIS)
111 }
112
113 pub fn commit_types(&self) -> BTreeMap<String, CommitTypeConfig> {
114 self.commit
115 .as_ref()
116 .and_then(|c| c.types.clone())
117 .unwrap_or_else(defaults::default_commit_types)
118 }
119
120 pub fn commit_scopes_mode(&self) -> crate::engine::models::policy::enforcement::ScopeMode {
121 self.commit
122 .as_ref()
123 .and_then(|c| c.scopes.as_ref())
124 .and_then(|s| s.mode)
125 .unwrap_or(defaults::COMMIT_SCOPE_MODE)
126 }
127
128 pub fn commit_scope_restrict_to_defined(&self) -> bool {
129 self.commit
130 .as_ref()
131 .and_then(|c| c.scopes.as_ref())
132 .and_then(|s| s.restrict_to_defined)
133 .unwrap_or(defaults::COMMIT_SCOPE_RESTRICT_TO_DEFINED)
134 }
135
136 pub fn commit_scope_allowed(&self) -> Vec<String> {
137 self.commit
138 .as_ref()
139 .and_then(|c| c.scopes.as_ref())
140 .and_then(|s| {
141 s.definitions
142 .clone()
143 .map(|defs| defs.keys().cloned().collect())
144 })
145 .unwrap_or_else(defaults::default_commit_allowed_scopes)
146 }
147
148 pub fn commit_breaking_require_header(&self) -> bool {
149 self.commit
150 .as_ref()
151 .and_then(|c| c.breaking.as_ref())
152 .and_then(|b| b.require_header)
153 .unwrap_or(defaults::COMMIT_BREAKING_REQUIRE_HEADER)
154 }
155
156 pub fn commit_breaking_require_footer(&self) -> bool {
157 self.commit
158 .as_ref()
159 .and_then(|c| c.breaking.as_ref())
160 .and_then(|b| b.require_footer)
161 .unwrap_or(defaults::COMMIT_BREAKING_REQUIRE_FOOTER)
162 }
163
164 pub fn commit_breaking_footer_key(&self) -> String {
165 self.commit
166 .as_ref()
167 .and_then(|c| c.breaking.as_ref())
168 .and_then(|b| b.footer_key.clone())
169 .unwrap_or_else(|| defaults::COMMIT_BREAKING_FOOTER_KEY.to_string())
170 }
171
172 pub fn commit_breaking_footer_keys_normalized(&self) -> Vec<String> {
173 let breaking = self.commit.as_ref().and_then(|c| c.breaking.as_ref());
174
175 if let Some(b) = breaking {
176 let mut keys = Vec::new();
177
178 if let Some(ref key) = b.footer_key
180 && !key.is_empty()
181 {
182 keys.push(key.clone());
183 }
184
185 if let Some(ref keys_list) = b.footer_keys {
187 for key in keys_list {
188 if !key.is_empty() && !keys.contains(key) {
189 keys.push(key.clone());
190 }
191 }
192 }
193
194 if !keys.is_empty() {
195 return keys;
196 }
197 }
198
199 vec!["BREAKING CHANGE".to_string(), "BREAKING-CHANGE".to_string()]
201 }
202
203 pub fn commit_breaking_emoji(&self) -> Option<String> {
204 self.commit
205 .as_ref()
206 .and_then(|c| c.breaking.as_ref())
207 .and_then(|b| b.emoji.clone())
208 }
209
210 pub fn commit_breaking_emoji_mode(
211 &self,
212 ) -> crate::engine::models::policy::enforcement::EmojiMode {
213 self.commit
214 .as_ref()
215 .and_then(|c| c.breaking.as_ref())
216 .and_then(|b| b.emoji_mode)
217 .unwrap_or_default()
218 }
219
220 pub fn commit_ticket_required(&self) -> bool {
221 self.commit
222 .as_ref()
223 .and_then(|c| c.ticket.as_ref())
224 .and_then(|t| t.required)
225 .unwrap_or(defaults::COMMIT_TICKET_REQUIRED)
226 }
227
228 pub fn commit_ticket_pattern(&self) -> Option<String> {
229 self.commit
230 .as_ref()
231 .and_then(|c| c.ticket.as_ref())
232 .and_then(|t| t.pattern.clone())
233 }
234
235 pub fn commit_ticket_source(&self) -> crate::engine::models::policy::enforcement::TicketSource {
236 self.commit
237 .as_ref()
238 .and_then(|c| c.ticket.as_ref())
239 .and_then(|t| t.source)
240 .unwrap_or(defaults::COMMIT_TICKET_SOURCE)
241 }
242
243 pub fn commit_protected_allow(&self) -> bool {
244 self.commit
245 .as_ref()
246 .and_then(|c| c.protected.as_ref())
247 .and_then(|p| p.allow)
248 .unwrap_or(defaults::COMMIT_PROTECTED_ALLOW)
249 }
250
251 pub fn commit_protected_force(&self) -> bool {
252 self.commit
253 .as_ref()
254 .and_then(|c| c.protected.as_ref())
255 .and_then(|p| p.force)
256 .unwrap_or(defaults::COMMIT_PROTECTED_FORCE)
257 }
258
259 pub fn commit_protected_warn(&self) -> bool {
260 self.commit
261 .as_ref()
262 .and_then(|c| c.protected.as_ref())
263 .and_then(|p| p.warn)
264 .unwrap_or(defaults::COMMIT_PROTECTED_WARN)
265 }
266
267 pub fn branch_remote(&self) -> String {
272 self.branch
273 .as_ref()
274 .and_then(|b| b.remote.clone())
275 .unwrap_or_else(|| defaults::BRANCH_REMOTE.to_string())
276 }
277
278 pub fn branch_protected_patterns(&self) -> Vec<String> {
279 self.branch
280 .as_ref()
281 .and_then(|b| b.protected.clone())
282 .unwrap_or_else(defaults::default_branch_protected_patterns)
283 }
284
285 pub fn branch_naming_pattern(&self) -> String {
286 self.branch
287 .as_ref()
288 .and_then(|b| b.naming.as_ref())
289 .and_then(|n| n.pattern.clone())
290 .unwrap_or_else(|| defaults::BRANCH_NAMING_PATTERN.to_string())
291 }
292
293 pub fn branch_naming_enforce(&self) -> bool {
294 defaults::BRANCH_NAMING_ENFORCE
295 }
296
297 pub fn branch_allowed_targets(&self) -> Vec<String> {
298 defaults::default_branch_allowed_targets()
299 }
300
301 pub fn pr_enabled(&self) -> bool {
306 self.pr
307 .as_ref()
308 .and_then(|p| p.enabled)
309 .unwrap_or(defaults::PR_ENABLED)
310 }
311
312 pub fn check_require_conventional(&self) -> bool {
317 self.check
318 .as_ref()
319 .and_then(|c| c.require_conventional)
320 .unwrap_or(defaults::CHECK_REQUIRE_CONVENTIONAL)
321 }
322
323 pub fn check_commits_enabled(&self) -> bool {
324 self.check
325 .as_ref()
326 .and_then(|c| c.commits.as_ref())
327 .and_then(|cc| cc.enabled)
328 .unwrap_or(defaults::CHECK_COMMITS_ENABLED)
329 }
330
331 pub fn check_commits_enforce_on(
332 &self,
333 ) -> crate::engine::models::policy::enforcement::CommitEnforcementScope {
334 self.check
335 .as_ref()
336 .and_then(|c| c.commits.as_ref())
337 .and_then(|cc| cc.enforce_on)
338 .unwrap_or(defaults::CHECK_COMMITS_ENFORCE_ON)
339 }
340
341 pub fn push_allow_protected(&self) -> bool {
346 self.push
347 .as_ref()
348 .and_then(|p| p.allow.as_ref())
349 .and_then(|a| a.protected)
350 .unwrap_or(defaults::PUSH_ALLOW_PROTECTED)
351 }
352
353 pub fn push_allow_force(&self) -> bool {
354 self.push
355 .as_ref()
356 .and_then(|p| p.allow.as_ref())
357 .and_then(|a| a.force)
358 .unwrap_or(defaults::PUSH_ALLOW_FORCE)
359 }
360
361 pub fn push_check_commits(&self) -> bool {
362 self.push
363 .as_ref()
364 .and_then(|p| p.check.as_ref())
365 .and_then(|c| c.commits)
366 .unwrap_or(defaults::PUSH_CHECK_COMMITS)
367 }
368
369 pub fn push_check_branch_policy(&self) -> bool {
370 self.push
371 .as_ref()
372 .and_then(|p| p.check.as_ref())
373 .and_then(|c| c.branch_policy)
374 .unwrap_or(defaults::PUSH_CHECK_BRANCH_POLICY)
375 }
376
377 pub fn versioning_tag_prefix(&self) -> String {
382 self.versioning
383 .as_ref()
384 .and_then(|v| v.tag_prefix.clone())
385 .unwrap_or_else(|| defaults::VERSIONING_TAG_PREFIX.to_string())
386 }
387
388 pub fn changelog_output(&self) -> String {
393 self.changelog
394 .as_ref()
395 .and_then(|c| c.output.clone())
396 .unwrap_or_else(|| defaults::CHANGELOG_OUTPUT.to_string())
397 }
398
399 pub fn changelog_format(&self) -> crate::engine::models::policy::enforcement::ChangelogFormat {
400 self.changelog
401 .as_ref()
402 .and_then(|c| c.format)
403 .unwrap_or(defaults::CHANGELOG_FORMAT)
404 }
405
406 pub fn changelog_group_by(&self) -> Vec<String> {
407 self.changelog
408 .as_ref()
409 .and_then(|c| c.layout.as_ref())
410 .and_then(|l| l.group_by.clone())
411 .unwrap_or_else(defaults::default_changelog_group_by)
412 }
413
414 pub fn changelog_section_order(&self) -> Vec<String> {
415 self.changelog
416 .as_ref()
417 .and_then(|c| c.layout.as_ref())
418 .and_then(|l| l.section_order.clone())
419 .unwrap_or_else(defaults::default_changelog_section_order)
420 }
421
422 pub fn changelog_scope_order(&self) -> Vec<String> {
423 self.changelog
424 .as_ref()
425 .and_then(|c| c.layout.as_ref())
426 .and_then(|l| l.scope_order.clone())
427 .unwrap_or_else(defaults::default_changelog_scope_order)
428 }
429
430 pub fn changelog_show_scope(&self) -> bool {
431 self.changelog
432 .as_ref()
433 .and_then(|c| c.layout.as_ref())
434 .and_then(|l| l.show_scope)
435 .unwrap_or(defaults::CHANGELOG_SHOW_SCOPE)
436 }
437
438 pub fn changelog_show_empty_sections(&self) -> Option<bool> {
439 Some(
440 self.changelog
441 .as_ref()
442 .and_then(|c| c.layout.as_ref())
443 .and_then(|l| l.show_empty_sections)
444 .unwrap_or(defaults::CHANGELOG_SHOW_EMPTY_SECTIONS),
445 )
446 }
447
448 pub fn changelog_show_empty_scopes(&self) -> Option<bool> {
449 Some(
450 self.changelog
451 .as_ref()
452 .and_then(|c| c.layout.as_ref())
453 .and_then(|l| l.show_empty_scopes)
454 .unwrap_or(defaults::CHANGELOG_SHOW_EMPTY_SCOPES),
455 )
456 }
457
458 pub fn changelog_misc_section(&self) -> Option<String> {
459 Some(
460 self.changelog
461 .as_ref()
462 .and_then(|c| c.layout.as_ref())
463 .and_then(|l| l.misc_section.clone())
464 .unwrap_or_else(|| defaults::CHANGELOG_MISC_SECTION.to_string()),
465 )
466 }
467
468 pub fn changelog_header_use(&self) -> bool {
469 self.changelog
470 .as_ref()
471 .and_then(|c| c.header.as_ref())
472 .and_then(|h| h.use_header)
473 .unwrap_or(true)
474 }
475
476 pub fn changelog_header_title(&self) -> String {
477 self.changelog
478 .as_ref()
479 .and_then(|c| c.header.as_ref())
480 .and_then(|h| h.title.clone())
481 .unwrap_or_else(|| "Changelog".to_string())
482 }
483
484 pub fn changelog_header_description(&self) -> Option<String> {
485 self.changelog
486 .as_ref()
487 .and_then(|c| c.header.as_ref())
488 .and_then(|h| h.description.clone())
489 }
490
491 pub fn changelog_sections(
492 &self,
493 ) -> std::collections::BTreeMap<String, crate::engine::config::base::ChangelogSectionConfig>
494 {
495 self.changelog
496 .as_ref()
497 .and_then(|c| c.sections.clone())
498 .unwrap_or_default()
499 }
500
501 pub fn changelog_unreleased_label(&self) -> String {
502 self.changelog
503 .as_ref()
504 .and_then(|c| c.layout.as_ref())
505 .and_then(|l| l.unreleased_label.clone())
506 .unwrap_or_else(|| "Unreleased".to_string())
507 }
508
509 pub fn changelog_date_format(&self) -> Option<String> {
510 self.changelog
511 .as_ref()
512 .and_then(|c| c.layout.as_ref())
513 .and_then(|l| l.date_format.clone())
514 }
515
516 pub fn release_enabled(&self) -> bool {
521 self.release
522 .as_ref()
523 .and_then(|r| r.enabled)
524 .unwrap_or(defaults::RELEASE_ENABLED)
525 }
526
527 pub fn release_source_branch(&self) -> String {
528 self.release
529 .as_ref()
530 .and_then(|r| r.source_branch.clone())
531 .unwrap_or_else(|| defaults::RELEASE_SOURCE_BRANCH.to_string())
532 }
533
534 pub fn release_target_branch(&self) -> String {
535 self.release
536 .as_ref()
537 .and_then(|r| r.target_branch.clone())
538 .unwrap_or_else(|| defaults::RELEASE_TARGET_BRANCH.to_string())
539 }
540
541 pub fn release_branch_format(&self) -> String {
542 self.release
543 .as_ref()
544 .and_then(|r| r.branch_format.clone())
545 .unwrap_or_else(|| defaults::RELEASE_BRANCH_FORMAT.to_string())
546 }
547
548 pub fn release_hotfix_pattern(&self) -> String {
549 self.release
550 .as_ref()
551 .and_then(|r| r.hotfix_pattern.clone())
552 .unwrap_or_else(|| defaults::RELEASE_HOTFIX_PATTERN.to_string())
553 }
554
555 pub fn release_require_clean_worktree(&self) -> bool {
556 self.release
557 .as_ref()
558 .and_then(|r| r.validation.as_ref())
559 .and_then(|v| v.require_clean_worktree)
560 .unwrap_or(defaults::RELEASE_REQUIRE_CLEAN_WORKTREE)
561 }
562
563 pub fn release_fail_if_tag_exists(&self) -> bool {
564 self.release
565 .as_ref()
566 .and_then(|r| r.validation.as_ref())
567 .and_then(|v| v.fail_if_tag_exists)
568 .unwrap_or(defaults::RELEASE_FAIL_IF_TAG_EXISTS)
569 }
570
571 pub fn release_fail_if_release_branch_exists(&self) -> bool {
572 self.release
573 .as_ref()
574 .and_then(|r| r.validation.as_ref())
575 .and_then(|v| v.fail_if_release_branch_exists)
576 .unwrap_or(defaults::RELEASE_FAIL_IF_RELEASE_BRANCH_EXISTS)
577 }
578
579 pub fn release_finish_tag(&self) -> bool {
580 self.release
581 .as_ref()
582 .and_then(|r| r.finish.as_ref())
583 .and_then(|f| f.tag)
584 .unwrap_or(defaults::RELEASE_FINISH_TAG)
585 }
586
587 pub fn release_finish_push(&self) -> bool {
588 self.release
589 .as_ref()
590 .and_then(|r| r.finish.as_ref())
591 .and_then(|f| f.push)
592 .unwrap_or(defaults::RELEASE_FINISH_PUSH)
593 }
594
595 pub fn release_finish_backmerge_branch(&self) -> String {
596 self.release
597 .as_ref()
598 .and_then(|r| r.finish.as_ref())
599 .and_then(|f| f.backmerge_branch.clone())
600 .unwrap_or_else(|| defaults::RELEASE_FINISH_BACKMERGE_BRANCH.to_string())
601 }
602
603 pub fn hooks_pre_commit(&self) -> bool {
608 self.hooks
609 .as_ref()
610 .and_then(|h| h.pre_commit)
611 .unwrap_or(defaults::HOOKS_PRE_COMMIT)
612 }
613
614 pub fn hooks_commit_msg(&self) -> bool {
615 self.hooks
616 .as_ref()
617 .and_then(|h| h.commit_msg)
618 .unwrap_or(defaults::HOOKS_COMMIT_MSG)
619 }
620
621 pub fn hooks_pre_push(&self) -> bool {
622 self.hooks
623 .as_ref()
624 .and_then(|h| h.pre_push)
625 .unwrap_or(defaults::HOOKS_PRE_PUSH)
626 }
627
628 pub fn ai_enabled(&self) -> bool {
633 self.ai
634 .as_ref()
635 .and_then(|a| a.enabled)
636 .unwrap_or(defaults::AI_ENABLED)
637 }
638
639 pub fn ai_provider(&self) -> AiProvider {
640 self.ai
641 .as_ref()
642 .and_then(|a| a.provider.clone())
643 .as_deref()
644 .map(AiProvider::from_str)
645 .unwrap_or(defaults::AI_PROVIDER)
646 }
647
648 pub fn ai_commit_enabled(&self) -> bool {
649 self.ai
650 .as_ref()
651 .and_then(|a| a.commands.as_ref())
652 .and_then(|c| c.commit)
653 .unwrap_or(defaults::AI_COMMAND_COMMIT)
654 }
655
656 pub fn ai_changelog_enabled(&self) -> bool {
657 self.ai
658 .as_ref()
659 .and_then(|a| a.commands.as_ref())
660 .and_then(|c| c.changelog)
661 .unwrap_or(defaults::AI_COMMAND_CHANGELOG)
662 }
663
664 pub fn ai_release_prepare_enabled(&self) -> bool {
665 self.ai
666 .as_ref()
667 .and_then(|a| a.commands.as_ref())
668 .and_then(|c| c.release_prepare)
669 .unwrap_or(defaults::AI_COMMAND_RELEASE_PREPARE)
670 }
671
672 pub fn registry_use(&self) -> Option<String> {
677 self.registry
678 .as_ref()
679 .and_then(|r| r.use_registry.clone())
680 .filter(|s| !s.trim().is_empty())
681 }
682
683 pub fn registries_map(&self) -> HashMap<String, NamedRegistryConfig> {
684 self.registries
685 .clone()
686 .unwrap_or_else(defaults::default_registries)
687 }
688}
689
690fn merge_commit_config(
698 high: Option<CommitConfig>,
699 low: Option<CommitConfig>,
700) -> Option<CommitConfig> {
701 match (high, low) {
702 (None, low) => low,
703 (high, None) => high,
704 (Some(h), Some(l)) => Some(CommitConfig {
705 subject_max_length: h.subject_max_length.or(l.subject_max_length),
706 use_emojis: h.use_emojis.or(l.use_emojis),
707 types: h.types.or(l.types),
709 scopes: h.scopes.or(l.scopes),
710 breaking: h.breaking.or(l.breaking),
711 protected: h.protected.or(l.protected),
712 ticket: h.ticket.or(l.ticket),
713 }),
714 }
715}