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