214 lines
6.4 KiB
Rust
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")));
|
|
}
|
|
}
|