xvc_walker/ignore_rules.rs
1//! Ignore patterns for a directory and its child directories.
2use crate::{Result, Source};
3use std::path::{Path, PathBuf};
4use std::sync::{Arc, RwLock};
5
6use crate::pattern::{MatchResult, Pattern};
7use rayon::prelude::*;
8
9use fast_glob::glob_match;
10use xvc_logging::watch;
11
12/// Complete set of ignore rules for a directory and its child directories.
13#[derive(Debug, Clone)]
14pub struct IgnoreRules {
15 /// The root of the ignore rules.
16 /// Typically this is the root directory of Git or Xvc repository.
17 pub root: PathBuf,
18
19 /// The name of the ignore file (e.g. `.xvcignore`, `.gitignore`) to be loaded for ignore rules.
20 pub ignore_filename: Option<String>,
21
22 /// All ignore patterns collected from ignore files or specified in code.
23 pub ignore_patterns: Arc<RwLock<Vec<Pattern>>>,
24
25 /// All whitelist patterns collected from ignore files or specified in code
26 pub whitelist_patterns: Arc<RwLock<Vec<Pattern>>>,
27}
28
29/// IgnoreRules shared across threads.
30pub type SharedIgnoreRules = Arc<RwLock<IgnoreRules>>;
31
32impl IgnoreRules {
33 /// An empty set of ignore rules that neither ignores nor whitelists any path.
34 pub fn empty(dir: &Path, ignore_filename: Option<&str>) -> Self {
35 IgnoreRules {
36 root: PathBuf::from(dir),
37 ignore_filename: ignore_filename.map(|s| s.to_string()),
38 ignore_patterns: Arc::new(RwLock::new(Vec::<Pattern>::new())),
39 whitelist_patterns: Arc::new(RwLock::new(Vec::<Pattern>::new())),
40 }
41 }
42
43 /// Constructs a new `IgnoreRules` instance from a given set of global ignore patterns.
44 pub fn from_global_patterns(
45 ignore_root: &Path,
46 ignore_filename: Option<&str>,
47 given: &str,
48 ) -> Self {
49 let mut given_patterns = Vec::<Pattern>::new();
50 // Add given patterns to ignore_patterns
51 for line in given.lines() {
52 let pattern = Pattern::new(Source::Global, line);
53 given_patterns.push(pattern);
54 }
55 IgnoreRules::from_patterns(ignore_root, ignore_filename, given_patterns)
56 }
57
58 /// Constructs a new `IgnoreRules` instance from a vector of patterns and a root path.
59 ///
60 /// This function separates the patterns into ignore patterns and whitelist patterns
61 /// based on their `PatternEffect`. It then stores these patterns and the root path
62 /// in a new `IgnoreRules` instance.
63 ///
64 /// # Arguments
65 ///
66 /// * `patterns` - A vector of `Pattern` instances to be used for creating the `IgnoreRules`.
67 /// * `ignore_root` - A reference to the root path for the ignore rules.
68 ///
69 /// # Returns
70 ///
71 /// A new `IgnoreRules` instance containing the given patterns and root path.
72 pub fn from_patterns(
73 ignore_root: &Path,
74 ignore_filename: Option<&str>,
75 mut patterns: Vec<Pattern>,
76 ) -> Self {
77 let mut ignore_patterns = Vec::new();
78 let mut whitelist_patterns = Vec::new();
79 patterns
80 .drain(0..patterns.len())
81 .for_each(|pattern| match pattern.effect {
82 crate::PatternEffect::Ignore => ignore_patterns.push(pattern),
83 crate::PatternEffect::Whitelist => whitelist_patterns.push(pattern),
84 });
85 IgnoreRules {
86 root: PathBuf::from(ignore_root),
87 ignore_filename: ignore_filename.map(|s| s.to_string()),
88 ignore_patterns: Arc::new(RwLock::new(ignore_patterns)),
89 whitelist_patterns: Arc::new(RwLock::new(whitelist_patterns)),
90 }
91 }
92
93 /// Checks if a given path matches any of the whitelist or ignore patterns.
94 ///
95 /// The function first checks if the path matches any of the whitelist patterns.
96 /// If a match is found, it returns `MatchResult::Whitelist`.
97 ///
98 /// If the path does not match any of the whitelist patterns, the function then checks
99 /// if the path matches any of the ignore patterns. If a match is found, it returns
100 /// `MatchResult::Ignore`.
101 ///
102 /// If the path does not match any of the whitelist or ignore patterns, the function
103 /// returns `MatchResult::NoMatch`.
104 ///
105 /// # Arguments
106 ///
107 /// * `path` - A reference to the path to check.
108 ///
109 /// # Returns
110 ///
111 /// * `MatchResult::Whitelist` if the path matches a whitelist pattern.
112 /// * `MatchResult::Ignore` if the path matches an ignore pattern.
113 /// * `MatchResult::NoMatch` if the path does not match any pattern.
114 pub fn check(&self, path: &Path) -> MatchResult {
115 let is_abs = path.is_absolute();
116 // strip_prefix eats the final slash, and ends_with behave differently than str, so we work
117 // around here
118 let path_str = path.to_string_lossy();
119 let final_slash = path_str.ends_with('/');
120
121 let path = if is_abs {
122 if final_slash {
123 format!(
124 "/{}/",
125 path.strip_prefix(&self.root)
126 .expect("path must be within root")
127 .to_string_lossy()
128 )
129 } else {
130 format!(
131 "/{}",
132 path.strip_prefix(&self.root)
133 .expect("path must be within root")
134 .to_string_lossy()
135 )
136 }
137 } else {
138 path_str.to_string()
139 };
140
141 {
142 let whitelist_patterns = self.whitelist_patterns.read().unwrap();
143 if let Some(p) = whitelist_patterns
144 .par_iter()
145 .find_any(|pattern| glob_match(&pattern.glob, &path))
146 {
147 watch!(p);
148 return MatchResult::Whitelist;
149 }
150 }
151
152 {
153 let ignore_patterns = self.ignore_patterns.read().unwrap();
154 if let Some(p) = ignore_patterns
155 .par_iter()
156 .find_any(|pattern| glob_match(&pattern.glob, &path))
157 {
158 watch!(p);
159 return MatchResult::Ignore;
160 }
161 }
162
163 MatchResult::NoMatch
164 }
165
166 /// Merges the ignore and whitelist patterns of another `IgnoreRules` instance into this one.
167 ///
168 /// This function locks the ignore and whitelist patterns of both `IgnoreRules` instances,
169 /// drains the patterns from the other instance, and pushes them into this instance.
170 /// The other instance is left empty after this operation.
171 ///
172 /// # Arguments
173 ///
174 /// * `other` - A reference to the other `IgnoreRules` instance to merge with.
175 ///
176 /// # Returns
177 ///
178 /// * `Ok(())` if the merge operation was successful.
179 /// * `Err` if the merge operation failed.
180 ///
181 /// # Panics
182 ///
183 /// This function will panic if the roots of the two `IgnoreRules` instances are not equal.
184 pub fn merge_with(&self, other: &IgnoreRules) -> Result<()> {
185 assert_eq!(self.root, other.root);
186
187 {
188 let mut ignore_patterns = self.ignore_patterns.write().unwrap();
189 let mut other_ignore_patterns = other.ignore_patterns.write().unwrap();
190 let len = other_ignore_patterns.len();
191 other_ignore_patterns
192 .drain(0..len)
193 .for_each(|p| ignore_patterns.push(p));
194 }
195
196 {
197 let mut whitelist_patterns = self.whitelist_patterns.write().unwrap();
198 let mut other_whitelist_patterns = other.whitelist_patterns.write().unwrap();
199 let len = other_whitelist_patterns.len();
200 other_whitelist_patterns
201 .drain(0..len)
202 .for_each(|p| whitelist_patterns.push(p));
203 }
204
205 Ok(())
206 }
207 /// Adds a list of patterns to the current ignore rules.
208 ///
209 /// # Arguments
210 ///
211 /// * `patterns` - A vector of patterns to be added to the ignore rules.
212 pub fn add_patterns(&self, patterns: Vec<Pattern>) -> Result<()> {
213 let other = IgnoreRules::from_patterns(&self.root, None, patterns);
214 self.merge_with(&other)
215 }
216}