Skip to main content

commit_wizard/engine/capabilities/versioning/
mod.rs

1use crate::engine::capabilities::commit::check::ParsedHeader;
2use crate::engine::models::{
3    git::CommitSummary,
4    policy::{commit::CommitModel, enforcement::BumpLevel, versioning::Version},
5};
6
7/// Classify commits into their bump levels based on commit type and policy.
8/// Implements the versioning algorithm from SRS §5.2:
9/// - breaking (has ! or BREAKING CHANGE:) → major
10/// - feat → minor
11/// - others → patch
12pub fn classify_commits(
13    commits: &[CommitSummary],
14    policy: &CommitModel,
15) -> Vec<(CommitSummary, BumpLevel)> {
16    commits
17        .iter()
18        .map(|commit| {
19            let bump = classify_single_commit(commit, policy);
20            (commit.clone(), bump)
21        })
22        .collect()
23}
24
25/// Classify a single commit into its bump level.
26fn classify_single_commit(commit: &CommitSummary, policy: &CommitModel) -> BumpLevel {
27    // Parse the commit header
28    let header = &commit.summary;
29
30    // Try to parse as conventional commit
31    if let Some(parsed) = ParsedHeader::parse(header) {
32        // Check for breaking changes in body/footer
33        let is_breaking_footer = commit
34            .full_message
35            .as_ref()
36            .map(|msg| msg.contains(&format!("{}:", policy.breaking_footer_key)))
37            .unwrap_or(false);
38
39        if parsed.is_breaking || is_breaking_footer {
40            return BumpLevel::Major;
41        }
42
43        // Find the commit type and get its bump level
44        if let Some(type_model) = policy.find_type(&parsed.type_name) {
45            return type_model.bump;
46        }
47
48        // Type not found - return default
49        return BumpLevel::None;
50    }
51
52    // Parsing failed - return None
53    BumpLevel::None
54}
55
56/// Calculate the next version based on commits since last tag.
57///
58/// Algorithm (SRS §5.3):
59/// 1. Get current version from last tag
60/// 2. Classify commits to find highest bump
61/// 3. Increment version using highest bump
62/// 4. Handle edge cases: no tag → use initial version (0.1.0)
63pub fn calculate_next_version(
64    current_version: Option<Version>,
65    classified_commits: &[(CommitSummary, BumpLevel)],
66) -> Version {
67    // Determine the highest bump level across all commits
68    let highest_bump = classified_commits
69        .iter()
70        .map(|(_, bump)| bump)
71        .max_by_key(|bump| match bump {
72            BumpLevel::Major => 3,
73            BumpLevel::Minor => 2,
74            BumpLevel::Patch => 1,
75            BumpLevel::None => 0,
76        })
77        .copied()
78        .unwrap_or(BumpLevel::None);
79
80    // Use current version if available, otherwise start from initial (0.1.0)
81    let base_version = current_version.unwrap_or(Version {
82        major: 0,
83        minor: 1,
84        patch: 0,
85    });
86
87    // Apply bump to the base version
88    base_version.with_bump(highest_bump)
89}