This commit is contained in:
209
src/convention.rs
Normal file
209
src/convention.rs
Normal 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(®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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user