1use std::path::{absolute, Path};
2
3use anyhow::Result;
4use ignore::{
5 gitignore::{Gitignore, GitignoreBuilder},
6 Match,
7};
8use log::debug;
9use regex::Regex;
10
11pub struct PathFilter {
12 ignore_regex: Option<Regex>,
13 gitignore: Gitignore,
14 repo_path: std::path::PathBuf,
15}
16
17impl PathFilter {
18 pub fn new(repo_path: &Path, ignore_regex: Option<Regex>) -> Result<Self> {
19 Ok(Self {
20 ignore_regex,
21 gitignore: build_gitignore(repo_path)?,
22 repo_path: repo_path.to_path_buf(),
23 })
24 }
25
26 pub fn is_path_ignored(&self, path: &Path) -> bool {
27 let normalized_path = match absolute(path) {
28 Ok(path) => path,
29 Err(_) => return false,
30 };
31 let relative_path = match normalized_path.strip_prefix(&self.repo_path) {
33 Ok(path) => path,
34 Err(_) => return false,
35 };
36
37 if relative_path.starts_with(".git") {
38 return true;
39 }
40
41 if let Match::Ignore(_) = self
42 .gitignore
43 .matched_path_or_any_parents(relative_path, relative_path.is_dir())
44 {
45 debug!("Path {} ignored via .gitignore", path.display());
46 return true;
47 }
48
49 if let Some(regex) = &self.ignore_regex {
50 if regex.is_match(&relative_path.to_string_lossy()) {
51 debug!(
52 "Path {} ignored via --ignore-regex",
53 relative_path.display()
54 );
55 return true;
56 }
57 }
58 false
59 }
60}
61
62fn build_gitignore(repo_path: &Path) -> Result<Gitignore> {
63 let mut builder = GitignoreBuilder::new(repo_path);
64 let gitignore_path = repo_path.join(".gitignore");
65 if gitignore_path.exists() {
66 log::trace!("Using gitignore {}", gitignore_path.display());
67 builder.add(gitignore_path);
68 }
69
70 let gitignore = builder.build()?;
71 Ok(gitignore)
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use std::fs;
78 use tempfile::TempDir;
79
80 #[test]
81 fn test_gitignore() -> Result<()> {
82 let temp_dir = TempDir::new()?;
83 let repo_path = temp_dir.path();
84
85 fs::write(repo_path.join(".gitignore"), "*.ignored\nignored_dir/")?;
87
88 let path_filter = PathFilter::new(repo_path, None)?;
89
90 assert!(path_filter.is_path_ignored(&repo_path.join(".git/config")));
92 assert!(path_filter.is_path_ignored(&repo_path.join("test.ignored")));
93 assert!(path_filter.is_path_ignored(&repo_path.join("ignored_dir/file.txt")));
94
95 assert!(!path_filter.is_path_ignored(&repo_path.join("test.txt")));
97 assert!(!path_filter.is_path_ignored(&repo_path.join("allowed_dir/file.txt")));
98
99 Ok(())
100 }
101
102 #[test]
103 fn test_ignore_regex() -> Result<()> {
104 let temp_dir = TempDir::new()?;
105 let repo_path = temp_dir.path();
106
107 let ignore_regex = Some(Regex::new(".*\\.temp$")?);
108 let path_filter = PathFilter::new(repo_path, ignore_regex)?;
109
110 assert!(path_filter.is_path_ignored(&repo_path.join("test.temp")));
112 assert!(path_filter.is_path_ignored(&repo_path.join("subdir/another.temp")));
113
114 assert!(!path_filter.is_path_ignored(&repo_path.join("test.txt")));
116 assert!(!path_filter.is_path_ignored(&repo_path.join("temp.txt")));
117
118 Ok(())
119 }
120
121 #[test]
122 fn test_path_absolute_error() -> Result<()> {
123 let temp_dir = TempDir::new()?;
124 let repo_path = temp_dir.path();
125 let path_filter = PathFilter::new(repo_path, None)?;
126
127 let invalid_path = Path::new("\0invalid");
129
130 assert!(!path_filter.is_path_ignored(invalid_path));
131 Ok(())
132 }
133
134 #[test]
135 fn test_strip_prefix_error() -> Result<()> {
136 let temp_dir = TempDir::new()?;
137 let repo_path = temp_dir.path();
138 let path_filter = PathFilter::new(repo_path, None)?;
139
140 let outside_path = temp_dir.path().parent().unwrap().join("outside.txt");
142
143 assert!(!path_filter.is_path_ignored(&outside_path));
144 Ok(())
145 }
146}