Files
auto-commit/src/analyzer.rs
7000pctAUTO 228fd5f921
Some checks failed
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled
fix: Update analyzer.rs, generator.rs, and prompt.rs with proper imports
2026-02-01 12:40:24 +00:00

214 lines
6.4 KiB
Rust

pub enum CommitType {
Feat,
Fix,
Docs,
Style,
Refactor,
Test,
Chore,
Build,
Ci,
Perf,
}
impl std::fmt::Display for CommitType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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"),
}
}
}
pub struct AnalysisResult {
pub commit_type: CommitType,
pub scope: Option<String>,
pub confidence: f64,
pub description: String,
pub reasons: Vec<String>,
}
pub fn analyze_changes(staged: &super::git::StagedChanges) -> AnalysisResult {
let files: Vec<&super::git::ChangedFile> = staged.files.iter().collect();
if files.is_empty() {
return AnalysisResult {
commit_type: CommitType::Chore,
scope: None,
confidence: 1.0,
description: "empty commit".to_string(),
reasons: vec!["No files changed".to_string()],
};
}
let mut type_scores: std::collections::HashMap<CommitType, (f64, Vec<String>)> = std::collections::HashMap::new();
for file in &files {
let path = std::path::Path::new(&file.path);
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let (type_, reason) = classify_file(&file.path, &file.status, ext, file_name);
type_scores
.entry(type_)
.or_insert((0.0, Vec::new()))
.0 += 1.0;
type_scores.get_mut(&type_).unwrap().1.push(reason);
}
let (best_type, (max_score, reasons)) = type_scores
.into_iter()
.max_by(|(a, (score_a, _)), (b, (score_b, _))| {
score_a.partial_cmp(&score_b).unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or((CommitType::Chore, (1.0, vec!["default".to()])));
let scope = extract_scope(&files.first().unwrap().path);
let description = generate_description(&files);
let confidence = (max_score as usize) as f64 / files.len() as f64;
AnalysisResult {
commit_type: best_type,
scope,
confidence,
description,
reasons,
}
}
fn classify_file(
path: &str,
status: &super::git::FileStatus,
ext: &str,
file_name: &str,
) -> (CommitType, String) {
if ext == "md" || path.to_lowercase().contains("readme") || path.to_lowercase().contains("changelog") {
return (CommitType::Docs, format!("Documentation file: {}", path));
}
if path.contains("test") || path.contains("_test") || ext == "test" {
return (CommitType::Test, format!("Test file: {}", path));
}
if path.contains(".github") || path.contains("workflow") || path.contains("ci") {
return (CommitType::Ci, format!("CI configuration: {}", path));
}
if file_name == "Cargo.toml" || file_name == "Cargo.lock" || ext == "toml" {
return (CommitType::Build, format!("Build configuration: {}", path));
}
match status {
super::git::FileStatus::Added if ext == "rs" => {
(CommitType::Feat, format!("New feature file: {}", path))
}
super::git::FileStatus::Modified if ext == "rs" => {
(CommitType::Fix, format!("Modified source file: {}", path))
}
super::git::FileStatus::Deleted => {
(CommitType::Fix, format!("Deleted file: {}", path))
}
super::git::FileStatus::Renamed => {
(CommitType::Refactor, format!("Renamed file: {}", path))
}
_ => (CommitType::Chore, format!("Other change: {}", path)),
}
}
pub fn extract_scope(path: &str) -> Option<String> {
let path = std::path::Path::new(path);
let components: Vec<String> = path
.components()
.filter_map(|c| c.as_os_str().to_str().map(|s| s.to_string()))
.collect();
if components.is_empty() {
return None;
}
let first = components.first().unwrap();
if first == "src" || first == "lib" || first == "tests" || first == "benches" {
Some(String::from("src"))
} else {
Some(first.clone())
}
}
pub fn get_all_commit_types() -> Vec<CommitType> {
vec![
CommitType::Feat,
CommitType::Fix,
CommitType::Docs,
CommitType::Style,
CommitType::Refactor,
CommitType::Test,
CommitType::Chore,
CommitType::Build,
CommitType::Ci,
CommitType::Perf,
]
}
fn generate_description(files: &[&super::git::ChangedFile]) -> String {
if files.is_empty() {
return String::from("empty commit");
}
if files.len() == 1 {
let f = files[0];
let action = match f.status {
super::git::FileStatus::Added => "add",
super::git::FileStatus::Deleted => "remove",
super::git::FileStatus::Modified => "update",
super::git::FileStatus::Renamed => "rename",
super::git::FileStatus::Unknown => "change",
};
let file_name = std::path::Path::new(&f.path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&f.path);
return format!("{} {}", action, file_name);
}
let total = files.len();
let added: usize = files.iter().filter(|f| f.is_new).count();
let modified: usize = files.iter().filter(|f| matches!(f.status, super::git::FileStatus::Modified)).count();
if added == total {
format!("add {} new files", total)
} else if modified == total {
format!("update {} files", total)
} else {
format!("change {} files ({} new, {} modified)", total, added, modified)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_scope_src() {
assert_eq!(extract_scope("src/main.rs"), Some(String::from("src")));
}
#[test]
fn test_extract_scope_nested() {
assert_eq!(extract_scope("src/utils/helper.rs"), Some(String::from("src")));
}
#[test]
fn test_extract_scope_root() {
assert_eq!(extract_scope("main.rs"), Some(String::from("main.rs")));
}
}