1use glob::Pattern;
2use std::fs;
3use std::path::Path;
4
5pub struct IgnorePatterns {
6 patterns: Vec<Pattern>,
7}
8
9impl IgnorePatterns {
10 pub fn load(root: &Path) -> Self {
11 let mut patterns = Vec::new();
12
13 if let Some(home) = dirs::home_dir() {
14 let global_ignore = home.join(".omignore");
15 if let Ok(ps) = Self::parse_file(&global_ignore) {
16 patterns.extend(ps);
17 }
18 }
19
20 let local_ignore = root.join(".omignore");
21 if let Ok(ps) = Self::parse_file(&local_ignore) {
22 patterns.extend(ps);
23 }
24
25 IgnorePatterns { patterns }
26 }
27
28 fn parse_file(path: &Path) -> Result<Vec<Pattern>, Box<dyn std::error::Error>> {
29 let content = fs::read_to_string(path)?;
30 let mut patterns = Vec::new();
31
32 for line in content.lines() {
33 let line = line.trim();
34 if line.is_empty() || line.starts_with('#') {
35 continue;
36 }
37
38 let pattern = if line.starts_with("**/") {
39 line.to_string()
40 } else if line.ends_with('/') {
41 format!("{}**", line)
42 } else if line.contains('*') || line.contains('?') {
43 format!("**/{}", line)
44 } else {
45 line.to_string()
46 };
47
48 if let Ok(p) = Pattern::new(&pattern) {
49 patterns.push(p);
50 }
51 }
52
53 Ok(patterns)
54 }
55
56 pub fn is_ignored(&self, path: &str) -> bool {
57 self.patterns.iter().any(|p| p.matches(path))
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use proptest::prelude::*;
65 use std::io::Write;
66 use tempfile::NamedTempFile;
67
68 proptest! {
69 #[test]
70 fn test_is_ignored_never_panics(s in "\\PC*") {
71 let patterns = vec![Pattern::new("**/node_modules/**").unwrap()];
72 let ignore = IgnorePatterns { patterns };
73 ignore.is_ignored(&s);
74 }
75 }
76
77 #[test]
78 fn test_pattern_matching() {
79 let patterns = vec![
80 Pattern::new("**/*.lock").unwrap(),
81 Pattern::new("**/*-lock.*").unwrap(),
82 Pattern::new("**/node_modules/**").unwrap(),
83 Pattern::new("dist/**").unwrap(),
84 ];
85
86 let ignore = IgnorePatterns { patterns };
87
88 assert!(ignore.is_ignored("package-lock.json"));
89 assert!(ignore.is_ignored("Cargo.lock"));
90 assert!(ignore.is_ignored("src/node_modules/foo/bar.js"));
91 assert!(ignore.is_ignored("dist/bundle.js"));
92 assert!(!ignore.is_ignored("src/main.rs"));
93 }
94
95 #[test]
96 fn test_parse_file_logic() {
97 let mut tmp = NamedTempFile::new().unwrap();
98 writeln!(tmp, "# comment").unwrap();
99 writeln!(tmp, " ").unwrap();
100 writeln!(tmp, "target/").unwrap();
101 writeln!(tmp, "*.log").unwrap();
102 writeln!(tmp, "foo.txt").unwrap();
103 writeln!(tmp, "**/bar/*").unwrap();
104
105 let patterns = IgnorePatterns::parse_file(tmp.path()).unwrap();
106 let ignore = IgnorePatterns { patterns };
107
108 assert!(ignore.is_ignored("target/debug/exe"));
109 assert!(ignore.is_ignored("some/path/test.log"));
110 assert!(ignore.is_ignored("foo.txt"));
111 assert!(ignore.is_ignored("a/bar/b"));
112 assert!(!ignore.is_ignored("src/main.rs"));
113
114 assert_eq!(ignore.patterns.len(), 4);
115 }
116
117 #[test]
118 fn test_directory_pattern_expansion() {
119 let mut tmp = NamedTempFile::new().unwrap();
120 writeln!(tmp, "dir/").unwrap();
121
122 let patterns = IgnorePatterns::parse_file(tmp.path()).unwrap();
123 let ignore = IgnorePatterns { patterns };
124
125 assert!(ignore.is_ignored("dir/file.txt"));
126 assert!(ignore.is_ignored("dir/subdir/file.txt"));
127 }
128}