cargo-governor 2.0.3

Machine-First, LLM-Ready, CI/CD-Native release automation tool for Rust crates
Documentation
//! Commit analysis for version determination

use governor_core::domain::{
    commit::{Commit, CommitType},
    version::{BreakingChange, Feature, Fix, MigrationComplexity},
};

/// Analysis results from commits
#[derive(Debug, Default)]
pub struct Analysis {
    pub breaking: Vec<BreakingCommit>,
    pub features: Vec<FeatureCommit>,
    pub fixes: Vec<FixCommit>,
    pub other: usize,
}

/// A breaking commit
#[derive(Debug, Clone)]
pub struct BreakingCommit {
    pub hash: String,
    pub short_hash: String,
    pub message: String,
    pub description: Option<String>,
    pub has_exclamation: bool,
}

/// A feature commit
#[derive(Debug, Clone)]
pub struct FeatureCommit {
    pub hash: String,
    pub short_hash: String,
    pub message: String,
    pub scope: Option<String>,
}

/// A fix commit
#[derive(Debug, Clone)]
pub struct FixCommit {
    pub hash: String,
    pub short_hash: String,
    pub message: String,
    pub scope: Option<String>,
}

/// Analyze commits and categorize them
#[must_use]
pub fn analyze_commits(commits: &[Commit]) -> Analysis {
    let mut analysis = Analysis::default();

    for commit in commits {
        let short_hash = commit.hash.chars().take(7).collect::<String>();

        match commit.commit_type {
            Some(CommitType::Feat) => {
                if commit.breaking {
                    analysis.breaking.push(BreakingCommit {
                        hash: commit.hash.clone(),
                        short_hash,
                        message: commit.short_message().to_string(),
                        description: Some("Breaking feature change".to_string()),
                        has_exclamation: commit.message.contains('!'),
                    });
                } else {
                    analysis.features.push(FeatureCommit {
                        hash: commit.hash.clone(),
                        short_hash,
                        message: commit.short_message().to_string(),
                        scope: commit.scope.clone(),
                    });
                }
            }
            Some(CommitType::Fix) => {
                if commit.breaking {
                    analysis.breaking.push(BreakingCommit {
                        hash: commit.hash.clone(),
                        short_hash,
                        message: commit.short_message().to_string(),
                        description: Some("Breaking fix".to_string()),
                        has_exclamation: commit.message.contains('!'),
                    });
                } else {
                    analysis.fixes.push(FixCommit {
                        hash: commit.hash.clone(),
                        short_hash,
                        message: commit.short_message().to_string(),
                        scope: commit.scope.clone(),
                    });
                }
            }
            Some(CommitType::Perf | CommitType::Refactor) => {
                if commit.breaking {
                    analysis.breaking.push(BreakingCommit {
                        hash: commit.hash.clone(),
                        short_hash,
                        message: commit.short_message().to_string(),
                        description: Some("Breaking refactor/perf change".to_string()),
                        has_exclamation: commit.message.contains('!'),
                    });
                }
            }
            _ => {
                analysis.other += 1;
            }
        }
    }

    analysis
}

/// Build breaking changes response
#[must_use]
pub fn build_breaking_changes(
    breaking: Vec<BreakingCommit>,
    package_name: &str,
) -> Vec<BreakingChange> {
    breaking
        .into_iter()
        .map(|c| BreakingChange {
            commit_hash: c.hash.clone(),
            short_hash: c.short_hash.clone(),
            message: c.message.clone(),
            breaking_description: c
                .description
                .unwrap_or_else(|| "Breaking change detected".to_string()),
            affected_crates: vec![package_name.to_string()],
            migration_complexity: if c.has_exclamation {
                MigrationComplexity::Complex
            } else {
                MigrationComplexity::Simple
            },
        })
        .collect()
}

/// Build features response
#[must_use]
pub fn build_features(features: Vec<FeatureCommit>, _package_name: &str) -> Vec<Feature> {
    features
        .into_iter()
        .map(|c| Feature {
            commit_hash: c.hash.clone(),
            short_hash: c.short_hash.clone(),
            message: c.message.clone(),
            scope: c.scope,
            affected_crates: Vec::new(),
        })
        .collect()
}

/// Build fixes response
#[must_use]
pub fn build_fixes(fixes: Vec<FixCommit>, _package_name: &str) -> Vec<Fix> {
    fixes
        .into_iter()
        .map(|c| Fix {
            commit_hash: c.hash.clone(),
            short_hash: c.short_hash.clone(),
            message: c.message.clone(),
            scope: c.scope,
            affected_crates: Vec::new(),
        })
        .collect()
}