use std::collections::HashMap;
use std::path::PathBuf;
use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CommitContext {
pub project: ProjectContext,
pub branch: BranchContext,
pub range: CommitRangeContext,
pub files: Vec<FileContext>,
pub user_provided: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProjectContext {
pub commit_guidelines: Option<String>,
pub pr_guidelines: Option<String>,
pub valid_scopes: Vec<ScopeDefinition>,
pub feature_contexts: HashMap<String, FeatureContext>,
pub project_conventions: ProjectConventions,
pub ecosystem: Ecosystem,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScopeDefinition {
pub name: String,
pub description: String,
pub examples: Vec<String>,
pub file_patterns: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureContext {
pub name: String,
pub description: String,
pub scope: String,
pub conventions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProjectConventions {
pub commit_format: Option<String>,
pub required_trailers: Vec<String>,
pub preferred_types: Vec<String>,
pub scope_requirements: ScopeRequirements,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ScopeRequirements {
pub required: bool,
pub valid_scopes: Vec<String>,
pub scope_mapping: HashMap<String, Vec<String>>, }
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum Ecosystem {
#[default]
Unknown,
Rust,
Node,
Python,
Go,
Java,
Generic,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BranchContext {
pub work_type: WorkType,
pub scope: Option<String>,
pub ticket_id: Option<String>,
pub description: String,
pub is_feature_branch: bool,
pub base_branch: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum WorkType {
#[default]
Unknown,
Feature,
Fix,
Docs,
Refactor,
Chore,
Test,
Ci,
Build,
Perf,
}
impl std::str::FromStr for WorkType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"feature" | "feat" => Ok(Self::Feature),
"fix" | "bugfix" => Ok(Self::Fix),
"docs" | "doc" => Ok(Self::Docs),
"refactor" | "refact" => Ok(Self::Refactor),
"chore" => Ok(Self::Chore),
"test" | "tests" => Ok(Self::Test),
"ci" => Ok(Self::Ci),
"build" => Ok(Self::Build),
"perf" | "performance" => Ok(Self::Perf),
_ => Ok(Self::Unknown),
}
}
}
impl std::fmt::Display for WorkType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unknown => write!(f, "unknown work"),
Self::Feature => write!(f, "feature development"),
Self::Fix => write!(f, "bug fix"),
Self::Docs => write!(f, "documentation update"),
Self::Refactor => write!(f, "refactoring"),
Self::Chore => write!(f, "maintenance"),
Self::Test => write!(f, "testing"),
Self::Ci => write!(f, "CI/CD"),
Self::Build => write!(f, "build system"),
Self::Perf => write!(f, "performance improvement"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CommitRangeContext {
pub related_commits: Vec<String>, pub common_files: Vec<PathBuf>,
pub work_pattern: WorkPattern,
pub scope_consistency: ScopeAnalysis,
pub architectural_impact: ArchitecturalImpact,
pub change_significance: ChangeSignificance,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum WorkPattern {
#[default]
Unknown,
Sequential,
Refactoring,
BugHunt,
Documentation,
Configuration,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ScopeAnalysis {
pub consistent_scope: Option<String>,
pub scope_changes: Vec<String>,
pub confidence: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum ArchitecturalImpact {
#[default]
Minimal,
Moderate,
Significant,
Breaking,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum ChangeSignificance {
#[default]
Minor,
Moderate,
Major,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileContext {
pub path: PathBuf,
pub file_purpose: FilePurpose,
pub architectural_layer: ArchitecturalLayer,
pub change_impact: ChangeImpact,
pub project_significance: ProjectSignificance,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FilePurpose {
Config,
Test,
Documentation,
CoreLogic,
Interface,
Build,
Tooling,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum ArchitecturalLayer {
Presentation,
Business,
Data,
Infrastructure,
Cross,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChangeImpact {
Style,
Additive,
Modification,
Breaking,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ProjectSignificance {
Routine,
Important,
Critical,
}
impl CommitContext {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_significant_change(&self) -> bool {
matches!(
self.range.change_significance,
ChangeSignificance::Major | ChangeSignificance::Critical
) || matches!(
self.range.architectural_impact,
ArchitecturalImpact::Significant | ArchitecturalImpact::Breaking
) || self.files.iter().any(|f| {
matches!(f.project_significance, ProjectSignificance::Critical)
|| matches!(
f.change_impact,
ChangeImpact::Breaking | ChangeImpact::Critical
)
})
}
pub fn suggested_verbosity(&self) -> VerbosityLevel {
if self.is_significant_change() {
VerbosityLevel::Comprehensive
} else if matches!(self.range.change_significance, ChangeSignificance::Moderate)
|| self.files.len() > 1
|| self.files.iter().any(|f| {
matches!(
f.architectural_layer,
ArchitecturalLayer::Presentation | ArchitecturalLayer::Business
)
})
{
VerbosityLevel::Detailed
} else {
VerbosityLevel::Concise
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum VerbosityLevel {
Concise,
Detailed,
Comprehensive,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn work_type_known_variants() {
assert!(matches!(
WorkType::from_str("feature").unwrap(),
WorkType::Feature
));
assert!(matches!(
WorkType::from_str("feat").unwrap(),
WorkType::Feature
));
assert!(matches!(WorkType::from_str("fix").unwrap(), WorkType::Fix));
assert!(matches!(
WorkType::from_str("bugfix").unwrap(),
WorkType::Fix
));
assert!(matches!(
WorkType::from_str("docs").unwrap(),
WorkType::Docs
));
assert!(matches!(WorkType::from_str("doc").unwrap(), WorkType::Docs));
assert!(matches!(
WorkType::from_str("refactor").unwrap(),
WorkType::Refactor
));
assert!(matches!(
WorkType::from_str("chore").unwrap(),
WorkType::Chore
));
assert!(matches!(
WorkType::from_str("test").unwrap(),
WorkType::Test
));
assert!(matches!(WorkType::from_str("ci").unwrap(), WorkType::Ci));
assert!(matches!(
WorkType::from_str("build").unwrap(),
WorkType::Build
));
assert!(matches!(
WorkType::from_str("perf").unwrap(),
WorkType::Perf
));
}
#[test]
fn work_type_unknown() {
assert!(matches!(
WorkType::from_str("random").unwrap(),
WorkType::Unknown
));
assert!(matches!(WorkType::from_str("").unwrap(), WorkType::Unknown));
}
#[test]
fn work_type_display() {
assert_eq!(WorkType::Feature.to_string(), "feature development");
assert_eq!(WorkType::Fix.to_string(), "bug fix");
assert_eq!(WorkType::Unknown.to_string(), "unknown work");
}
#[test]
fn significant_when_breaking_impact() {
let mut ctx = CommitContext::new();
ctx.range.architectural_impact = ArchitecturalImpact::Breaking;
assert!(ctx.is_significant_change());
}
#[test]
fn significant_when_critical_change() {
let mut ctx = CommitContext::new();
ctx.range.change_significance = ChangeSignificance::Critical;
assert!(ctx.is_significant_change());
}
#[test]
fn significant_when_critical_file() {
let mut ctx = CommitContext::new();
ctx.files.push(FileContext {
path: "src/main.rs".into(),
file_purpose: FilePurpose::CoreLogic,
architectural_layer: ArchitecturalLayer::Business,
change_impact: ChangeImpact::Breaking,
project_significance: ProjectSignificance::Critical,
});
assert!(ctx.is_significant_change());
}
#[test]
fn not_significant_when_minor() {
let ctx = CommitContext::new();
assert!(!ctx.is_significant_change());
}
#[test]
fn comprehensive_for_significant() {
let mut ctx = CommitContext::new();
ctx.range.architectural_impact = ArchitecturalImpact::Breaking;
assert!(matches!(
ctx.suggested_verbosity(),
VerbosityLevel::Comprehensive
));
}
#[test]
fn detailed_for_moderate() {
let mut ctx = CommitContext::new();
ctx.range.change_significance = ChangeSignificance::Moderate;
assert!(matches!(
ctx.suggested_verbosity(),
VerbosityLevel::Detailed
));
}
#[test]
fn concise_for_minor() {
let ctx = CommitContext::new();
assert!(matches!(ctx.suggested_verbosity(), VerbosityLevel::Concise));
}
}