Skip to main content

commit_wizard/engine/models/policy/
commit.rs

1use 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
212/// Validates a commit message against conventional commits.
213pub 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}