xvc_walker/
pattern.rs

1//! Pattern describes a single line in an ignore file and its semantics
2//! It is used to match a path with the given pattern
3use crate::sync;
4pub use error::{Error, Result};
5pub use ignore_rules::IgnoreRules;
6pub use std::hash::Hash;
7pub use sync::{PathSync, PathSyncSingleton};
8
9pub use crate::notify::{make_watcher, PathEvent, RecommendedWatcher};
10
11use std::{fmt::Debug, path::PathBuf};
12
13use crate::error;
14use crate::ignore_rules;
15
16/// Show whether a path matches to a glob rule
17#[derive(Debug, Clone)]
18pub enum MatchResult {
19    /// There is no match between glob(s) and path
20    NoMatch,
21    /// Path matches to ignored glob(s)
22    Ignore,
23    /// Path matches to whitelisted glob(s)
24    Whitelist,
25}
26
27/// Is the pattern matches anywhere or only relative to a directory?
28#[derive(Debug, Clone, Hash, PartialEq, Eq)]
29pub enum PatternRelativity {
30    /// Match the path regardless of the directory prefix
31    Anywhere,
32    /// Match the path if it only starts with `directory`
33    RelativeTo {
34        /// The directory that the pattern must have as prefix to be considered a match
35        directory: String,
36    },
37}
38
39/// Is the path only a directory, or could it be directory or file?
40#[derive(Debug, Clone, Hash, PartialEq, Eq)]
41pub enum PathKind {
42    /// Path matches to directory or file
43    Any,
44    /// Path matches only to directory
45    Directory,
46}
47
48/// Is this pattern a ignore or whitelist pattern?
49#[derive(Debug, Clone, Eq, PartialEq, Hash)]
50pub enum PatternEffect {
51    /// This is an ignore pattern
52    Ignore,
53    /// This is a whitelist pattern
54    Whitelist,
55}
56
57/// Do we get this pattern from a file (.gitignore, .xvcignore, ...) or specify it directly in
58/// code?
59#[derive(Debug, Clone, Hash, PartialEq, Eq)]
60pub enum Source {
61    /// Pattern is globally defined in code
62    Global,
63
64    /// Pattern is obtained from file
65    File {
66        /// Path of the pattern file
67        path: PathBuf,
68        /// (1-based) line number the pattern retrieved
69        line: usize,
70    },
71
72    /// Pattern is from CLI
73    CommandLine {
74        /// Current directory
75        current_dir: PathBuf,
76    },
77}
78
79/// Pattern is generic and could be an instance of String, Glob, Regex or any other object.
80/// The type is evolved by compiling.
81/// A pattern can start its life as `Pattern<String>` and can be compiled into `Pattern<Glob>` or
82/// `Pattern<Regex>`.
83#[derive(Debug)]
84pub struct Pattern {
85    /// The pattern type
86    pub glob: String,
87    /// The original string that defines the pattern
88    pub original: String,
89    /// Where did we get this pattern?
90    pub source: Source,
91    /// Is this ignore or whitelist pattern?
92    pub effect: PatternEffect,
93    /// Does it have an implied prefix?
94    pub relativity: PatternRelativity,
95    /// Is the path a directory or anything?
96    pub path_kind: PathKind,
97}
98
99impl Pattern {
100    /// Create a new pattern from a string and its source
101    pub fn new(source: Source, original: &str) -> Self {
102        let original = original.to_owned();
103        let current_dir = match &source {
104            Source::Global => "".to_string(),
105            Source::File { path, .. } => {
106                let path = path
107                    .parent()
108                    .expect("Pattern source file doesn't have parent")
109                    .to_string_lossy()
110                    .to_string();
111                if path.starts_with('/') {
112                    path
113                } else {
114                    format!("/{path}")
115                }
116            }
117            Source::CommandLine { current_dir } => current_dir.to_string_lossy().to_string(),
118        };
119
120        // if Pattern starts with ! it's whitelist, if ends with / it's dir only, if it contains
121        // non final slash, it should be considered under the current dir only, otherwise it
122        // matches
123
124        let begin_exclamation = original.starts_with('!');
125        let mut line = if begin_exclamation || original.starts_with(r"\!") {
126            original[1..].to_owned()
127        } else {
128            original.to_owned()
129        };
130
131        // TODO: We should handle filenames with trailing spaces better, with regex match and removing
132        // the \\ from the name
133        if !line.ends_with("\\ ") {
134            line = line.trim_end().to_string();
135        }
136
137        let end_slash = line.ends_with('/');
138        if end_slash {
139            line = line[..line.len() - 1].to_string()
140        }
141
142        let begin_slash = line.starts_with('/');
143        let non_final_slash = if !line.is_empty() {
144            line[..line.len() - 1].chars().any(|c| c == '/')
145        } else {
146            false
147        };
148
149        if begin_slash {
150            line = line[1..].to_string();
151        }
152
153        let current_dir = if current_dir.ends_with('/') {
154            &current_dir[..current_dir.len() - 1]
155        } else {
156            &current_dir
157        };
158
159        let effect = if begin_exclamation {
160            PatternEffect::Whitelist
161        } else {
162            PatternEffect::Ignore
163        };
164
165        let path_kind = if end_slash {
166            PathKind::Directory
167        } else {
168            PathKind::Any
169        };
170
171        let relativity = if non_final_slash {
172            PatternRelativity::RelativeTo {
173                directory: current_dir.to_owned(),
174            }
175        } else {
176            PatternRelativity::Anywhere
177        };
178
179        let glob = transform_pattern_for_glob(&line, relativity.clone(), path_kind.clone());
180
181        Pattern {
182            glob,
183            original,
184            source,
185            effect,
186            relativity,
187            path_kind,
188        }
189    }
190}
191
192fn transform_pattern_for_glob(
193    original: &str,
194    relativity: PatternRelativity,
195    path_kind: PathKind,
196) -> String {
197    let anything_anywhere = |p| format!("**/{p}");
198    let anything_relative = |p, directory| format!("{directory}/**/{p}");
199    let directory_anywhere = |p| format!("**/{p}/**");
200    let directory_relative = |p, directory| format!("{directory}/**/{p}/**");
201
202    match (path_kind, relativity) {
203        (PathKind::Any, PatternRelativity::Anywhere) => anything_anywhere(original),
204        (PathKind::Any, PatternRelativity::RelativeTo { directory }) => {
205            anything_relative(original, directory)
206        }
207        (PathKind::Directory, PatternRelativity::Anywhere) => directory_anywhere(original),
208        (PathKind::Directory, PatternRelativity::RelativeTo { directory }) => {
209            directory_relative(original, directory)
210        }
211    }
212}
213
214/// Build a list of patterns from a list of strings
215pub fn build_pattern_list(patterns: Vec<String>, source: Source) -> Vec<Pattern> {
216    patterns
217        .iter()
218        .map(|p| Pattern::new(source.clone(), p))
219        .collect()
220}