1use std::path::Path;
4
5use ignore::gitignore::{Gitignore, GitignoreBuilder};
6use tracing::debug;
7
8#[derive(Debug)]
10pub struct GitignoreFilter {
11 gitignore: Gitignore
13}
14
15impl GitignoreFilter {
16 #[must_use]
20 pub fn new(repo_path: &Path) -> Self {
21 let gitignore_path = repo_path.join(".gitignore");
22 let mut builder = GitignoreBuilder::new(repo_path);
23
24 if gitignore_path.exists()
25 && let Some(err) = builder.add(&gitignore_path)
26 {
27 debug!(error = %err, "error parsing .gitignore");
28 }
29
30 let gitignore = builder.build().unwrap_or_else(|_| {
31 GitignoreBuilder::new(repo_path)
33 .build()
34 .unwrap_or_else(|_| Gitignore::empty())
35 });
36
37 Self { gitignore }
38 }
39
40 #[must_use]
42 pub fn is_ignored(&self, path: &Path) -> bool {
43 let is_dir = path.is_dir();
44 self.gitignore.matched_path_or_any_parents(path, is_dir).is_ignore()
45 }
46}
47
48#[cfg(test)]
49#[allow(clippy::expect_used)]
50mod tests {
51 use std::path::PathBuf;
52
53 use super::*;
54
55 #[test]
56 fn test_empty_filter() {
57 let filter = GitignoreFilter::new(Path::new("/nonexistent"));
58 assert!(!filter.is_ignored(&PathBuf::from("/nonexistent/file.txt")));
60 }
61
62 #[test]
63 fn test_gitignore_pattern() {
64 let tmp = tempfile::tempdir().expect("tempdir");
65 let path = tmp.path();
66 std::fs::write(path.join(".gitignore"), "*.log\n").expect("write gitignore");
67
68 let filter = GitignoreFilter::new(path);
69 assert!(
70 filter.is_ignored(&path.join("test.log")),
71 "*.log should ignore test.log"
72 );
73 }
74
75 #[test]
76 fn test_gitignore_allows_unmatched() {
77 let tmp = tempfile::tempdir().expect("tempdir");
78 let path = tmp.path();
79 std::fs::write(path.join(".gitignore"), "*.log\n").expect("write gitignore");
80
81 let filter = GitignoreFilter::new(path);
82 assert!(
83 !filter.is_ignored(&path.join("file.txt")),
84 "file.txt should not be ignored"
85 );
86 }
87
88 #[test]
89 fn test_gitignore_dir_pattern() {
90 let tmp = tempfile::tempdir().expect("tempdir");
91 let path = tmp.path();
92 std::fs::write(path.join(".gitignore"), "build/\n").expect("write gitignore");
93
94 std::fs::create_dir(path.join("build")).expect("mkdir build");
96 std::fs::write(path.join("build/output.js"), "data").expect("write file");
97
98 let filter = GitignoreFilter::new(path);
99 assert!(filter.is_ignored(&path.join("build")), "build/ should be ignored");
100 }
101
102 #[test]
104 fn test_macos_dotfiles_ignored() {
105 let tmp = tempfile::tempdir().expect("tempdir");
106 let path = tmp.path();
107 std::fs::write(path.join(".gitignore"), ".DS_Store\n._*\n").expect("write");
108
109 let filter = GitignoreFilter::new(path);
110 assert!(
111 filter.is_ignored(&path.join(".DS_Store")),
112 ".DS_Store should be ignored"
113 );
114 assert!(
115 filter.is_ignored(&path.join("._test.txt")),
116 "._test.txt should be ignored"
117 );
118 assert!(
119 !filter.is_ignored(&path.join("normal.txt")),
120 "normal.txt should not be ignored"
121 );
122 }
123
124 #[test]
126 fn test_editor_temp_files_ignored() {
127 let tmp = tempfile::tempdir().expect("tempdir");
128 let path = tmp.path();
129 std::fs::write(path.join(".gitignore"), "*.swp\n*~\n").expect("write");
130
131 let filter = GitignoreFilter::new(path);
132 assert!(filter.is_ignored(&path.join(".file.swp")), ".swp should be ignored");
133 assert!(filter.is_ignored(&path.join("file.txt~")), "backup~ should be ignored");
134 assert!(
135 !filter.is_ignored(&path.join("file.txt")),
136 "file.txt should not be ignored"
137 );
138 }
139
140 #[test]
142 fn test_multiple_gitignore_patterns() {
143 let tmp = tempfile::tempdir().expect("tempdir");
144 let path = tmp.path();
145 std::fs::write(path.join(".gitignore"), "*.log\n*.tmp\ntarget/\n").expect("write");
146
147 std::fs::create_dir(path.join("target")).expect("mkdir");
149
150 let filter = GitignoreFilter::new(path);
151 assert!(
152 filter.is_ignored(&path.join("debug.log")),
153 "debug.log should be ignored"
154 );
155 assert!(filter.is_ignored(&path.join("temp.tmp")), "temp.tmp should be ignored");
156 assert!(filter.is_ignored(&path.join("target")), "target/ should be ignored");
157 assert!(
158 !filter.is_ignored(&path.join("src/main.rs")),
159 "main.rs should not be ignored"
160 );
161 }
162
163 #[test]
167 fn test_is_hidden_path() {
168 let tmp = tempfile::tempdir().expect("tempdir");
169 let path = tmp.path();
170 std::fs::write(path.join(".gitignore"), ".git/\n").expect("write gitignore");
171
172 std::fs::create_dir(path.join(".git")).expect("mkdir .git");
174
175 let filter = GitignoreFilter::new(path);
176 assert!(
177 filter.is_ignored(&path.join(".git")),
178 ".git directory should be hidden (filtered)"
179 );
180 assert!(
181 !filter.is_ignored(&path.join(".gitignore")),
182 ".gitignore should NOT be hidden (it is not .git/)"
183 );
184 assert!(
185 !filter.is_ignored(&path.join(".gitkeep")),
186 ".gitkeep should NOT be hidden (it is not .git/)"
187 );
188 }
189
190 #[test]
193 fn test_gitignore_combined_patterns() {
194 let tmp = tempfile::tempdir().expect("tempdir");
195 let path = tmp.path();
196 std::fs::write(path.join(".gitignore"), "*.log\n*.tmp\n.DS_Store\n").expect("write");
197
198 let filter = GitignoreFilter::new(path);
199
200 assert!(
202 filter.is_ignored(&path.join("test.log")),
203 "test.log should be ignored (pattern *.log)"
204 );
205 assert!(
206 filter.is_ignored(&path.join("test.tmp")),
207 "test.tmp should be ignored (pattern *.tmp)"
208 );
209 assert!(
210 filter.is_ignored(&path.join(".DS_Store")),
211 ".DS_Store should be ignored (exact match)"
212 );
213 assert!(
214 filter.is_ignored(&path.join("subdir/deep.log")),
215 "subdir/deep.log should be ignored (*.log recursive)"
216 );
217 assert!(
218 filter.is_ignored(&path.join("cache/session.tmp")),
219 "cache/session.tmp should be ignored (*.tmp recursive)"
220 );
221
222 assert!(
224 !filter.is_ignored(&path.join("README.md")),
225 "README.md should NOT be ignored"
226 );
227 assert!(
228 !filter.is_ignored(&path.join("src/main.rs")),
229 "src/main.rs should NOT be ignored"
230 );
231 assert!(
232 !filter.is_ignored(&path.join("test.txt")),
233 "test.txt should NOT be ignored"
234 );
235 assert!(
236 !filter.is_ignored(&path.join("logger.rs")),
237 "logger.rs should NOT be ignored (contains 'log' but not as extension)"
238 );
239 }
240}