fix: Add missing analyze_changes, get_all_commit_types functions to analyzer.rs
This commit is contained in:
228
src/analyzer.rs
228
src/analyzer.rs
@@ -3,49 +3,132 @@ use colored::Colorize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn analyze_git_status() -> Result<HashMap<String, Vec<String>>> {
|
||||
let output = std::process::Command::new("git")
|
||||
.args(&["status", "--porcelain"])
|
||||
.output()
|
||||
.context("Failed to execute git status")?;
|
||||
pub enum CommitType {
|
||||
Feat,
|
||||
Fix,
|
||||
Docs,
|
||||
Style,
|
||||
Refactor,
|
||||
Test,
|
||||
Chore,
|
||||
Build,
|
||||
Ci,
|
||||
Perf,
|
||||
}
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!("Git command failed"));
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let mut changes = HashMap::new();
|
||||
pub struct AnalysisResult {
|
||||
pub commit_type: CommitType,
|
||||
pub scope: Option<String>,
|
||||
pub confidence: f64,
|
||||
pub description: String,
|
||||
pub reasons: Vec<String>,
|
||||
}
|
||||
|
||||
for line in stdout.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
pub fn analyze_changes(staged: &crate::git::StagedChanges) -> AnalysisResult {
|
||||
let files: Vec<&crate::git::ChangedFile> = staged.files.iter().collect();
|
||||
|
||||
let parts: Vec<&str> = line.splitn(2, " ").collect();
|
||||
if parts.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let status = parts[0];
|
||||
let path = parts[1].trim();
|
||||
|
||||
let change_type = match status {
|
||||
"M" => "modified",
|
||||
"A" => "added",
|
||||
"D" => "deleted",
|
||||
"R" => "renamed",
|
||||
"?" => "untracked",
|
||||
"C" => "copied",
|
||||
_ => "unknown",
|
||||
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()],
|
||||
};
|
||||
|
||||
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> {
|
||||
@@ -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 priority = vec!["added", "modified", "deleted", "renamed", "copied", "untracked"];
|
||||
let priority = vec!["added", "modified", "deleted", "renamed", "copied", "untacked"];
|
||||
|
||||
for change_type in priority {
|
||||
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 {
|
||||
"added" => "📦 ",
|
||||
"modified" => "✏️ ",
|
||||
"deleted" => "🗑️ ",
|
||||
"renamed" => "📝 ",
|
||||
"copied" => "📄 ",
|
||||
"untracked" => "❓ ",
|
||||
_ => "📁 ",
|
||||
"added" => "➕ Files",
|
||||
"modified" => "✏️ Files",
|
||||
"deleted" => "🗑️ Files",
|
||||
"renamed" => "📝 Files",
|
||||
"copied" => "📋 Files",
|
||||
"untacked" => "🔍 Files",
|
||||
_ => "📄 Files",
|
||||
};
|
||||
|
||||
summary.push_str(&format!("{}Files {}:\n", prefix, change_type));
|
||||
summary.push_str(&format!("{} {}:\n", prefix, change_type));
|
||||
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
|
||||
}
|
||||
|
||||
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)]
|
||||
mod tests {
|
||||
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("added".to_string(), vec!["src/new_module.rs".to_string()]);
|
||||
|
||||
let summary = format_change_summary(&changes);
|
||||
assert!(summary.contains("📦 Files added:"));
|
||||
let summary = format_change_summary(changes);
|
||||
assert!(summary.contains("➕ Files added:"));
|
||||
assert!(summary.contains("✏️ Files modified:"));
|
||||
assert!(summary.contains("src/main.rs"));
|
||||
assert!(summary.contains("src/new_module.rs"));
|
||||
|
||||
Reference in New Issue
Block a user