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}