1use std::collections::HashMap;
17use std::ffi::{OsStr, OsString};
18use std::fs::{File, FileType};
19use std::io::{self, BufRead};
20use std::path::{Path, PathBuf};
21use std::sync::{Arc, RwLock};
22
23use crate::gitignore::{self, Gitignore, GitignoreBuilder};
24use crate::overrides::{self, Override};
25use crate::pathutil::{is_hidden, strip_prefix};
26use crate::types::{self, Types};
27use crate::walk::DirEntry;
28use crate::{Error, Match, PartialErrorBuilder};
29
30#[derive(Clone, Debug)]
33pub struct IgnoreMatch<'a>(IgnoreMatchInner<'a>);
34
35#[derive(Clone, Debug)]
38enum IgnoreMatchInner<'a> {
39 Override(overrides::Glob<'a>),
40 Gitignore(&'a gitignore::Glob),
41 Types(types::Glob<'a>),
42 Hidden,
43}
44
45impl<'a> IgnoreMatch<'a> {
46 fn overrides(x: overrides::Glob<'a>) -> IgnoreMatch<'a> {
47 IgnoreMatch(IgnoreMatchInner::Override(x))
48 }
49
50 fn gitignore(x: &'a gitignore::Glob) -> IgnoreMatch<'a> {
51 IgnoreMatch(IgnoreMatchInner::Gitignore(x))
52 }
53
54 fn types(x: types::Glob<'a>) -> IgnoreMatch<'a> {
55 IgnoreMatch(IgnoreMatchInner::Types(x))
56 }
57
58 fn hidden() -> IgnoreMatch<'static> {
59 IgnoreMatch(IgnoreMatchInner::Hidden)
60 }
61}
62
63#[derive(Clone, Copy, Debug)]
66struct IgnoreOptions {
67 hidden: bool,
69 ignore: bool,
71 parents: bool,
73 git_global: bool,
75 git_ignore: bool,
77 git_exclude: bool,
79 ignore_case_insensitive: bool,
81 require_git: bool,
84}
85
86#[derive(Clone, Debug)]
88pub struct Ignore(Arc<IgnoreInner>);
89
90#[derive(Clone, Debug)]
91struct IgnoreInner {
92 compiled: Arc<RwLock<HashMap<OsString, Ignore>>>,
99 dir: PathBuf,
101 overrides: Arc<Override>,
103 types: Arc<Types>,
105 parent: Option<Ignore>,
110 is_absolute_parent: bool,
112 absolute_base: Option<Arc<PathBuf>>,
115 explicit_ignores: Arc<Vec<Gitignore>>,
117 custom_ignore_filenames: Arc<Vec<OsString>>,
119 custom_ignore_matcher: Gitignore,
121 ignore_matcher: Gitignore,
123 git_global_matcher: Arc<Gitignore>,
125 git_ignore_matcher: Gitignore,
127 git_exclude_matcher: Gitignore,
129 has_git: bool,
131 opts: IgnoreOptions,
133}
134
135impl Ignore {
136 pub fn path(&self) -> &Path {
138 &self.0.dir
139 }
140
141 pub fn is_root(&self) -> bool {
143 self.0.parent.is_none()
144 }
145
146 pub fn is_absolute_parent(&self) -> bool {
148 self.0.is_absolute_parent
149 }
150
151 pub fn parent(&self) -> Option<Ignore> {
153 self.0.parent.clone()
154 }
155
156 pub fn add_parents<P: AsRef<Path>>(
161 &self,
162 path: P,
163 ) -> (Ignore, Option<Error>) {
164 if !self.0.opts.parents
165 && !self.0.opts.git_ignore
166 && !self.0.opts.git_exclude
167 && !self.0.opts.git_global
168 {
169 return (self.clone(), None);
172 }
173 if !self.is_root() {
174 panic!("Ignore::add_parents called on non-root matcher");
175 }
176 let absolute_base = match path.as_ref().canonicalize() {
177 Ok(path) => Arc::new(path),
178 Err(_) => {
179 return (self.clone(), None);
184 }
185 };
186 let mut parents = vec![];
188 let mut path = &**absolute_base;
189 while let Some(parent) = path.parent() {
190 parents.push(parent);
191 path = parent;
192 }
193 let mut errs = PartialErrorBuilder::default();
194 let mut ig = self.clone();
195 for parent in parents.into_iter().rev() {
196 let mut compiled = self.0.compiled.write().unwrap();
197 if let Some(prebuilt) = compiled.get(parent.as_os_str()) {
198 ig = prebuilt.clone();
199 continue;
200 }
201 let (mut igtmp, err) = ig.add_child_path(parent);
202 errs.maybe_push(err);
203 igtmp.is_absolute_parent = true;
204 igtmp.absolute_base = Some(absolute_base.clone());
205 igtmp.has_git =
206 if self.0.opts.require_git && self.0.opts.git_ignore {
207 parent.join(".git").exists()
208 } else {
209 false
210 };
211 ig = Ignore(Arc::new(igtmp));
212 compiled.insert(parent.as_os_str().to_os_string(), ig.clone());
213 }
214 (ig, errs.into_error_option())
215 }
216
217 pub fn add_child<P: AsRef<Path>>(
226 &self,
227 dir: P,
228 ) -> (Ignore, Option<Error>) {
229 let (ig, err) = self.add_child_path(dir.as_ref());
230 (Ignore(Arc::new(ig)), err)
231 }
232
233 fn add_child_path(&self, dir: &Path) -> (IgnoreInner, Option<Error>) {
235 let git_type = if self.0.opts.require_git
236 && (self.0.opts.git_ignore || self.0.opts.git_exclude)
237 {
238 dir.join(".git").metadata().ok().map(|md| md.file_type())
239 } else {
240 None
241 };
242 let has_git = git_type.map(|_| true).unwrap_or(false);
243
244 let mut errs = PartialErrorBuilder::default();
245 let custom_ig_matcher = if self.0.custom_ignore_filenames.is_empty() {
246 Gitignore::empty()
247 } else {
248 let (m, err) = create_gitignore(
249 &dir,
250 &dir,
251 &self.0.custom_ignore_filenames,
252 self.0.opts.ignore_case_insensitive,
253 );
254 errs.maybe_push(err);
255 m
256 };
257 let ig_matcher = if !self.0.opts.ignore {
258 Gitignore::empty()
259 } else {
260 let (m, err) = create_gitignore(
261 &dir,
262 &dir,
263 &[".ignore"],
264 self.0.opts.ignore_case_insensitive,
265 );
266 errs.maybe_push(err);
267 m
268 };
269 let gi_matcher = if !self.0.opts.git_ignore {
270 Gitignore::empty()
271 } else {
272 let (m, err) = create_gitignore(
273 &dir,
274 &dir,
275 &[".gitignore"],
276 self.0.opts.ignore_case_insensitive,
277 );
278 errs.maybe_push(err);
279 m
280 };
281 let gi_exclude_matcher = if !self.0.opts.git_exclude {
282 Gitignore::empty()
283 } else {
284 match resolve_git_commondir(dir, git_type) {
285 Ok(git_dir) => {
286 let (m, err) = create_gitignore(
287 &dir,
288 &git_dir,
289 &["info/exclude"],
290 self.0.opts.ignore_case_insensitive,
291 );
292 errs.maybe_push(err);
293 m
294 }
295 Err(err) => {
296 errs.maybe_push(err);
297 Gitignore::empty()
298 }
299 }
300 };
301 let ig = IgnoreInner {
302 compiled: self.0.compiled.clone(),
303 dir: dir.to_path_buf(),
304 overrides: self.0.overrides.clone(),
305 types: self.0.types.clone(),
306 parent: Some(self.clone()),
307 is_absolute_parent: false,
308 absolute_base: self.0.absolute_base.clone(),
309 explicit_ignores: self.0.explicit_ignores.clone(),
310 custom_ignore_filenames: self.0.custom_ignore_filenames.clone(),
311 custom_ignore_matcher: custom_ig_matcher,
312 ignore_matcher: ig_matcher,
313 git_global_matcher: self.0.git_global_matcher.clone(),
314 git_ignore_matcher: gi_matcher,
315 git_exclude_matcher: gi_exclude_matcher,
316 has_git,
317 opts: self.0.opts,
318 };
319 (ig, errs.into_error_option())
320 }
321
322 fn has_any_ignore_rules(&self) -> bool {
324 let opts = self.0.opts;
325 let has_custom_ignore_files =
326 !self.0.custom_ignore_filenames.is_empty();
327 let has_explicit_ignores = !self.0.explicit_ignores.is_empty();
328
329 opts.ignore
330 || opts.git_global
331 || opts.git_ignore
332 || opts.git_exclude
333 || has_custom_ignore_files
334 || has_explicit_ignores
335 }
336
337 pub fn matched_dir_entry<'a>(
339 &'a self,
340 dent: &DirEntry,
341 ) -> Match<IgnoreMatch<'a>> {
342 let m = self.matched(dent.path(), dent.is_dir());
343 if m.is_none() && self.0.opts.hidden && is_hidden(dent) {
344 return Match::Ignore(IgnoreMatch::hidden());
345 }
346 m
347 }
348
349 fn matched<'a, P: AsRef<Path>>(
354 &'a self,
355 path: P,
356 is_dir: bool,
357 ) -> Match<IgnoreMatch<'a>> {
358 let mut path = path.as_ref();
361 if let Some(p) = strip_prefix("./", path) {
362 path = p;
363 }
364 if !self.0.overrides.is_empty() {
369 let mat = self
370 .0
371 .overrides
372 .matched(path, is_dir)
373 .map(IgnoreMatch::overrides);
374 if !mat.is_none() {
375 return mat;
376 }
377 }
378 let mut whitelisted = Match::None;
379 if self.has_any_ignore_rules() {
380 let mat = self.matched_ignore(path, is_dir);
381 if mat.is_ignore() {
382 return mat;
383 } else if mat.is_whitelist() {
384 whitelisted = mat;
385 }
386 }
387 if !self.0.types.is_empty() {
388 let mat =
389 self.0.types.matched(path, is_dir).map(IgnoreMatch::types);
390 if mat.is_ignore() {
391 return mat;
392 } else if mat.is_whitelist() {
393 whitelisted = mat;
394 }
395 }
396 whitelisted
397 }
398
399 fn matched_ignore<'a>(
402 &'a self,
403 path: &Path,
404 is_dir: bool,
405 ) -> Match<IgnoreMatch<'a>> {
406 let (
407 mut m_custom_ignore,
408 mut m_ignore,
409 mut m_gi,
410 mut m_gi_exclude,
411 mut m_explicit,
412 ) = (Match::None, Match::None, Match::None, Match::None, Match::None);
413 let any_git =
414 !self.0.opts.require_git || self.parents().any(|ig| ig.0.has_git);
415 let mut saw_git = false;
416 for ig in self.parents().take_while(|ig| !ig.0.is_absolute_parent) {
417 if m_custom_ignore.is_none() {
418 m_custom_ignore =
419 ig.0.custom_ignore_matcher
420 .matched(path, is_dir)
421 .map(IgnoreMatch::gitignore);
422 }
423 if m_ignore.is_none() {
424 m_ignore =
425 ig.0.ignore_matcher
426 .matched(path, is_dir)
427 .map(IgnoreMatch::gitignore);
428 }
429 if any_git && !saw_git && m_gi.is_none() {
430 m_gi =
431 ig.0.git_ignore_matcher
432 .matched(path, is_dir)
433 .map(IgnoreMatch::gitignore);
434 }
435 if any_git && !saw_git && m_gi_exclude.is_none() {
436 m_gi_exclude =
437 ig.0.git_exclude_matcher
438 .matched(path, is_dir)
439 .map(IgnoreMatch::gitignore);
440 }
441 saw_git = saw_git || ig.0.has_git;
442 }
443 if self.0.opts.parents {
444 if let Some(abs_parent_path) = self.absolute_base() {
445 let path = abs_parent_path.join(path);
446 for ig in
447 self.parents().skip_while(|ig| !ig.0.is_absolute_parent)
448 {
449 if m_custom_ignore.is_none() {
450 m_custom_ignore =
451 ig.0.custom_ignore_matcher
452 .matched(&path, is_dir)
453 .map(IgnoreMatch::gitignore);
454 }
455 if m_ignore.is_none() {
456 m_ignore =
457 ig.0.ignore_matcher
458 .matched(&path, is_dir)
459 .map(IgnoreMatch::gitignore);
460 }
461 if any_git && !saw_git && m_gi.is_none() {
462 m_gi =
463 ig.0.git_ignore_matcher
464 .matched(&path, is_dir)
465 .map(IgnoreMatch::gitignore);
466 }
467 if any_git && !saw_git && m_gi_exclude.is_none() {
468 m_gi_exclude =
469 ig.0.git_exclude_matcher
470 .matched(&path, is_dir)
471 .map(IgnoreMatch::gitignore);
472 }
473 saw_git = saw_git || ig.0.has_git;
474 }
475 }
476 }
477 for gi in self.0.explicit_ignores.iter().rev() {
478 if !m_explicit.is_none() {
479 break;
480 }
481 m_explicit = gi.matched(&path, is_dir).map(IgnoreMatch::gitignore);
482 }
483 let m_global = if any_git {
484 self.0
485 .git_global_matcher
486 .matched(&path, is_dir)
487 .map(IgnoreMatch::gitignore)
488 } else {
489 Match::None
490 };
491
492 m_custom_ignore
493 .or(m_ignore)
494 .or(m_gi)
495 .or(m_gi_exclude)
496 .or(m_global)
497 .or(m_explicit)
498 }
499
500 pub fn parents(&self) -> Parents<'_> {
502 Parents(Some(self))
503 }
504
505 fn absolute_base(&self) -> Option<&Path> {
508 self.0.absolute_base.as_ref().map(|p| &***p)
509 }
510}
511
512pub struct Parents<'a>(Option<&'a Ignore>);
516
517impl<'a> Iterator for Parents<'a> {
518 type Item = &'a Ignore;
519
520 fn next(&mut self) -> Option<&'a Ignore> {
521 match self.0.take() {
522 None => None,
523 Some(ig) => {
524 self.0 = ig.0.parent.as_ref();
525 Some(ig)
526 }
527 }
528 }
529}
530
531#[derive(Clone, Debug)]
533pub struct IgnoreBuilder {
534 dir: PathBuf,
536 overrides: Arc<Override>,
538 types: Arc<Types>,
540 explicit_ignores: Vec<Gitignore>,
542 custom_ignore_filenames: Vec<OsString>,
544 opts: IgnoreOptions,
546}
547
548impl IgnoreBuilder {
549 pub fn new() -> IgnoreBuilder {
554 IgnoreBuilder {
555 dir: Path::new("").to_path_buf(),
556 overrides: Arc::new(Override::empty()),
557 types: Arc::new(Types::empty()),
558 explicit_ignores: vec![],
559 custom_ignore_filenames: vec![],
560 opts: IgnoreOptions {
561 hidden: true,
562 ignore: true,
563 parents: true,
564 git_global: true,
565 git_ignore: true,
566 git_exclude: true,
567 ignore_case_insensitive: false,
568 require_git: true,
569 },
570 }
571 }
572
573 pub fn build(&self) -> Ignore {
578 let git_global_matcher = if !self.opts.git_global {
579 Gitignore::empty()
580 } else {
581 let mut builder = GitignoreBuilder::new("");
582 builder
583 .case_insensitive(self.opts.ignore_case_insensitive)
584 .unwrap();
585 let (gi, err) = builder.build_global();
586 if let Some(err) = err {
587 log::debug!("{}", err);
588 }
589 gi
590 };
591
592 Ignore(Arc::new(IgnoreInner {
593 compiled: Arc::new(RwLock::new(HashMap::new())),
594 dir: self.dir.clone(),
595 overrides: self.overrides.clone(),
596 types: self.types.clone(),
597 parent: None,
598 is_absolute_parent: true,
599 absolute_base: None,
600 explicit_ignores: Arc::new(self.explicit_ignores.clone()),
601 custom_ignore_filenames: Arc::new(
602 self.custom_ignore_filenames.clone(),
603 ),
604 custom_ignore_matcher: Gitignore::empty(),
605 ignore_matcher: Gitignore::empty(),
606 git_global_matcher: Arc::new(git_global_matcher),
607 git_ignore_matcher: Gitignore::empty(),
608 git_exclude_matcher: Gitignore::empty(),
609 has_git: false,
610 opts: self.opts,
611 }))
612 }
613
614 pub fn overrides(&mut self, overrides: Override) -> &mut IgnoreBuilder {
620 self.overrides = Arc::new(overrides);
621 self
622 }
623
624 pub fn types(&mut self, types: Types) -> &mut IgnoreBuilder {
630 self.types = Arc::new(types);
631 self
632 }
633
634 pub fn add_ignore(&mut self, ig: Gitignore) -> &mut IgnoreBuilder {
636 self.explicit_ignores.push(ig);
637 self
638 }
639
640 pub fn add_custom_ignore_filename<S: AsRef<OsStr>>(
647 &mut self,
648 file_name: S,
649 ) -> &mut IgnoreBuilder {
650 self.custom_ignore_filenames.push(file_name.as_ref().to_os_string());
651 self
652 }
653
654 pub fn hidden(&mut self, yes: bool) -> &mut IgnoreBuilder {
658 self.opts.hidden = yes;
659 self
660 }
661
662 pub fn ignore(&mut self, yes: bool) -> &mut IgnoreBuilder {
669 self.opts.ignore = yes;
670 self
671 }
672
673 pub fn parents(&mut self, yes: bool) -> &mut IgnoreBuilder {
680 self.opts.parents = yes;
681 self
682 }
683
684 pub fn git_global(&mut self, yes: bool) -> &mut IgnoreBuilder {
693 self.opts.git_global = yes;
694 self
695 }
696
697 pub fn git_ignore(&mut self, yes: bool) -> &mut IgnoreBuilder {
704 self.opts.git_ignore = yes;
705 self
706 }
707
708 pub fn git_exclude(&mut self, yes: bool) -> &mut IgnoreBuilder {
715 self.opts.git_exclude = yes;
716 self
717 }
718
719 pub fn require_git(&mut self, yes: bool) -> &mut IgnoreBuilder {
725 self.opts.require_git = yes;
726 self
727 }
728
729 pub fn ignore_case_insensitive(
733 &mut self,
734 yes: bool,
735 ) -> &mut IgnoreBuilder {
736 self.opts.ignore_case_insensitive = yes;
737 self
738 }
739}
740
741pub fn create_gitignore<T: AsRef<OsStr>>(
750 dir: &Path,
751 dir_for_ignorefile: &Path,
752 names: &[T],
753 case_insensitive: bool,
754) -> (Gitignore, Option<Error>) {
755 let mut builder = GitignoreBuilder::new(dir);
756 let mut errs = PartialErrorBuilder::default();
757 builder.case_insensitive(case_insensitive).unwrap();
758 for name in names {
759 let gipath = dir_for_ignorefile.join(name.as_ref());
760 if cfg!(windows) || gipath.exists() {
773 errs.maybe_push_ignore_io(builder.add(gipath));
774 }
775 }
776 let gi = match builder.build() {
777 Ok(gi) => gi,
778 Err(err) => {
779 errs.push(err);
780 GitignoreBuilder::new(dir).build().unwrap()
781 }
782 };
783 (gi, errs.into_error_option())
784}
785
786fn resolve_git_commondir(
795 dir: &Path,
796 git_type: Option<FileType>,
797) -> Result<PathBuf, Option<Error>> {
798 let git_dir_path = || dir.join(".git");
799 let git_dir = git_dir_path();
800 if !git_type.map_or(false, |ft| ft.is_file()) {
801 return Ok(git_dir);
802 }
803 let file = match File::open(git_dir) {
804 Ok(file) => io::BufReader::new(file),
805 Err(err) => {
806 return Err(Some(Error::Io(err).with_path(git_dir_path())));
807 }
808 };
809 let dot_git_line = match file.lines().next() {
810 Some(Ok(line)) => line,
811 Some(Err(err)) => {
812 return Err(Some(Error::Io(err).with_path(git_dir_path())));
813 }
814 None => return Err(None),
815 };
816 if !dot_git_line.starts_with("gitdir: ") {
817 return Err(None);
818 }
819 let real_git_dir = PathBuf::from(&dot_git_line["gitdir: ".len()..]);
820 let git_commondir_file = || real_git_dir.join("commondir");
821 let file = match File::open(git_commondir_file()) {
822 Ok(file) => io::BufReader::new(file),
823 Err(_) => return Err(None),
824 };
825 let commondir_line = match file.lines().next() {
826 Some(Ok(line)) => line,
827 Some(Err(err)) => {
828 return Err(Some(Error::Io(err).with_path(git_commondir_file())));
829 }
830 None => return Err(None),
831 };
832 let commondir_abs = if commondir_line.starts_with(".") {
833 real_git_dir.join(commondir_line) } else {
835 PathBuf::from(commondir_line)
836 };
837 Ok(commondir_abs)
838}
839
840#[cfg(test)]
841mod tests {
842 use std::fs::{self, File};
843 use std::io::Write;
844 use std::path::Path;
845
846 use crate::dir::IgnoreBuilder;
847 use crate::gitignore::Gitignore;
848 use crate::tests::TempDir;
849 use crate::Error;
850
851 fn wfile<P: AsRef<Path>>(path: P, contents: &str) {
852 let mut file = File::create(path).unwrap();
853 file.write_all(contents.as_bytes()).unwrap();
854 }
855
856 fn mkdirp<P: AsRef<Path>>(path: P) {
857 fs::create_dir_all(path).unwrap();
858 }
859
860 fn partial(err: Error) -> Vec<Error> {
861 match err {
862 Error::Partial(errs) => errs,
863 _ => panic!("expected partial error but got {:?}", err),
864 }
865 }
866
867 fn tmpdir() -> TempDir {
868 TempDir::new().unwrap()
869 }
870
871 #[test]
872 fn explicit_ignore() {
873 let td = tmpdir();
874 wfile(td.path().join("not-an-ignore"), "foo\n!bar");
875
876 let (gi, err) = Gitignore::new(td.path().join("not-an-ignore"));
877 assert!(err.is_none());
878 let (ig, err) =
879 IgnoreBuilder::new().add_ignore(gi).build().add_child(td.path());
880 assert!(err.is_none());
881 assert!(ig.matched("foo", false).is_ignore());
882 assert!(ig.matched("bar", false).is_whitelist());
883 assert!(ig.matched("baz", false).is_none());
884 }
885
886 #[test]
887 fn git_exclude() {
888 let td = tmpdir();
889 mkdirp(td.path().join(".git/info"));
890 wfile(td.path().join(".git/info/exclude"), "foo\n!bar");
891
892 let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
893 assert!(err.is_none());
894 assert!(ig.matched("foo", false).is_ignore());
895 assert!(ig.matched("bar", false).is_whitelist());
896 assert!(ig.matched("baz", false).is_none());
897 }
898
899 #[test]
900 fn gitignore() {
901 let td = tmpdir();
902 mkdirp(td.path().join(".git"));
903 wfile(td.path().join(".gitignore"), "foo\n!bar");
904
905 let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
906 assert!(err.is_none());
907 assert!(ig.matched("foo", false).is_ignore());
908 assert!(ig.matched("bar", false).is_whitelist());
909 assert!(ig.matched("baz", false).is_none());
910 }
911
912 #[test]
913 fn gitignore_no_git() {
914 let td = tmpdir();
915 wfile(td.path().join(".gitignore"), "foo\n!bar");
916
917 let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
918 assert!(err.is_none());
919 assert!(ig.matched("foo", false).is_none());
920 assert!(ig.matched("bar", false).is_none());
921 assert!(ig.matched("baz", false).is_none());
922 }
923
924 #[test]
925 fn gitignore_allowed_no_git() {
926 let td = tmpdir();
927 wfile(td.path().join(".gitignore"), "foo\n!bar");
928
929 let (ig, err) = IgnoreBuilder::new()
930 .require_git(false)
931 .build()
932 .add_child(td.path());
933 assert!(err.is_none());
934 assert!(ig.matched("foo", false).is_ignore());
935 assert!(ig.matched("bar", false).is_whitelist());
936 assert!(ig.matched("baz", false).is_none());
937 }
938
939 #[test]
940 fn ignore() {
941 let td = tmpdir();
942 wfile(td.path().join(".ignore"), "foo\n!bar");
943
944 let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
945 assert!(err.is_none());
946 assert!(ig.matched("foo", false).is_ignore());
947 assert!(ig.matched("bar", false).is_whitelist());
948 assert!(ig.matched("baz", false).is_none());
949 }
950
951 #[test]
952 fn custom_ignore() {
953 let td = tmpdir();
954 let custom_ignore = ".customignore";
955 wfile(td.path().join(custom_ignore), "foo\n!bar");
956
957 let (ig, err) = IgnoreBuilder::new()
958 .add_custom_ignore_filename(custom_ignore)
959 .build()
960 .add_child(td.path());
961 assert!(err.is_none());
962 assert!(ig.matched("foo", false).is_ignore());
963 assert!(ig.matched("bar", false).is_whitelist());
964 assert!(ig.matched("baz", false).is_none());
965 }
966
967 #[test]
969 fn custom_ignore_over_ignore() {
970 let td = tmpdir();
971 let custom_ignore = ".customignore";
972 wfile(td.path().join(".ignore"), "foo");
973 wfile(td.path().join(custom_ignore), "!foo");
974
975 let (ig, err) = IgnoreBuilder::new()
976 .add_custom_ignore_filename(custom_ignore)
977 .build()
978 .add_child(td.path());
979 assert!(err.is_none());
980 assert!(ig.matched("foo", false).is_whitelist());
981 }
982
983 #[test]
985 fn custom_ignore_precedence() {
986 let td = tmpdir();
987 let custom_ignore1 = ".customignore1";
988 let custom_ignore2 = ".customignore2";
989 wfile(td.path().join(custom_ignore1), "foo");
990 wfile(td.path().join(custom_ignore2), "!foo");
991
992 let (ig, err) = IgnoreBuilder::new()
993 .add_custom_ignore_filename(custom_ignore1)
994 .add_custom_ignore_filename(custom_ignore2)
995 .build()
996 .add_child(td.path());
997 assert!(err.is_none());
998 assert!(ig.matched("foo", false).is_whitelist());
999 }
1000
1001 #[test]
1003 fn ignore_over_gitignore() {
1004 let td = tmpdir();
1005 wfile(td.path().join(".gitignore"), "foo");
1006 wfile(td.path().join(".ignore"), "!foo");
1007
1008 let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
1009 assert!(err.is_none());
1010 assert!(ig.matched("foo", false).is_whitelist());
1011 }
1012
1013 #[test]
1015 fn exclude_lowest() {
1016 let td = tmpdir();
1017 wfile(td.path().join(".gitignore"), "!foo");
1018 wfile(td.path().join(".ignore"), "!bar");
1019 mkdirp(td.path().join(".git/info"));
1020 wfile(td.path().join(".git/info/exclude"), "foo\nbar\nbaz");
1021
1022 let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
1023 assert!(err.is_none());
1024 assert!(ig.matched("baz", false).is_ignore());
1025 assert!(ig.matched("foo", false).is_whitelist());
1026 assert!(ig.matched("bar", false).is_whitelist());
1027 }
1028
1029 #[test]
1030 fn errored() {
1031 let td = tmpdir();
1032 wfile(td.path().join(".gitignore"), "{foo");
1033
1034 let (_, err) = IgnoreBuilder::new().build().add_child(td.path());
1035 assert!(err.is_some());
1036 }
1037
1038 #[test]
1039 fn errored_both() {
1040 let td = tmpdir();
1041 wfile(td.path().join(".gitignore"), "{foo");
1042 wfile(td.path().join(".ignore"), "{bar");
1043
1044 let (_, err) = IgnoreBuilder::new().build().add_child(td.path());
1045 assert_eq!(2, partial(err.expect("an error")).len());
1046 }
1047
1048 #[test]
1049 fn errored_partial() {
1050 let td = tmpdir();
1051 mkdirp(td.path().join(".git"));
1052 wfile(td.path().join(".gitignore"), "{foo\nbar");
1053
1054 let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
1055 assert!(err.is_some());
1056 assert!(ig.matched("bar", false).is_ignore());
1057 }
1058
1059 #[test]
1060 fn errored_partial_and_ignore() {
1061 let td = tmpdir();
1062 wfile(td.path().join(".gitignore"), "{foo\nbar");
1063 wfile(td.path().join(".ignore"), "!bar");
1064
1065 let (ig, err) = IgnoreBuilder::new().build().add_child(td.path());
1066 assert!(err.is_some());
1067 assert!(ig.matched("bar", false).is_whitelist());
1068 }
1069
1070 #[test]
1071 fn not_present_empty() {
1072 let td = tmpdir();
1073
1074 let (_, err) = IgnoreBuilder::new().build().add_child(td.path());
1075 assert!(err.is_none());
1076 }
1077
1078 #[test]
1079 fn stops_at_git_dir() {
1080 let td = tmpdir();
1083 mkdirp(td.path().join(".git"));
1084 mkdirp(td.path().join("foo/.git"));
1085 wfile(td.path().join(".gitignore"), "foo");
1086 wfile(td.path().join(".ignore"), "bar");
1087
1088 let ig0 = IgnoreBuilder::new().build();
1089 let (ig1, err) = ig0.add_child(td.path());
1090 assert!(err.is_none());
1091 let (ig2, err) = ig1.add_child(ig1.path().join("foo"));
1092 assert!(err.is_none());
1093
1094 assert!(ig1.matched("foo", false).is_ignore());
1095 assert!(ig2.matched("foo", false).is_none());
1096
1097 assert!(ig1.matched("bar", false).is_ignore());
1098 assert!(ig2.matched("bar", false).is_ignore());
1099 }
1100
1101 #[test]
1102 fn absolute_parent() {
1103 let td = tmpdir();
1104 mkdirp(td.path().join(".git"));
1105 mkdirp(td.path().join("foo"));
1106 wfile(td.path().join(".gitignore"), "bar");
1107
1108 let ig0 = IgnoreBuilder::new().build();
1111 let (ig1, err) = ig0.add_child(td.path().join("foo"));
1112 assert!(err.is_none());
1113 assert!(ig1.matched("bar", false).is_none());
1114
1115 let ig0 = IgnoreBuilder::new().build();
1117 let (ig1, err) = ig0.add_parents(td.path().join("foo"));
1118 assert!(err.is_none());
1119 let (ig2, err) = ig1.add_child(td.path().join("foo"));
1120 assert!(err.is_none());
1121 assert!(ig2.matched("bar", false).is_ignore());
1122 }
1123
1124 #[test]
1125 fn absolute_parent_anchored() {
1126 let td = tmpdir();
1127 mkdirp(td.path().join(".git"));
1128 mkdirp(td.path().join("src/llvm"));
1129 wfile(td.path().join(".gitignore"), "/llvm/\nfoo");
1130
1131 let ig0 = IgnoreBuilder::new().build();
1132 let (ig1, err) = ig0.add_parents(td.path().join("src"));
1133 assert!(err.is_none());
1134 let (ig2, err) = ig1.add_child("src");
1135 assert!(err.is_none());
1136
1137 assert!(ig1.matched("llvm", true).is_none());
1138 assert!(ig2.matched("llvm", true).is_none());
1139 assert!(ig2.matched("src/llvm", true).is_none());
1140 assert!(ig2.matched("foo", false).is_ignore());
1141 assert!(ig2.matched("src/foo", false).is_ignore());
1142 }
1143
1144 #[test]
1145 fn git_info_exclude_in_linked_worktree() {
1146 let td = tmpdir();
1147 let git_dir = td.path().join(".git");
1148 mkdirp(git_dir.join("info"));
1149 wfile(git_dir.join("info/exclude"), "ignore_me");
1150 mkdirp(git_dir.join("worktrees/linked-worktree"));
1151 let commondir_path =
1152 || git_dir.join("worktrees/linked-worktree/commondir");
1153 mkdirp(td.path().join("linked-worktree"));
1154 let worktree_git_dir_abs = format!(
1155 "gitdir: {}",
1156 git_dir.join("worktrees/linked-worktree").to_str().unwrap(),
1157 );
1158 wfile(td.path().join("linked-worktree/.git"), &worktree_git_dir_abs);
1159
1160 wfile(commondir_path(), "../..");
1162 let ib = IgnoreBuilder::new().build();
1163 let (ignore, err) = ib.add_child(td.path().join("linked-worktree"));
1164 assert!(err.is_none());
1165 assert!(ignore.matched("ignore_me", false).is_ignore());
1166
1167 wfile(commondir_path(), git_dir.to_str().unwrap());
1169 let (ignore, err) = ib.add_child(td.path().join("linked-worktree"));
1170 assert!(err.is_none());
1171 assert!(ignore.matched("ignore_me", false).is_ignore());
1172
1173 assert!(fs::remove_file(commondir_path()).is_ok());
1175 let (_, err) = ib.add_child(td.path().join("linked-worktree"));
1176 assert!(err.is_none());
1179
1180 wfile(td.path().join("linked-worktree/.git"), "garbage");
1181 let (_, err) = ib.add_child(td.path().join("linked-worktree"));
1182 assert!(err.is_none());
1183
1184 wfile(td.path().join("linked-worktree/.git"), "gitdir: garbage");
1185 let (_, err) = ib.add_child(td.path().join("linked-worktree"));
1186 assert!(err.is_none());
1187 }
1188}