1/*!
2The gitignore module provides a way to match globs from a gitignore file
3against file paths.
4
5Note that this module implements the specification as described in the
6`gitignore` man page from scratch. That is, this module does *not* shell out to
7the `git` command line tool.
8*/
9
10use std::cell::RefCell;
11use std::env;
12use std::fs::File;
13use std::io::{self, BufRead, Read};
14use std::path::{Path, PathBuf};
15use std::str;
16use std::sync::Arc;
17
18use globset::{Candidate, GlobBuilder, GlobSet, GlobSetBuilder};
19use regex::bytes::Regex;
20use thread_local::ThreadLocal;
21
22use crate::pathutil::{is_file_name, strip_prefix};
23use crate::{Error, Match, PartialErrorBuilder};
24
25/// Glob represents a single glob in a gitignore file.
26///
27/// This is used to report information about the highest precedent glob that
28/// matched in one or more gitignore files.
29#[derive(Clone, Debug)]
30pub struct Glob {
31 /// The file path that this glob was extracted from.
32 from: Option<PathBuf>,
33 /// The original glob string.
34 original: String,
35 /// The actual glob string used to convert to a regex.
36 actual: String,
37 /// Whether this is a whitelisted glob or not.
38 is_whitelist: bool,
39 /// Whether this glob should only match directories or not.
40 is_only_dir: bool,
41}
42
43impl Glob {
44 /// Returns the file path that defined this glob.
45 pub fn from(&self) -> Option<&Path> {
46 self.from.as_ref().map(|p| &**p)
47 }
48
49 /// The original glob as it was defined in a gitignore file.
50 pub fn original(&self) -> &str {
51 &self.original
52 }
53
54 /// The actual glob that was compiled to respect gitignore
55 /// semantics.
56 pub fn actual(&self) -> &str {
57 &self.actual
58 }
59
60 /// Whether this was a whitelisted glob or not.
61 pub fn is_whitelist(&self) -> bool {
62 self.is_whitelist
63 }
64
65 /// Whether this glob must match a directory or not.
66 pub fn is_only_dir(&self) -> bool {
67 self.is_only_dir
68 }
69
70 /// Returns true if and only if this glob has a `**/` prefix.
71 fn has_doublestar_prefix(&self) -> bool {
72 self.actual.starts_with("**/") || self.actual == "**"
73 }
74}
75
76/// Gitignore is a matcher for the globs in one or more gitignore files
77/// in the same directory.
78#[derive(Clone, Debug)]
79pub struct Gitignore {
80 set: GlobSet,
81 root: PathBuf,
82 globs: Vec<Glob>,
83 num_ignores: u64,
84 num_whitelists: u64,
85 matches: Option<Arc<ThreadLocal<RefCell<Vec<usize>>>>>,
86}
87
88impl Gitignore {
89 /// Creates a new gitignore matcher from the gitignore file path given.
90 ///
91 /// If it's desirable to include multiple gitignore files in a single
92 /// matcher, or read gitignore globs from a different source, then
93 /// use `GitignoreBuilder`.
94 ///
95 /// This always returns a valid matcher, even if it's empty. In particular,
96 /// a Gitignore file can be partially valid, e.g., when one glob is invalid
97 /// but the rest aren't.
98 ///
99 /// Note that I/O errors are ignored. For more granular control over
100 /// errors, use `GitignoreBuilder`.
101 pub fn new<P: AsRef<Path>>(
102 gitignore_path: P,
103 ) -> (Gitignore, Option<Error>) {
104 let path = gitignore_path.as_ref();
105 let parent = path.parent().unwrap_or(Path::new("/"));
106 let mut builder = GitignoreBuilder::new(parent);
107 let mut errs = PartialErrorBuilder::default();
108 errs.maybe_push_ignore_io(builder.add(path));
109 match builder.build() {
110 Ok(gi) => (gi, errs.into_error_option()),
111 Err(err) => {
112 errs.push(err);
113 (Gitignore::empty(), errs.into_error_option())
114 }
115 }
116 }
117
118 /// Creates a new gitignore matcher from the global ignore file, if one
119 /// exists.
120 ///
121 /// The global config file path is specified by git's `core.excludesFile`
122 /// config option.
123 ///
124 /// Git's config file location is `$HOME/.gitconfig`. If `$HOME/.gitconfig`
125 /// does not exist or does not specify `core.excludesFile`, then
126 /// `$XDG_CONFIG_HOME/git/ignore` is read. If `$XDG_CONFIG_HOME` is not
127 /// set or is empty, then `$HOME/.config/git/ignore` is used instead.
128 pub fn global() -> (Gitignore, Option<Error>) {
129 GitignoreBuilder::new("").build_global()
130 }
131
132 /// Creates a new empty gitignore matcher that never matches anything.
133 ///
134 /// Its path is empty.
135 pub fn empty() -> Gitignore {
136 Gitignore {
137 set: GlobSet::empty(),
138 root: PathBuf::from(""),
139 globs: vec![],
140 num_ignores: 0,
141 num_whitelists: 0,
142 matches: None,
143 }
144 }
145
146 /// Returns the directory containing this gitignore matcher.
147 ///
148 /// All matches are done relative to this path.
149 pub fn path(&self) -> &Path {
150 &*self.root
151 }
152
153 /// Returns true if and only if this gitignore has zero globs, and
154 /// therefore never matches any file path.
155 pub fn is_empty(&self) -> bool {
156 self.set.is_empty()
157 }
158
159 /// Returns the total number of globs, which should be equivalent to
160 /// `num_ignores + num_whitelists`.
161 pub fn len(&self) -> usize {
162 self.set.len()
163 }
164
165 /// Returns the total number of ignore globs.
166 pub fn num_ignores(&self) -> u64 {
167 self.num_ignores
168 }
169
170 /// Returns the total number of whitelisted globs.
171 pub fn num_whitelists(&self) -> u64 {
172 self.num_whitelists
173 }
174
175 /// Returns whether the given path (file or directory) matched a pattern in
176 /// this gitignore matcher.
177 ///
178 /// `is_dir` should be true if the path refers to a directory and false
179 /// otherwise.
180 ///
181 /// The given path is matched relative to the path given when building
182 /// the matcher. Specifically, before matching `path`, its prefix (as
183 /// determined by a common suffix of the directory containing this
184 /// gitignore) is stripped. If there is no common suffix/prefix overlap,
185 /// then `path` is assumed to be relative to this matcher.
186 pub fn matched<P: AsRef<Path>>(
187 &self,
188 path: P,
189 is_dir: bool,
190 ) -> Match<&Glob> {
191 if self.is_empty() {
192 return Match::None;
193 }
194 self.matched_stripped(self.strip(path.as_ref()), is_dir)
195 }
196
197 /// Returns whether the given path (file or directory, and expected to be
198 /// under the root) or any of its parent directories (up to the root)
199 /// matched a pattern in this gitignore matcher.
200 ///
201 /// NOTE: This method is more expensive than walking the directory hierarchy
202 /// top-to-bottom and matching the entries. But, is easier to use in cases
203 /// when a list of paths are available without a hierarchy.
204 ///
205 /// `is_dir` should be true if the path refers to a directory and false
206 /// otherwise.
207 ///
208 /// The given path is matched relative to the path given when building
209 /// the matcher. Specifically, before matching `path`, its prefix (as
210 /// determined by a common suffix of the directory containing this
211 /// gitignore) is stripped. If there is no common suffix/prefix overlap,
212 /// then `path` is assumed to be relative to this matcher.
213 ///
214 /// # Panics
215 ///
216 /// This method panics if the given file path is not under the root path
217 /// of this matcher.
218 pub fn matched_path_or_any_parents<P: AsRef<Path>>(
219 &self,
220 path: P,
221 is_dir: bool,
222 ) -> Match<&Glob> {
223 if self.is_empty() {
224 return Match::None;
225 }
226 let mut path = self.strip(path.as_ref());
227 assert!(!path.has_root(), "path is expected to be under the root");
228
229 match self.matched_stripped(path, is_dir) {
230 Match::None => (), // walk up
231 a_match => return a_match,
232 }
233 while let Some(parent) = path.parent() {
234 match self.matched_stripped(parent, /* is_dir */ true) {
235 Match::None => path = parent, // walk up
236 a_match => return a_match,
237 }
238 }
239 Match::None
240 }
241
242 /// Like matched, but takes a path that has already been stripped.
243 fn matched_stripped<P: AsRef<Path>>(
244 &self,
245 path: P,
246 is_dir: bool,
247 ) -> Match<&Glob> {
248 if self.is_empty() {
249 return Match::None;
250 }
251 let path = path.as_ref();
252 let _matches = self.matches.as_ref().unwrap().get_or_default();
253 let mut matches = _matches.borrow_mut();
254 let candidate = Candidate::new(path);
255 self.set.matches_candidate_into(&candidate, &mut *matches);
256 for &i in matches.iter().rev() {
257 let glob = &self.globs[i];
258 if !glob.is_only_dir() || is_dir {
259 return if glob.is_whitelist() {
260 Match::Whitelist(glob)
261 } else {
262 Match::Ignore(glob)
263 };
264 }
265 }
266 Match::None
267 }
268
269 /// Strips the given path such that it's suitable for matching with this
270 /// gitignore matcher.
271 fn strip<'a, P: 'a + AsRef<Path> + ?Sized>(
272 &'a self,
273 path: &'a P,
274 ) -> &'a Path {
275 let mut path = path.as_ref();
276 // A leading ./ is completely superfluous. We also strip it from
277 // our gitignore root path, so we need to strip it from our candidate
278 // path too.
279 if let Some(p) = strip_prefix("./", path) {
280 path = p;
281 }
282 // Strip any common prefix between the candidate path and the root
283 // of the gitignore, to make sure we get relative matching right.
284 // BUT, a file name might not have any directory components to it,
285 // in which case, we don't want to accidentally strip any part of the
286 // file name.
287 //
288 // As an additional special case, if the root is just `.`, then we
289 // shouldn't try to strip anything, e.g., when path begins with a `.`.
290 if self.root != Path::new(".") && !is_file_name(path) {
291 if let Some(p) = strip_prefix(&self.root, path) {
292 path = p;
293 // If we're left with a leading slash, get rid of it.
294 if let Some(p) = strip_prefix("/", path) {
295 path = p;
296 }
297 }
298 }
299 path
300 }
301}
302
303/// Builds a matcher for a single set of globs from a .gitignore file.
304#[derive(Clone, Debug)]
305pub struct GitignoreBuilder {
306 builder: GlobSetBuilder,
307 root: PathBuf,
308 globs: Vec<Glob>,
309 case_insensitive: bool,
310}
311
312impl GitignoreBuilder {
313 /// Create a new builder for a gitignore file.
314 ///
315 /// The path given should be the path at which the globs for this gitignore
316 /// file should be matched. Note that paths are always matched relative
317 /// to the root path given here. Generally, the root path should correspond
318 /// to the *directory* containing a `.gitignore` file.
319 pub fn new<P: AsRef<Path>>(root: P) -> GitignoreBuilder {
320 let root = root.as_ref();
321 GitignoreBuilder {
322 builder: GlobSetBuilder::new(),
323 root: strip_prefix("./", root).unwrap_or(root).to_path_buf(),
324 globs: vec![],
325 case_insensitive: false,
326 }
327 }
328
329 /// Builds a new matcher from the globs added so far.
330 ///
331 /// Once a matcher is built, no new globs can be added to it.
332 pub fn build(&self) -> Result<Gitignore, Error> {
333 let nignore = self.globs.iter().filter(|g| !g.is_whitelist()).count();
334 let nwhite = self.globs.iter().filter(|g| g.is_whitelist()).count();
335 let set = self
336 .builder
337 .build()
338 .map_err(|err| Error::Glob { glob: None, err: err.to_string() })?;
339 Ok(Gitignore {
340 set: set,
341 root: self.root.clone(),
342 globs: self.globs.clone(),
343 num_ignores: nignore as u64,
344 num_whitelists: nwhite as u64,
345 matches: Some(Arc::new(ThreadLocal::default())),
346 })
347 }
348
349 /// Build a global gitignore matcher using the configuration in this
350 /// builder.
351 ///
352 /// This consumes ownership of the builder unlike `build` because it
353 /// must mutate the builder to add the global gitignore globs.
354 ///
355 /// Note that this ignores the path given to this builder's constructor
356 /// and instead derives the path automatically from git's global
357 /// configuration.
358 pub fn build_global(mut self) -> (Gitignore, Option<Error>) {
359 match gitconfig_excludes_path() {
360 None => (Gitignore::empty(), None),
361 Some(path) => {
362 if !path.is_file() {
363 (Gitignore::empty(), None)
364 } else {
365 let mut errs = PartialErrorBuilder::default();
366 errs.maybe_push_ignore_io(self.add(path));
367 match self.build() {
368 Ok(gi) => (gi, errs.into_error_option()),
369 Err(err) => {
370 errs.push(err);
371 (Gitignore::empty(), errs.into_error_option())
372 }
373 }
374 }
375 }
376 }
377 }
378
379 /// Add each glob from the file path given.
380 ///
381 /// The file given should be formatted as a `gitignore` file.
382 ///
383 /// Note that partial errors can be returned. For example, if there was
384 /// a problem adding one glob, an error for that will be returned, but
385 /// all other valid globs will still be added.
386 pub fn add<P: AsRef<Path>>(&mut self, path: P) -> Option<Error> {
387 let path = path.as_ref();
388 let file = match File::open(path) {
389 Err(err) => return Some(Error::Io(err).with_path(path)),
390 Ok(file) => file,
391 };
392 let rdr = io::BufReader::new(file);
393 let mut errs = PartialErrorBuilder::default();
394 for (i, line) in rdr.lines().enumerate() {
395 let lineno = (i + 1) as u64;
396 let line = match line {
397 Ok(line) => line,
398 Err(err) => {
399 errs.push(Error::Io(err).tagged(path, lineno));
400 break;
401 }
402 };
403 if let Err(err) = self.add_line(Some(path.to_path_buf()), &line) {
404 errs.push(err.tagged(path, lineno));
405 }
406 }
407 errs.into_error_option()
408 }
409
410 /// Add each glob line from the string given.
411 ///
412 /// If this string came from a particular `gitignore` file, then its path
413 /// should be provided here.
414 ///
415 /// The string given should be formatted as a `gitignore` file.
416 #[cfg(test)]
417 fn add_str(
418 &mut self,
419 from: Option<PathBuf>,
420 gitignore: &str,
421 ) -> Result<&mut GitignoreBuilder, Error> {
422 for line in gitignore.lines() {
423 self.add_line(from.clone(), line)?;
424 }
425 Ok(self)
426 }
427
428 /// Add a line from a gitignore file to this builder.
429 ///
430 /// If this line came from a particular `gitignore` file, then its path
431 /// should be provided here.
432 ///
433 /// If the line could not be parsed as a glob, then an error is returned.
434 pub fn add_line(
435 &mut self,
436 from: Option<PathBuf>,
437 mut line: &str,
438 ) -> Result<&mut GitignoreBuilder, Error> {
439 #![allow(deprecated)]
440
441 if line.starts_with("#") {
442 return Ok(self);
443 }
444 if !line.ends_with("\\ ") {
445 line = line.trim_right();
446 }
447 if line.is_empty() {
448 return Ok(self);
449 }
450 let mut glob = Glob {
451 from: from,
452 original: line.to_string(),
453 actual: String::new(),
454 is_whitelist: false,
455 is_only_dir: false,
456 };
457 let mut is_absolute = false;
458 if line.starts_with("\\!") || line.starts_with("\\#") {
459 line = &line[1..];
460 is_absolute = line.chars().nth(0) == Some('/');
461 } else {
462 if line.starts_with("!") {
463 glob.is_whitelist = true;
464 line = &line[1..];
465 }
466 if line.starts_with("/") {
467 // `man gitignore` says that if a glob starts with a slash,
468 // then the glob can only match the beginning of a path
469 // (relative to the location of gitignore). We achieve this by
470 // simply banning wildcards from matching /.
471 line = &line[1..];
472 is_absolute = true;
473 }
474 }
475 // If it ends with a slash, then this should only match directories,
476 // but the slash should otherwise not be used while globbing.
477 if line.as_bytes().last() == Some(&b'/') {
478 glob.is_only_dir = true;
479 line = &line[..line.len() - 1];
480 // If the slash was escaped, then remove the escape.
481 // See: https://github.com/BurntSushi/ripgrep/issues/2236
482 if line.as_bytes().last() == Some(&b'\\') {
483 line = &line[..line.len() - 1];
484 }
485 }
486 glob.actual = line.to_string();
487 // If there is a literal slash, then this is a glob that must match the
488 // entire path name. Otherwise, we should let it match anywhere, so use
489 // a **/ prefix.
490 if !is_absolute && !line.chars().any(|c| c == '/') {
491 // ... but only if we don't already have a **/ prefix.
492 if !glob.has_doublestar_prefix() {
493 glob.actual = format!("**/{}", glob.actual);
494 }
495 }
496 // If the glob ends with `/**`, then we should only match everything
497 // inside a directory, but not the directory itself. Standard globs
498 // will match the directory. So we add `/*` to force the issue.
499 if glob.actual.ends_with("/**") {
500 glob.actual = format!("{}/*", glob.actual);
501 }
502 let parsed = GlobBuilder::new(&glob.actual)
503 .literal_separator(true)
504 .case_insensitive(self.case_insensitive)
505 .backslash_escape(true)
506 .build()
507 .map_err(|err| Error::Glob {
508 glob: Some(glob.original.clone()),
509 err: err.kind().to_string(),
510 })?;
511 self.builder.add(parsed);
512 self.globs.push(glob);
513 Ok(self)
514 }
515
516 /// Toggle whether the globs should be matched case insensitively or not.
517 ///
518 /// When this option is changed, only globs added after the change will be
519 /// affected.
520 ///
521 /// This is disabled by default.
522 pub fn case_insensitive(
523 &mut self,
524 yes: bool,
525 ) -> Result<&mut GitignoreBuilder, Error> {
526 // TODO: This should not return a `Result`. Fix this in the next semver
527 // release.
528 self.case_insensitive = yes;
529 Ok(self)
530 }
531}
532
533/// Return the file path of the current environment's global gitignore file.
534///
535/// Note that the file path returned may not exist.
536fn gitconfig_excludes_path() -> Option<PathBuf> {
537 // git supports $HOME/.gitconfig and $XDG_CONFIG_HOME/git/config. Notably,
538 // both can be active at the same time, where $HOME/.gitconfig takes
539 // precedent. So if $HOME/.gitconfig defines a `core.excludesFile`, then
540 // we're done.
541 match gitconfig_home_contents().and_then(|x: Vec| parse_excludes_file(&x)) {
542 Some(path: PathBuf) => return Some(path),
543 None => {}
544 }
545 match gitconfig_xdg_contents().and_then(|x: Vec| parse_excludes_file(&x)) {
546 Some(path: PathBuf) => return Some(path),
547 None => {}
548 }
549 excludes_file_default()
550}
551
552/// Returns the file contents of git's global config file, if one exists, in
553/// the user's home directory.
554fn gitconfig_home_contents() -> Option<Vec<u8>> {
555 let home: PathBuf = match home_dir() {
556 None => return None,
557 Some(home: PathBuf) => home,
558 };
559 let mut file: BufReader = match File::open(path:home.join(path:".gitconfig")) {
560 Err(_) => return None,
561 Ok(file: File) => io::BufReader::new(inner:file),
562 };
563 let mut contents: Vec = vec![];
564 file.read_to_end(&mut contents).ok().map(|_| contents)
565}
566
567/// Returns the file contents of git's global config file, if one exists, in
568/// the user's XDG_CONFIG_HOME directory.
569fn gitconfig_xdg_contents() -> Option<Vec<u8>> {
570 let path: Option = envOption::var_os(key:"XDG_CONFIG_HOME")
571 .and_then(|x: OsString| if x.is_empty() { None } else { Some(PathBuf::from(x)) })
572 .or_else(|| home_dir().map(|p: PathBuf| p.join(path:".config")))
573 .map(|x: PathBuf| x.join(path:"git/config"));
574 let mut file: BufReader = match path.and_then(|p: PathBuf| File::open(path:p).ok()) {
575 None => return None,
576 Some(file: File) => io::BufReader::new(inner:file),
577 };
578 let mut contents: Vec = vec![];
579 file.read_to_end(&mut contents).ok().map(|_| contents)
580}
581
582/// Returns the default file path for a global .gitignore file.
583///
584/// Specifically, this respects XDG_CONFIG_HOME.
585fn excludes_file_default() -> Option<PathBuf> {
586 envOption::var_os(key:"XDG_CONFIG_HOME")
587 .and_then(|x: OsString| if x.is_empty() { None } else { Some(PathBuf::from(x)) })
588 .or_else(|| home_dir().map(|p: PathBuf| p.join(path:".config")))
589 .map(|x: PathBuf| x.join(path:"git/ignore"))
590}
591
592/// Extract git's `core.excludesfile` config setting from the raw file contents
593/// given.
594fn parse_excludes_file(data: &[u8]) -> Option<PathBuf> {
595 // N.B. This is the lazy approach, and isn't technically correct, but
596 // probably works in more circumstances. I guess we would ideally have
597 // a full INI parser. Yuck.
598 lazy_static::lazy_static! {
599 static ref RE: Regex =
600 Regex::new(r"(?im)^\s*excludesfile\s*=\s*(.+)\s*$").unwrap();
601 };
602 let caps: Captures<'_> = match RE.captures(text:data) {
603 None => return None,
604 Some(caps: Captures<'_>) => caps,
605 };
606 str::from_utf8(&caps[1]).ok().map(|s: &str| PathBuf::from(expand_tilde(path:s)))
607}
608
609/// Expands ~ in file paths to the value of $HOME.
610fn expand_tilde(path: &str) -> String {
611 let home: String = match home_dir() {
612 None => return path.to_string(),
613 Some(home: PathBuf) => home.to_string_lossy().into_owned(),
614 };
615 path.replace(from:"~", &home)
616}
617
618/// Returns the location of the user's home directory.
619fn home_dir() -> Option<PathBuf> {
620 // We're fine with using env::home_dir for now. Its bugs are, IMO, pretty
621 // minor corner cases. We should still probably eventually migrate to
622 // the `dirs` crate to get a proper implementation.
623 #![allow(deprecated)]
624 env::home_dir()
625}
626
627#[cfg(test)]
628mod tests {
629 use super::{Gitignore, GitignoreBuilder};
630 use std::path::Path;
631
632 fn gi_from_str<P: AsRef<Path>>(root: P, s: &str) -> Gitignore {
633 let mut builder = GitignoreBuilder::new(root);
634 builder.add_str(None, s).unwrap();
635 builder.build().unwrap()
636 }
637
638 macro_rules! ignored {
639 ($name:ident, $root:expr, $gi:expr, $path:expr) => {
640 ignored!($name, $root, $gi, $path, false);
641 };
642 ($name:ident, $root:expr, $gi:expr, $path:expr, $is_dir:expr) => {
643 #[test]
644 fn $name() {
645 let gi = gi_from_str($root, $gi);
646 assert!(gi.matched($path, $is_dir).is_ignore());
647 }
648 };
649 }
650
651 macro_rules! not_ignored {
652 ($name:ident, $root:expr, $gi:expr, $path:expr) => {
653 not_ignored!($name, $root, $gi, $path, false);
654 };
655 ($name:ident, $root:expr, $gi:expr, $path:expr, $is_dir:expr) => {
656 #[test]
657 fn $name() {
658 let gi = gi_from_str($root, $gi);
659 assert!(!gi.matched($path, $is_dir).is_ignore());
660 }
661 };
662 }
663
664 const ROOT: &'static str = "/home/foobar/rust/rg";
665
666 ignored!(ig1, ROOT, "months", "months");
667 ignored!(ig2, ROOT, "*.lock", "Cargo.lock");
668 ignored!(ig3, ROOT, "*.rs", "src/main.rs");
669 ignored!(ig4, ROOT, "src/*.rs", "src/main.rs");
670 ignored!(ig5, ROOT, "/*.c", "cat-file.c");
671 ignored!(ig6, ROOT, "/src/*.rs", "src/main.rs");
672 ignored!(ig7, ROOT, "!src/main.rs\n*.rs", "src/main.rs");
673 ignored!(ig8, ROOT, "foo/", "foo", true);
674 ignored!(ig9, ROOT, "**/foo", "foo");
675 ignored!(ig10, ROOT, "**/foo", "src/foo");
676 ignored!(ig11, ROOT, "**/foo/**", "src/foo/bar");
677 ignored!(ig12, ROOT, "**/foo/**", "wat/src/foo/bar/baz");
678 ignored!(ig13, ROOT, "**/foo/bar", "foo/bar");
679 ignored!(ig14, ROOT, "**/foo/bar", "src/foo/bar");
680 ignored!(ig15, ROOT, "abc/**", "abc/x");
681 ignored!(ig16, ROOT, "abc/**", "abc/x/y");
682 ignored!(ig17, ROOT, "abc/**", "abc/x/y/z");
683 ignored!(ig18, ROOT, "a/**/b", "a/b");
684 ignored!(ig19, ROOT, "a/**/b", "a/x/b");
685 ignored!(ig20, ROOT, "a/**/b", "a/x/y/b");
686 ignored!(ig21, ROOT, r"\!xy", "!xy");
687 ignored!(ig22, ROOT, r"\#foo", "#foo");
688 ignored!(ig23, ROOT, "foo", "./foo");
689 ignored!(ig24, ROOT, "target", "grep/target");
690 ignored!(ig25, ROOT, "Cargo.lock", "./tabwriter-bin/Cargo.lock");
691 ignored!(ig26, ROOT, "/foo/bar/baz", "./foo/bar/baz");
692 ignored!(ig27, ROOT, "foo/", "xyz/foo", true);
693 ignored!(ig28, "./src", "/llvm/", "./src/llvm", true);
694 ignored!(ig29, ROOT, "node_modules/ ", "node_modules", true);
695 ignored!(ig30, ROOT, "**/", "foo/bar", true);
696 ignored!(ig31, ROOT, "path1/*", "path1/foo");
697 ignored!(ig32, ROOT, ".a/b", ".a/b");
698 ignored!(ig33, "./", ".a/b", ".a/b");
699 ignored!(ig34, ".", ".a/b", ".a/b");
700 ignored!(ig35, "./.", ".a/b", ".a/b");
701 ignored!(ig36, "././", ".a/b", ".a/b");
702 ignored!(ig37, "././.", ".a/b", ".a/b");
703 ignored!(ig38, ROOT, "\\[", "[");
704 ignored!(ig39, ROOT, "\\?", "?");
705 ignored!(ig40, ROOT, "\\*", "*");
706 ignored!(ig41, ROOT, "\\a", "a");
707 ignored!(ig42, ROOT, "s*.rs", "sfoo.rs");
708 ignored!(ig43, ROOT, "**", "foo.rs");
709 ignored!(ig44, ROOT, "**/**/*", "a/foo.rs");
710
711 not_ignored!(ignot1, ROOT, "amonths", "months");
712 not_ignored!(ignot2, ROOT, "monthsa", "months");
713 not_ignored!(ignot3, ROOT, "/src/*.rs", "src/grep/src/main.rs");
714 not_ignored!(ignot4, ROOT, "/*.c", "mozilla-sha1/sha1.c");
715 not_ignored!(ignot5, ROOT, "/src/*.rs", "src/grep/src/main.rs");
716 not_ignored!(ignot6, ROOT, "*.rs\n!src/main.rs", "src/main.rs");
717 not_ignored!(ignot7, ROOT, "foo/", "foo", false);
718 not_ignored!(ignot8, ROOT, "**/foo/**", "wat/src/afoo/bar/baz");
719 not_ignored!(ignot9, ROOT, "**/foo/**", "wat/src/fooa/bar/baz");
720 not_ignored!(ignot10, ROOT, "**/foo/bar", "foo/src/bar");
721 not_ignored!(ignot11, ROOT, "#foo", "#foo");
722 not_ignored!(ignot12, ROOT, "\n\n\n", "foo");
723 not_ignored!(ignot13, ROOT, "foo/**", "foo", true);
724 not_ignored!(
725 ignot14,
726 "./third_party/protobuf",
727 "m4/ltoptions.m4",
728 "./third_party/protobuf/csharp/src/packages/repositories.config"
729 );
730 not_ignored!(ignot15, ROOT, "!/bar", "foo/bar");
731 not_ignored!(ignot16, ROOT, "*\n!**/", "foo", true);
732 not_ignored!(ignot17, ROOT, "src/*.rs", "src/grep/src/main.rs");
733 not_ignored!(ignot18, ROOT, "path1/*", "path2/path1/foo");
734 not_ignored!(ignot19, ROOT, "s*.rs", "src/foo.rs");
735
736 fn bytes(s: &str) -> Vec<u8> {
737 s.to_string().into_bytes()
738 }
739
740 fn path_string<P: AsRef<Path>>(path: P) -> String {
741 path.as_ref().to_str().unwrap().to_string()
742 }
743
744 #[test]
745 fn parse_excludes_file1() {
746 let data = bytes("[core]\nexcludesFile = /foo/bar");
747 let got = super::parse_excludes_file(&data).unwrap();
748 assert_eq!(path_string(got), "/foo/bar");
749 }
750
751 #[test]
752 fn parse_excludes_file2() {
753 let data = bytes("[core]\nexcludesFile = ~/foo/bar");
754 let got = super::parse_excludes_file(&data).unwrap();
755 assert_eq!(path_string(got), super::expand_tilde("~/foo/bar"));
756 }
757
758 #[test]
759 fn parse_excludes_file3() {
760 let data = bytes("[core]\nexcludeFile = /foo/bar");
761 assert!(super::parse_excludes_file(&data).is_none());
762 }
763
764 // See: https://github.com/BurntSushi/ripgrep/issues/106
765 #[test]
766 fn regression_106() {
767 gi_from_str("/", " ");
768 }
769
770 #[test]
771 fn case_insensitive() {
772 let gi = GitignoreBuilder::new(ROOT)
773 .case_insensitive(true)
774 .unwrap()
775 .add_str(None, "*.html")
776 .unwrap()
777 .build()
778 .unwrap();
779 assert!(gi.matched("foo.html", false).is_ignore());
780 assert!(gi.matched("foo.HTML", false).is_ignore());
781 assert!(!gi.matched("foo.htm", false).is_ignore());
782 assert!(!gi.matched("foo.HTM", false).is_ignore());
783 }
784
785 ignored!(cs1, ROOT, "*.html", "foo.html");
786 not_ignored!(cs2, ROOT, "*.html", "foo.HTML");
787 not_ignored!(cs3, ROOT, "*.html", "foo.htm");
788 not_ignored!(cs4, ROOT, "*.html", "foo.HTM");
789}
790