use std::path::Path;
use ignore::gitignore::GitignoreBuilder;
use crate::model::DiffFile;
pub fn filter_diff_files(repo_root: &Path, diff_files: Vec<DiffFile>) -> Vec<DiffFile> {
let Some(matcher) = load_matcher(repo_root) else {
return diff_files;
};
diff_files
.into_iter()
.filter(|file| {
!matcher
.matched_path_or_any_parents(file.display_path(), false)
.is_ignore()
})
.collect()
}
fn load_matcher(repo_root: &Path) -> Option<ignore::gitignore::Gitignore> {
let gitignore_file = repo_root.join(".gitignore");
let tuicrignore_file = repo_root.join(".tuicrignore");
if !gitignore_file.is_file() && !tuicrignore_file.is_file() {
return None;
}
let mut builder = GitignoreBuilder::new(repo_root);
if gitignore_file.is_file() {
let _ = builder.add(&gitignore_file);
}
if tuicrignore_file.is_file() {
let _ = builder.add(&tuicrignore_file);
}
builder.build().ok()
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use tempfile::tempdir;
use super::*;
use crate::model::FileStatus;
fn make_diff_file(path: &str) -> DiffFile {
DiffFile {
old_path: None,
new_path: Some(PathBuf::from(path)),
status: FileStatus::Modified,
hunks: Vec::new(),
is_binary: false,
is_too_large: false,
is_commit_message: false,
}
}
#[test]
fn keeps_all_files_when_tuicrignore_is_missing() {
let dir = tempdir().expect("failed to create temp dir");
let files = vec![
make_diff_file("src/main.rs"),
make_diff_file("target/debug/app"),
];
let filtered = filter_diff_files(dir.path(), files);
assert_eq!(filtered.len(), 2);
}
#[test]
fn filters_matching_files() {
let dir = tempdir().expect("failed to create temp dir");
let ignore_path = dir.path().join(".tuicrignore");
fs::write(&ignore_path, "target/\n*.lock\n").expect("failed to write .tuicrignore");
let files = vec![
make_diff_file("src/main.rs"),
make_diff_file("target/debug/app"),
make_diff_file("Cargo.lock"),
];
let filtered = filter_diff_files(dir.path(), files);
let kept_paths: Vec<String> = filtered
.iter()
.map(|f| f.display_path().display().to_string())
.collect();
assert_eq!(kept_paths, vec!["src/main.rs"]);
}
#[test]
fn supports_unignore_rules() {
let dir = tempdir().expect("failed to create temp dir");
let ignore_path = dir.path().join(".tuicrignore");
fs::write(&ignore_path, "generated/\n!generated/keep.rs\n")
.expect("failed to write .tuicrignore");
let files = vec![
make_diff_file("generated/drop.rs"),
make_diff_file("generated/keep.rs"),
make_diff_file("src/main.rs"),
];
let filtered = filter_diff_files(dir.path(), files);
let kept_paths: Vec<String> = filtered
.iter()
.map(|f| f.display_path().display().to_string())
.collect();
assert_eq!(kept_paths, vec!["generated/keep.rs", "src/main.rs"]);
}
#[test]
fn respects_gitignore() {
let dir = tempdir().expect("failed to create temp dir");
let gitignore = dir.path().join(".gitignore");
fs::write(&gitignore, "target/\n*.log\n").expect("failed to write .gitignore");
let files = vec![
make_diff_file("src/main.rs"),
make_diff_file("target/debug/app"),
make_diff_file("build.log"),
];
let filtered = filter_diff_files(dir.path(), files);
let kept: Vec<String> = filtered
.iter()
.map(|f| f.display_path().display().to_string())
.collect();
assert_eq!(kept, vec!["src/main.rs"]);
}
#[test]
fn tuicrignore_overrides_gitignore() {
let dir = tempdir().expect("failed to create temp dir");
let gitignore = dir.path().join(".gitignore");
fs::write(&gitignore, "*.lock\n").expect("failed to write .gitignore");
let tuicrignore = dir.path().join(".tuicrignore");
fs::write(&tuicrignore, "!Cargo.lock\n").expect("failed to write .tuicrignore");
let files = vec![
make_diff_file("Cargo.lock"),
make_diff_file("yarn.lock"),
make_diff_file("src/lib.rs"),
];
let filtered = filter_diff_files(dir.path(), files);
let kept: Vec<String> = filtered
.iter()
.map(|f| f.display_path().display().to_string())
.collect();
assert_eq!(kept, vec!["Cargo.lock", "src/lib.rs"]);
}
#[test]
fn gitignore_alone_filters_without_tuicrignore() {
let dir = tempdir().expect("failed to create temp dir");
let gitignore = dir.path().join(".gitignore");
fs::write(&gitignore, "dist/\n").expect("failed to write .gitignore");
let files = vec![
make_diff_file("src/index.ts"),
make_diff_file("dist/bundle.js"),
];
let filtered = filter_diff_files(dir.path(), files);
let kept: Vec<String> = filtered
.iter()
.map(|f| f.display_path().display().to_string())
.collect();
assert_eq!(kept, vec!["src/index.ts"]);
}
#[test]
fn handles_deleted_file_paths() {
let dir = tempdir().expect("failed to create temp dir");
let ignore_path = dir.path().join(".tuicrignore");
fs::write(&ignore_path, "generated/\n").expect("failed to write .tuicrignore");
let deleted = DiffFile {
old_path: Some(PathBuf::from("generated/old.txt")),
new_path: None,
status: FileStatus::Deleted,
hunks: Vec::new(),
is_binary: false,
is_too_large: false,
is_commit_message: false,
};
let kept = make_diff_file("src/lib.rs");
let filtered = filter_diff_files(dir.path(), vec![deleted, kept]);
let kept_paths: Vec<String> = filtered
.iter()
.map(|f| f.display_path().display().to_string())
.collect();
assert_eq!(kept_paths, vec!["src/lib.rs"]);
}
}