Add analyzer.rs and convention.rs modules
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-01-31 11:39:06 +00:00
parent c185c7019c
commit 25d3c72abc

209
src/convention.rs Normal file
View File

@@ -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<Self, Self::Err> {
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<Scope>,
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<String, CommitType>,
pub scope_patterns: Vec<(Regex, String)>,
pub ignored_patterns: Vec<Regex>,
}
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<Scope> {
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(&regex_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
}
}