commit_wizard/engine/models/policy/
commit.rs1use crate::engine::{
2 constants::DEFAULT_COMMIT_BUMP,
3 models::{
4 policy::enforcement::{BumpLevel, EmojiMode, ScopeMode, TicketSource},
5 runtime::ResolvedConfig,
6 },
7};
8use regex::Regex;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ScopeRequirement {
12 Disabled,
13 Optional,
14 Required,
15}
16
17impl From<ScopeMode> for ScopeRequirement {
18 fn from(value: ScopeMode) -> Self {
19 match value {
20 ScopeMode::Disabled => Self::Disabled,
21 ScopeMode::Optional => Self::Optional,
22 ScopeMode::Required => Self::Required,
23 }
24 }
25}
26
27#[derive(Debug, Clone)]
28pub struct CommitTypeModel {
29 pub key: String,
30 pub emoji: Option<String>,
31 pub description: Option<String>,
32 pub bump: BumpLevel,
33 pub section: String,
34}
35
36#[derive(Debug, Clone)]
37pub struct CommitProtectedModel {
38 pub allow: bool,
39 pub force: bool,
40 pub warn: bool,
41}
42
43#[derive(Debug, Clone)]
44pub struct TicketPolicy {
45 pub enabled: bool,
46 pub required: bool,
47 pub regex: Option<String>,
48 pub source: TicketSource,
49}
50
51#[derive(Debug, Clone)]
52pub struct HeaderFormatPolicy {
53 pub require_scope: bool,
54 pub allow_breaking_bang: bool,
55}
56
57#[derive(Debug, Clone)]
58pub struct CommitModel {
59 pub subject_max_length: u32,
60 pub use_emojis: bool,
61 pub types: Vec<CommitTypeModel>,
62
63 pub scope_requirement: ScopeRequirement,
64 pub restrict_scopes_to_defined: bool,
65 pub allowed_scopes: Option<Vec<String>>,
66
67 pub require_conventional: bool,
68 pub breaking_header_required: bool,
69 pub breaking_footer_required: bool,
70 pub breaking_footer_key: String,
71 pub breaking_footer_keys: Vec<String>,
72 pub breaking_emoji: Option<String>,
73 pub breaking_emoji_mode: EmojiMode,
74
75 pub ticket: TicketPolicy,
76 pub header_format: HeaderFormatPolicy,
77
78 pub protected: CommitProtectedModel,
79}
80
81impl Default for CommitModel {
82 fn default() -> Self {
83 Self {
84 subject_max_length: 72,
85 use_emojis: false,
86 types: vec![
87 CommitTypeModel {
88 key: "feat".to_string(),
89 emoji: Some("✨".to_string()),
90 description: Some("A new feature".to_string()),
91 bump: BumpLevel::Minor,
92 section: "Features".to_string(),
93 },
94 CommitTypeModel {
95 key: "fix".to_string(),
96 emoji: Some("🐛".to_string()),
97 description: Some("A bug fix".to_string()),
98 bump: BumpLevel::Patch,
99 section: "Bug Fixes".to_string(),
100 },
101 ],
102 scope_requirement: ScopeRequirement::Optional,
103 restrict_scopes_to_defined: false,
104 allowed_scopes: None,
105 require_conventional: true,
106 breaking_header_required: false,
107 breaking_footer_required: false,
108 breaking_footer_key: "BREAKING CHANGE".to_string(),
109 breaking_footer_keys: vec![
110 "BREAKING CHANGE".to_string(),
111 "BREAKING-CHANGE".to_string(),
112 ],
113 breaking_emoji: None,
114 breaking_emoji_mode: EmojiMode::Prepend,
115 ticket: TicketPolicy {
116 enabled: false,
117 required: false,
118 regex: None,
119 source: TicketSource::Disabled,
120 },
121 header_format: HeaderFormatPolicy {
122 require_scope: false,
123 allow_breaking_bang: true,
124 },
125 protected: CommitProtectedModel {
126 allow: true,
127 force: false,
128 warn: true,
129 },
130 }
131 }
132}
133
134impl CommitModel {
135 pub fn from_config(config: &ResolvedConfig) -> Self {
136 let base = &config.base;
137
138 let types_map = base.commit_types();
139
140 let types = types_map
141 .into_iter()
142 .map(|(key, value)| CommitTypeModel {
143 key,
144 emoji: value.emoji,
145 description: value.description,
146 bump: value.bump.unwrap_or(DEFAULT_COMMIT_BUMP),
147 section: value.section.unwrap_or_else(|| "Miscellaneous".to_string()),
148 })
149 .collect();
150
151 let allowed_scopes_vec = base.commit_scope_allowed();
152 let allowed_scopes = if allowed_scopes_vec.is_empty() {
153 None
154 } else {
155 Some(allowed_scopes_vec)
156 };
157
158 Self {
159 subject_max_length: base.commit_subject_max_length(),
160 use_emojis: base.commit_use_emojis(),
161
162 types,
163
164 scope_requirement: base.commit_scopes_mode().into(),
165 restrict_scopes_to_defined: base.commit_scope_restrict_to_defined(),
166 allowed_scopes,
167
168 require_conventional: base.check_require_conventional(),
169 breaking_header_required: base.commit_breaking_require_header(),
170 breaking_footer_required: base.commit_breaking_require_footer(),
171 breaking_footer_key: base.commit_breaking_footer_key(),
172 breaking_footer_keys: base.commit_breaking_footer_keys_normalized(),
173 breaking_emoji: base.commit_breaking_emoji(),
174 breaking_emoji_mode: base.commit_breaking_emoji_mode(),
175
176 ticket: TicketPolicy {
177 enabled: base.commit_ticket_required(),
178 required: base.commit_ticket_required(),
179 regex: base.commit_ticket_pattern(),
180 source: base.commit_ticket_source(),
181 },
182
183 header_format: HeaderFormatPolicy {
184 require_scope: matches!(base.commit_scopes_mode(), ScopeMode::Required),
185 allow_breaking_bang: true,
186 },
187
188 protected: CommitProtectedModel {
189 allow: base.commit_protected_allow(),
190 force: base.commit_protected_force(),
191 warn: base.commit_protected_warn(),
192 },
193 }
194 }
195
196 pub fn allows_type(&self, value: &str) -> bool {
197 self.types.iter().any(|t| t.key == value)
198 }
199
200 pub fn find_type(&self, value: &str) -> Option<&CommitTypeModel> {
201 self.types.iter().find(|t| t.key == value)
202 }
203
204 pub fn allows_scope(&self, value: &str) -> bool {
205 match &self.allowed_scopes {
206 Some(scopes) => scopes.iter().any(|s| s == value),
207 None => true,
208 }
209 }
210}
211
212pub fn is_valid_conventional_commit_message(message: &str, allowed_types: &[&str]) -> bool {
214 let pattern = format!(r"^({})(\([\w\-]+\))?(!)?: .+", allowed_types.join("|"));
215 Regex::new(&pattern).unwrap().is_match(message.trim())
216}