use ignore::gitignore::{Gitignore, GitignoreBuilder};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IgnoreStatus {
Visible,
GitIgnored,
Hidden,
CustomIgnored,
}
#[derive(Debug)]
pub struct IgnorePatterns {
gitignores: Vec<(PathBuf, Gitignore)>,
custom_patterns: Vec<String>,
show_hidden: bool,
show_gitignored: bool,
show_custom_ignored: bool,
}
impl IgnorePatterns {
pub fn new() -> Self {
Self {
gitignores: Vec::new(),
custom_patterns: Vec::new(),
show_hidden: false,
show_gitignored: false,
show_custom_ignored: false,
}
}
pub fn load_gitignore(&mut self, dir: &Path) -> std::io::Result<()> {
let gitignore_path = dir.join(".gitignore");
if !gitignore_path.exists() {
return Ok(()); }
let mut builder = GitignoreBuilder::new(dir);
builder.add(&gitignore_path);
match builder.build() {
Ok(gitignore) => {
self.gitignores.retain(|(path, _)| path != dir);
self.gitignores.push((dir.to_path_buf(), gitignore));
Ok(())
}
Err(e) => {
tracing::warn!("Failed to load .gitignore from {:?}: {}", gitignore_path, e);
Ok(()) }
}
}
pub fn add_custom_pattern(&mut self, pattern: String) {
if !self.custom_patterns.contains(&pattern) {
self.custom_patterns.push(pattern);
}
}
pub fn remove_custom_pattern(&mut self, pattern: &str) {
self.custom_patterns.retain(|p| p != pattern);
}
pub fn is_ignored(&self, path: &Path, is_dir: bool) -> bool {
let status = self.get_status(path, is_dir);
match status {
IgnoreStatus::Visible => false,
IgnoreStatus::GitIgnored => !self.show_gitignored,
IgnoreStatus::Hidden => !self.show_hidden,
IgnoreStatus::CustomIgnored => !self.show_custom_ignored,
}
}
pub fn get_status(&self, path: &Path, is_dir: bool) -> IgnoreStatus {
if let Some(name) = path.file_name() {
if let Some(name_str) = name.to_str() {
if name_str.starts_with('.') && name_str != ".." && name_str != "." {
return IgnoreStatus::Hidden;
}
}
}
if self.matches_custom_pattern(path) {
return IgnoreStatus::CustomIgnored;
}
if self.matches_gitignore(path, is_dir) {
return IgnoreStatus::GitIgnored;
}
IgnoreStatus::Visible
}
fn matches_gitignore(&self, path: &Path, is_dir: bool) -> bool {
for (gitignore_dir, gitignore) in &self.gitignores {
if path.starts_with(gitignore_dir) {
let relative_path = path.strip_prefix(gitignore_dir).unwrap_or(path);
let matched = gitignore.matched(relative_path, is_dir);
if matched.is_ignore() {
return true;
}
}
}
false
}
fn matches_custom_pattern(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
for pattern in &self.custom_patterns {
if pattern.ends_with('/') {
if path_str.contains(pattern.trim_end_matches('/')) {
return true;
}
} else if pattern.starts_with('*') {
let ext = pattern.trim_start_matches('*');
if path_str.ends_with(ext) {
return true;
}
} else {
if path_str.contains(pattern) {
return true;
}
}
}
false
}
pub fn set_show_hidden(&mut self, show: bool) {
self.show_hidden = show;
}
pub fn show_hidden(&self) -> bool {
self.show_hidden
}
pub fn set_show_gitignored(&mut self, show: bool) {
self.show_gitignored = show;
}
pub fn show_gitignored(&self) -> bool {
self.show_gitignored
}
pub fn set_show_custom_ignored(&mut self, show: bool) {
self.show_custom_ignored = show;
}
pub fn toggle_show_gitignored(&mut self) {
self.show_gitignored = !self.show_gitignored;
}
pub fn toggle_show_hidden(&mut self) {
self.show_hidden = !self.show_hidden;
}
pub fn clear_gitignores(&mut self) {
self.gitignores.clear();
}
pub fn clear_custom_patterns(&mut self) {
self.custom_patterns.clear();
}
pub fn gitignore_count(&self) -> usize {
self.gitignores.len()
}
}
impl Default for IgnorePatterns {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_hidden_file_detection() {
let patterns = IgnorePatterns::new();
assert_eq!(
patterns.get_status(Path::new("/foo/.hidden"), false),
IgnoreStatus::Hidden
);
assert_eq!(
patterns.get_status(Path::new("/foo/visible.txt"), false),
IgnoreStatus::Visible
);
assert_eq!(
patterns.get_status(Path::new("."), true),
IgnoreStatus::Visible
);
assert_eq!(
patterns.get_status(Path::new(".."), true),
IgnoreStatus::Visible
);
}
#[test]
fn test_custom_patterns() {
let mut patterns = IgnorePatterns::new();
patterns.add_custom_pattern("*.o".to_string());
patterns.add_custom_pattern("target/".to_string());
assert_eq!(
patterns.get_status(Path::new("/foo/main.o"), false),
IgnoreStatus::CustomIgnored
);
assert_eq!(
patterns.get_status(Path::new("/foo/target/debug"), true),
IgnoreStatus::CustomIgnored
);
assert_eq!(
patterns.get_status(Path::new("/foo/src/main.rs"), false),
IgnoreStatus::Visible
);
}
#[test]
fn test_gitignore_loading() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let gitignore_path = temp_dir.path().join(".gitignore");
let mut file = fs::File::create(&gitignore_path)?;
writeln!(file, "*.log")?;
writeln!(file, "build/")?;
writeln!(file, "# Comment")?;
writeln!(file, "!important.log")?;
let mut patterns = IgnorePatterns::new();
patterns.load_gitignore(temp_dir.path())?;
assert_eq!(patterns.gitignore_count(), 1);
Ok(())
}
#[test]
fn test_show_hidden_toggle() {
let mut patterns = IgnorePatterns::new();
let hidden_path = Path::new("/foo/.hidden");
assert!(!patterns.show_hidden());
assert!(patterns.is_ignored(hidden_path, false));
patterns.toggle_show_hidden();
assert!(patterns.show_hidden());
assert!(!patterns.is_ignored(hidden_path, false));
}
#[test]
fn test_show_gitignored_toggle() {
let mut patterns = IgnorePatterns::new();
assert!(!patterns.show_gitignored());
patterns.toggle_show_gitignored();
assert!(patterns.show_gitignored());
patterns.set_show_gitignored(false);
assert!(!patterns.show_gitignored());
}
#[test]
fn test_multiple_gitignores() -> std::io::Result<()> {
let temp_root = TempDir::new()?;
let sub_dir = temp_root.path().join("subdir");
fs::create_dir(&sub_dir)?;
let mut root_gitignore = fs::File::create(temp_root.path().join(".gitignore"))?;
writeln!(root_gitignore, "*.tmp")?;
let mut sub_gitignore = fs::File::create(sub_dir.join(".gitignore"))?;
writeln!(sub_gitignore, "*.bak")?;
let mut patterns = IgnorePatterns::new();
patterns.load_gitignore(temp_root.path())?;
patterns.load_gitignore(&sub_dir)?;
assert_eq!(patterns.gitignore_count(), 2);
Ok(())
}
}