From 25d3c72abcc4824540a77b33864ac8cabaefa1f9 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Sat, 31 Jan 2026 11:39:06 +0000 Subject: [PATCH] Add analyzer.rs and convention.rs modules --- src/convention.rs | 209 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 src/convention.rs diff --git a/src/convention.rs b/src/convention.rs new file mode 100644 index 0000000..3b4a25b --- /dev/null +++ b/src/convention.rs @@ -0,0 +1,209 @@ +use std::collections::HashMap; +use std::fmt; +use regex::Regex; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum CommitType { + Feat, + Fix, + Docs, + Style, + Refactor, + Test, + Chore, + Build, + Ci, + Perf, + Revert, + Custom(String), +} + +impl fmt::Display for CommitType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CommitType::Feat => write!(f, "feat"), + CommitType::Fix => write!(f, "fix"), + CommitType::Docs => write!(f, "docs"), + CommitType::Style => write!(f, "style"), + CommitType::Refactor => write!(f, "refactor"), + CommitType::Test => write!(f, "test"), + CommitType::Chore => write!(f, "chore"), + CommitType::Build => write!(f, "build"), + CommitType::Ci => write!(f, "ci"), + CommitType::Perf => write!(f, "perf"), + CommitType::Revert => write!(f, "revert"), + CommitType::Custom(s) => write!(f, "{}", s), + } + } +} + +impl std::str::FromStr for CommitType { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + match s.to_lowercase().as_str() { + "feat" | "feature" => Ok(CommitType::Feat), + "fix" | "bugfix" => Ok(CommitType::Fix), + "docs" | "documentation" => Ok(CommitType::Docs), + "style" | "formatting" => Ok(CommitType::Style), + "refactor" | "restructure" => Ok(CommitType::Refactor), + "test" | "testing" => Ok(CommitType::Test), + "chore" | "maintenance" => Ok(CommitType::Chore), + "build" | "builds" => Ok(CommitType::Build), + "ci" | "continuous" => Ok(CommitType::Ci), + "perf" | "performance" => Ok(CommitType::Perf), + "revert" | "reverts" => Ok(CommitType::Revert), + _ => Ok(CommitType::Custom(s.to_string())), + } + } +} + +#[derive(Debug, Clone)] +pub struct Scope { + pub name: String, + pub is_custom: bool, +} + +impl Scope { + pub fn new(name: String) -> Self { + Self { + name, + is_custom: false, + } + } + + pub fn custom(name: String) -> Self { + Self { + name, + is_custom: true, + } + } +} + +impl fmt::Display for Scope { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +#[derive(Debug, Clone)] +pub struct CommitSuggestion { + pub commit_type: CommitType, + pub scope: Option, + pub description: String, + pub confidence: f64, + pub file_count: usize, +} + +impl fmt::Display for CommitSuggestion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.scope { + Some(scope) => write!(f, "{}({}): {}", self.commit_type, scope, self.description), + None => write!(f, "{}: {}", self.commit_type, self.description), + } + } +} + +#[derive(Debug, Clone)] +pub struct CommitConvention { + pub type_mapping: HashMap, + pub scope_patterns: Vec<(Regex, String)>, + pub ignored_patterns: Vec, +} + +impl Default for CommitConvention { + fn default() -> Self { + let mut type_mapping = HashMap::new(); + type_mapping.insert("src/**/*.rs".to_string(), CommitType::Feat); + type_mapping.insert("lib/**/*.rs".to_string(), CommitType::Feat); + type_mapping.insert("src/**/*.ts".to_string(), CommitType::Feat); + type_mapping.insert("src/**/*.js".to_string(), CommitType::Feat); + type_mapping.insert("src/**/*.py".to_string(), CommitType::Feat); + type_mapping.insert("**/*test*.rs".to_string(), CommitType::Test); + type_mapping.insert("**/*spec*.rs".to_string(), CommitType::Test); + type_mapping.insert("tests/**/*.rs".to_string(), CommitType::Test); + type_mapping.insert("**/*_test.py".to_string(), CommitType::Test); + type_mapping.insert("test_*.py".to_string(), CommitType::Test); + type_mapping.insert("**/*.md".to_string(), CommitType::Docs); + type_mapping.insert("docs/**/*.md".to_string(), CommitType::Docs); + type_mapping.insert("README*".to_string(), CommitType::Docs); + type_mapping.insert("**/*.yaml".to_string(), CommitType::Chore); + type_mapping.insert("**/*.yml".to_string(), CommitType::Chore); + type_mapping.insert("Dockerfile*".to_string(), CommitType::Build); + type_mapping.insert("**/package.json".to_string(), CommitType::Chore); + type_mapping.insert("**/Cargo.toml".to_string(), CommitType::Chore); + type_mapping.insert("**/*.lock".to_string(), CommitType::Chore); + + let mut scope_patterns = Vec::new(); + scope_patterns.push((Regex::new(r"^src/([^/]+)/").unwrap(), "src")); + scope_patterns.push((Regex::new(r"^lib/([^/]+)/").unwrap(), "lib")); + scope_patterns.push((Regex::new(r"^app/([^/]+)/").unwrap(), "app")); + scope_patterns.push((Regex::new(r"^packages/([^/]+)/").unwrap(), "packages")); + scope_patterns.push((Regex::new(r"^features/([^/]+)/").unwrap(), "features")); + scope_patterns.push((Regex::new(r"^components/([^/]+)/").unwrap(), "components")); + scope_patterns.push((Regex::new(r"^controllers/([^/]+)/").unwrap(), "controllers")); + scope_patterns.push((Regex::new(r"^models/([^/]+)/").unwrap(), "models")); + scope_patterns.push((Regex::new(r"^services/([^/]+)/").unwrap(), "services")); + + let ignored_patterns = vec![ + Regex::new(r"^target/").unwrap(), + Regex::new(r"^node_modules/").unwrap(), + Regex::new(r"^\.git/").unwrap(), + Regex::new(r"^dist/").unwrap(), + Regex::new(r"^build/").unwrap(), + Regex::new(r"^\.idea/").unwrap(), + Regex::new(r"^\.vscode/").unwrap(), + Regex::new(r"\.DS_Store").unwrap(), + ]; + + Self { + type_mapping, + scope_patterns, + ignored_patterns, + } + } +} + +impl CommitConvention { + pub fn detect_type(&self, file_path: &str) -> Option<(CommitType, f64)> { + for (pattern, commit_type) in &self.type_mapping { + if self.matches_pattern(pattern, file_path) { + let confidence = if pattern.contains("**/*") { 0.7 } else { 0.9 }; + return Some((commit_type.clone(), confidence)); + } + } + None + } + + pub fn detect_scope(&self, file_path: &str) -> Option { + for (pattern, _base) in &self.scope_patterns { + if let Some(caps) = pattern.captures(file_path) { + if let Some(scope) = caps.get(1) { + return Some(Scope::new(scope.as_str().to_string())); + } + } + } + None + } + + fn matches_pattern(&self, pattern: &str, file_path: &str) -> bool { + let regex_pattern = pattern + .replace("**/*", ".*") + .replace("**", ".*") + .replace("*", "[^/]*") + .replace(".", "\\."); + + Regex::new(®ex_pattern) + .map(|re| re.is_match(file_path)) + .unwrap_or(false) + } + + pub fn should_ignore(&self, file_path: &str) -> bool { + for pattern in &self.ignored_patterns { + if pattern.is_match(file_path) { + return true; + } + } + false + } +}