ignore.rs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. //! Ignoring globs in `.gitignore` files.
  2. //!
  3. //! This uses a cache because the file with the globs in might not be the same
  4. //! directory that we’re listing!
  5. use std::fs::File;
  6. use std::io::Read;
  7. use std::path::{Path, PathBuf};
  8. use std::sync::RwLock;
  9. use fs::filter::IgnorePatterns;
  10. /// An **ignore cache** holds sets of glob patterns paired with the
  11. /// directories that they should be ignored underneath. Believe it or not,
  12. /// that’s a valid English sentence.
  13. #[derive(Default, Debug)]
  14. pub struct IgnoreCache {
  15. entries: RwLock<Vec<(PathBuf, IgnorePatterns)>>
  16. }
  17. impl IgnoreCache {
  18. pub fn new() -> IgnoreCache {
  19. IgnoreCache::default()
  20. }
  21. pub fn discover_underneath(&self, path: &Path) {
  22. let mut path = Some(path);
  23. let mut entries = self.entries.write().unwrap();
  24. while let Some(p) = path {
  25. if p.components().next().is_none() { break }
  26. let ignore_file = p.join(".gitignore");
  27. if ignore_file.is_file() {
  28. debug!("Found a .gitignore file: {:?}", ignore_file);
  29. if let Ok(mut file) = File::open(ignore_file) {
  30. let mut contents = String::new();
  31. match file.read_to_string(&mut contents) {
  32. Ok(_) => {
  33. let patterns = file_lines_to_patterns(contents.lines());
  34. entries.push((p.into(), patterns));
  35. }
  36. Err(e) => debug!("Failed to read a .gitignore: {:?}", e)
  37. }
  38. }
  39. }
  40. else {
  41. debug!("Found no .gitignore file at {:?}", ignore_file);
  42. }
  43. path = p.parent();
  44. }
  45. }
  46. pub fn is_ignored(&self, suspect: &Path) -> bool {
  47. let entries = self.entries.read().unwrap();
  48. entries.iter().any(|&(ref base_path, ref patterns)| {
  49. if let Ok(suffix) = suspect.strip_prefix(&base_path) {
  50. patterns.is_ignored_path(suffix)
  51. }
  52. else {
  53. false
  54. }
  55. })
  56. }
  57. }
  58. fn file_lines_to_patterns<'a, I>(iter: I) -> IgnorePatterns
  59. where I: Iterator<Item=&'a str>
  60. {
  61. let iter = iter.filter(|el| !el.is_empty());
  62. let iter = iter.filter(|el| !el.starts_with("#"));
  63. // TODO: Figure out if this should trim whitespace or not
  64. // Errors are currently being ignored... not a good look
  65. IgnorePatterns::parse_from_iter(iter).0
  66. }
  67. #[cfg(test)]
  68. mod test {
  69. use super::*;
  70. #[test]
  71. fn parse_nothing() {
  72. use std::iter::empty;
  73. let (patterns, _) = IgnorePatterns::parse_from_iter(empty());
  74. assert_eq!(patterns, file_lines_to_patterns(empty()));
  75. }
  76. #[test]
  77. fn parse_some_globs() {
  78. let stuff = vec![ "*.mp3", "README.md" ];
  79. let reals = vec![ "*.mp3", "README.md" ];
  80. let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
  81. assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
  82. }
  83. #[test]
  84. fn parse_some_comments() {
  85. let stuff = vec![ "*.mp3", "# I am a comment!", "#", "README.md" ];
  86. let reals = vec![ "*.mp3", "README.md" ];
  87. let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
  88. assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
  89. }
  90. #[test]
  91. fn parse_some_blank_lines() {
  92. let stuff = vec![ "*.mp3", "", "", "README.md" ];
  93. let reals = vec![ "*.mp3", "README.md" ];
  94. let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
  95. assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
  96. }
  97. #[test]
  98. fn parse_some_whitespacey_lines() {
  99. let stuff = vec![ " *.mp3", " ", " a ", "README.md " ];
  100. let reals = vec![ " *.mp3", " ", " a ", "README.md " ];
  101. let (patterns, _) = IgnorePatterns::parse_from_iter(reals.into_iter());
  102. assert_eq!(patterns, file_lines_to_patterns(stuff.into_iter()));
  103. }
  104. fn test_cache(dir: &'static str, pats: Vec<&str>) -> IgnoreCache {
  105. IgnoreCache { entries: RwLock::new(vec![ (dir.into(), IgnorePatterns::parse_from_iter(pats.into_iter()).0) ]) }
  106. }
  107. #[test]
  108. fn an_empty_cache_ignores_nothing() {
  109. let ignores = IgnoreCache::default();
  110. assert_eq!(false, ignores.is_ignored(Path::new("/usr/bin/drinking")));
  111. assert_eq!(false, ignores.is_ignored(Path::new("target/debug/exa")));
  112. }
  113. #[test]
  114. fn a_nonempty_cache_ignores_some_things() {
  115. let ignores = test_cache("/vagrant", vec![ "target" ]);
  116. assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/src")));
  117. assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/target")));
  118. }
  119. #[test]
  120. fn ignore_some_globs() {
  121. let ignores = test_cache("/vagrant", vec![ "*.ipr", "*.iws", ".docker" ]);
  122. assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/exa.ipr")));
  123. assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/exa.iws")));
  124. assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/exa.iwiwal")));
  125. assert_eq!(true, ignores.is_ignored(Path::new("/vagrant/.docker")));
  126. assert_eq!(false, ignores.is_ignored(Path::new("/vagrant/exa.docker")));
  127. assert_eq!(false, ignores.is_ignored(Path::new("/srcode/exa.ipr")));
  128. assert_eq!(false, ignores.is_ignored(Path::new("/srcode/exa.iws")));
  129. }
  130. #[test] #[ignore]
  131. fn ignore_relatively() {
  132. let ignores = test_cache(".", vec![ "target" ]);
  133. assert_eq!(true, ignores.is_ignored(Path::new("./target")));
  134. assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
  135. assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
  136. assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
  137. assert_eq!(false, ignores.is_ignored(Path::new("./.target")));
  138. }
  139. #[test] #[ignore]
  140. fn ignore_relatively_sometimes() {
  141. let ignores = test_cache(".", vec![ "project/target" ]);
  142. assert_eq!(false, ignores.is_ignored(Path::new("./target")));
  143. assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
  144. assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
  145. assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
  146. }
  147. #[test] #[ignore]
  148. fn ignore_relatively_absolutely() {
  149. let ignores = test_cache(".", vec![ "/project/target" ]);
  150. assert_eq!(false, ignores.is_ignored(Path::new("./target")));
  151. assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
  152. assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
  153. assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
  154. }
  155. #[test] #[ignore] // not 100% sure if dot works this way...
  156. fn ignore_relatively_absolutely_dot() {
  157. let ignores = test_cache(".", vec![ "./project/target" ]);
  158. assert_eq!(false, ignores.is_ignored(Path::new("./target")));
  159. assert_eq!(true, ignores.is_ignored(Path::new("./project/target")));
  160. assert_eq!(true, ignores.is_ignored(Path::new("./project/project/target")));
  161. assert_eq!(true, ignores.is_ignored(Path::new("./project/project/project/target")));
  162. }
  163. }