use std::fs;
use std::path::Path;
use glob::Pattern;
use tracing::debug;
pub const DEFAULT_IGNORE_PATTERNS: &[&str] = &[
"*.exe",
"*.dll",
"*.so",
"*.dylib",
"*.bin",
"*.obj",
"*.o",
"*.a",
"*.lib",
"*.pyc",
"*.pyo",
"*.class",
"*.jar",
"*.war",
"*.ear",
"*.wasm",
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
"*.bmp",
"*.ico",
"*.webp",
"*.svg",
"*.tiff",
"*.tif",
"*.psd",
"*.ai",
"*.eps",
"*.mp3",
"*.mp4",
"*.avi",
"*.mov",
"*.wmv",
"*.flv",
"*.wav",
"*.ogg",
"*.webm",
"*.mkv",
"*.zip",
"*.tar",
"*.gz",
"*.rar",
"*.7z",
"*.bz2",
"*.xz",
"*.tar.gz",
"*.tgz",
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"Cargo.lock",
"Gemfile.lock",
"poetry.lock",
"Pipfile.lock",
"composer.lock",
"pubspec.lock",
"go.sum",
"flake.lock",
"*.lock",
"*.db",
"*.sqlite",
"*.sqlite3",
"*.mdb",
"*.ttf",
"*.otf",
"*.woff",
"*.woff2",
"*.eot",
"*.pdf",
"*.doc",
"*.docx",
"*.xls",
"*.xlsx",
"*.ppt",
"*.pptx",
"*.min.js",
"*.min.css",
"*.map",
"*.js.map",
"*.css.map",
"*.pb.go",
"*.pb.cc",
"*.pb.h",
"*_generated.go",
"*_generated.ts",
"*.generated.cs",
];
#[derive(Debug)]
pub struct AiCommitIgnore {
patterns: Vec<Pattern>,
}
impl AiCommitIgnore {
pub fn new() -> Self {
let mut patterns = Vec::new();
for pattern_str in DEFAULT_IGNORE_PATTERNS {
if let Ok(pattern) = Pattern::new(pattern_str) {
patterns.push(pattern);
}
}
if let Ok(content) = fs::read_to_string(".aicommitignore") {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('!') {
continue;
}
if let Ok(pattern) = Pattern::new(line) {
patterns.push(pattern);
} else {
debug!("Invalid pattern in .aicommitignore: {}", line);
}
}
}
Self { patterns }
}
pub fn is_ignored(&self, file_path: &str) -> bool {
let filename = Path::new(file_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(file_path);
for pattern in &self.patterns {
if pattern.matches(file_path) {
return true;
}
if pattern.matches(filename) {
return true;
}
if pattern.matches_path(Path::new(file_path)) {
return true;
}
}
false
}
}
impl Default for AiCommitIgnore {
fn default() -> Self {
Self::new()
}
}
pub fn extract_file_path_from_diff_header(header: &str) -> Option<String> {
if let Some(b_pos) = header.find(" b/") {
let path_start = b_pos + 3; let path = header[path_start..].trim();
if !path.is_empty() {
return Some(path.to_string());
}
}
if let Some(a_pos) = header.find("a/") {
let path_start = a_pos + 2;
if let Some(b_pos) = header[path_start..].find(" b/") {
let path = &header[path_start..path_start + b_pos];
if !path.is_empty() {
return Some(path.to_string());
}
}
}
None
}
pub fn filter_diff_by_ignore_patterns(diff: &str, skip_filter: bool) -> String {
if skip_filter {
return diff.to_string();
}
let ignore = AiCommitIgnore::new();
let file_pattern = "diff --git ";
let sections: Vec<&str> = diff.split(file_pattern).collect();
let mut filtered = String::new();
if !sections.is_empty() && !sections[0].trim().is_empty() {
filtered.push_str(sections[0]);
}
for section in sections.iter().skip(1) {
if section.trim().is_empty() {
continue;
}
let first_line = section.lines().next().unwrap_or("");
if let Some(file_path) = extract_file_path_from_diff_header(&format!("diff --git {}", first_line)) {
if ignore.is_ignored(&file_path) {
debug!("Ignoring diff for file: {}", file_path);
continue;
}
}
filtered.push_str(file_pattern);
filtered.push_str(section);
}
filtered
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_file_path() {
assert_eq!(
extract_file_path_from_diff_header("diff --git a/src/main.rs b/src/main.rs"),
Some("src/main.rs".to_string())
);
assert_eq!(
extract_file_path_from_diff_header("diff --git a/package-lock.json b/package-lock.json"),
Some("package-lock.json".to_string())
);
}
#[test]
fn test_default_patterns() {
let ignore = AiCommitIgnore::new();
assert!(ignore.is_ignored("test.exe"));
assert!(ignore.is_ignored("lib.dll"));
assert!(ignore.is_ignored("lib.so"));
assert!(ignore.is_ignored("package-lock.json"));
assert!(ignore.is_ignored("yarn.lock"));
assert!(ignore.is_ignored("Cargo.lock"));
assert!(ignore.is_ignored("image.png"));
assert!(ignore.is_ignored("photo.jpg"));
assert!(!ignore.is_ignored("main.rs"));
assert!(!ignore.is_ignored("index.ts"));
assert!(!ignore.is_ignored("app.py"));
}
#[test]
fn test_filter_diff() {
let diff = r#"diff --git a/src/main.rs b/src/main.rs
index 1234567..abcdefg 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,4 @@
+// New comment
fn main() {
println!("Hello");
}
diff --git a/package-lock.json b/package-lock.json
index aaaaaaa..bbbbbbb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,100 +1,200 @@
{
"name": "test",
"lockfileVersion": 3
}
diff --git a/README.md b/README.md
index ccccccc..ddddddd 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
# Project
+New line
"#;
let filtered = filter_diff_by_ignore_patterns(diff, false);
assert!(filtered.contains("src/main.rs"));
assert!(!filtered.contains("package-lock.json"));
assert!(filtered.contains("README.md"));
let unfiltered = filter_diff_by_ignore_patterns(diff, true);
assert!(unfiltered.contains("src/main.rs"));
assert!(unfiltered.contains("package-lock.json"));
assert!(unfiltered.contains("README.md"));
}
}