fix: Add missing analyze_changes, get_all_commit_types functions to analyzer.rs
Some checks failed
CI / lint (push) Failing after 3s
CI / build (push) Has been skipped
CI / test (push) Has been skipped

This commit is contained in:
2026-02-01 12:29:11 +00:00
parent a204e6c900
commit dec64839ce

View File

@@ -3,49 +3,132 @@ use colored::Colorize;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
pub fn analyze_git_status() -> Result<HashMap<String, Vec<String>>> { pub enum CommitType {
let output = std::process::Command::new("git") Feat,
.args(&["status", "--porcelain"]) Fix,
.output() Docs,
.context("Failed to execute git status")?; Style,
Refactor,
Test,
Chore,
Build,
Ci,
Perf,
}
if !output.status.success() { impl std::fmt::Display for CommitType {
return Err(anyhow!("Git command failed")); 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"),
}
} }
}
let stdout = String::from_utf8_lossy(&output.stdout); pub struct AnalysisResult {
let mut changes = HashMap::new(); pub commit_type: CommitType,
pub scope: Option<String>,
pub confidence: f64,
pub description: String,
pub reasons: Vec<String>,
}
for line in stdout.lines() { pub fn analyze_changes(staged: &crate::git::StagedChanges) -> AnalysisResult {
if line.trim().is_empty() { let files: Vec<&crate::git::ChangedFile> = staged.files.iter().collect();
continue;
}
let parts: Vec<&str> = line.splitn(2, " ").collect(); if files.is_empty() {
if parts.len() < 2 { return AnalysisResult {
continue; commit_type: CommitType::Chore,
} scope: None,
confidence: 1.0,
let status = parts[0]; description: "empty commit".to_string(),
let path = parts[1].trim(); reasons: vec!["No files changed".to_string()],
let change_type = match status {
"M" => "modified",
"A" => "added",
"D" => "deleted",
"R" => "renamed",
"?" => "untracked",
"C" => "copied",
_ => "unknown",
}; };
changes
.entry(change_type.to_string())
.or_insert_with(Vec::new)
.push(path.to_string());
} }
Ok(changes) let mut type_scores: HashMap<CommitType, (f64, Vec<String>)> = HashMap::new();
for file in &files {
let 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 parent = path.parent().and_then(|p| p.file_name()).and_then(|n| n.to_str()).unwrap_or("");
let (type_, reason) = classify_file(&file.path, &file.status, ext, file_name, parent);
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: &crate::git::FileStatus,
ext: &str,
file_name: &str,
parent: &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 {
crate::git::FileStatus::Added if ext == "rs" => {
(CommitType::Feat, format!("New feature file: {}", path))
}
crate::git::FileStatus::Modified if ext == "rs" => {
(CommitType::Fix, format!("Modified source file: {}", path))
}
crate::git::FileStatus::Deleted => {
(CommitType::Fix, format!("Deleted file: {}", path))
}
crate::git::FileStatus::Renamed => {
(CommitType::Refactor, format!("Renamed file: {}", path))
}
_ => (CommitType::Chore, format!("Other change: {}", path)),
}
} }
pub fn extract_scope(path: &str) -> Option<String> { pub fn extract_scope(path: &str) -> Option<String> {
@@ -67,10 +150,10 @@ pub fn extract_scope(path: &str) -> Option<String> {
} }
} }
pub fn format_change_summary(changes: &HashMap<String, Vec<String>>) -> String { pub fn format_change_summary(changes: HashMap<String, Vec<String>>) -> String {
let mut summary = String::new(); let mut summary = String::new();
let priority = vec!["added", "modified", "deleted", "renamed", "copied", "untracked"]; let priority = vec!["added", "modified", "deleted", "renamed", "copied", "untacked"];
for change_type in priority { for change_type in priority {
if let Some(files) = changes.get(change_type) { if let Some(files) = changes.get(change_type) {
@@ -79,18 +162,18 @@ pub fn format_change_summary(changes: &HashMap<String, Vec<String>>) -> String {
} }
let prefix = match change_type { let prefix = match change_type {
"added" => "📦 ", "added" => " Files",
"modified" => "✏️ ", "modified" => "✏️ Files",
"deleted" => "🗑️ ", "deleted" => "🗑️ Files",
"renamed" => "📝 ", "renamed" => "📝 Files",
"copied" => "📄 ", "copied" => "📋 Files",
"untracked" => "", "untacked" => "🔍 Files",
_ => "📁 ", _ => "📄 Files",
}; };
summary.push_str(&format!("{}Files {}:\n", prefix, change_type)); summary.push_str(&format!("{} {}:\n", prefix, change_type));
for file in files { for file in files {
summary.push_str(&format!(" - {}\n", file)); summary.push_str(&format!(" - {}\n", file));
} }
} }
} }
@@ -98,6 +181,55 @@ pub fn format_change_summary(changes: &HashMap<String, Vec<String>>) -> String {
summary summary
} }
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: &[&crate::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 {
crate::git::FileStatus::Added => "add",
crate::git::FileStatus::Deleted => "remove",
crate::git::FileStatus::Modified => "update",
crate::git::FileStatus::Renamed => "rename",
crate::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, crate::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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -128,8 +260,8 @@ mod tests {
changes.insert("modified".to_string(), vec!["src/main.rs".to_string(), "Cargo.toml".to_string()]); changes.insert("modified".to_string(), vec!["src/main.rs".to_string(), "Cargo.toml".to_string()]);
changes.insert("added".to_string(), vec!["src/new_module.rs".to_string()]); changes.insert("added".to_string(), vec!["src/new_module.rs".to_string()]);
let summary = format_change_summary(&changes); let summary = format_change_summary(changes);
assert!(summary.contains("📦 Files added:")); assert!(summary.contains(" Files added:"));
assert!(summary.contains("✏️ Files modified:")); assert!(summary.contains("✏️ Files modified:"));
assert!(summary.contains("src/main.rs")); assert!(summary.contains("src/main.rs"));
assert!(summary.contains("src/new_module.rs")); assert!(summary.contains("src/new_module.rs"));