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::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"));
|
||||||
|
|||||||
Reference in New Issue
Block a user