##// END OF EJS Templates
rust: simplify pattern file parsing...
Spencer Baugh -
r51750:796b5d66 default
parent child Browse files
Show More
@@ -1,776 +1,807 b''
1 1 // filepatterns.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Handling of Mercurial-specific patterns.
9 9
10 10 use crate::{
11 11 utils::{
12 12 files::{canonical_path, get_bytes_from_path, get_path_from_bytes},
13 13 hg_path::{path_to_hg_path_buf, HgPathBuf, HgPathError},
14 14 SliceExt,
15 15 },
16 16 FastHashMap, PatternError,
17 17 };
18 18 use lazy_static::lazy_static;
19 19 use regex::bytes::{NoExpand, Regex};
20 20 use std::ops::Deref;
21 21 use std::path::{Path, PathBuf};
22 22 use std::vec::Vec;
23 23
24 24 lazy_static! {
25 25 static ref RE_ESCAPE: Vec<Vec<u8>> = {
26 26 let mut v: Vec<Vec<u8>> = (0..=255).map(|byte| vec![byte]).collect();
27 27 let to_escape = b"()[]{}?*+-|^$\\.&~# \t\n\r\x0b\x0c";
28 28 for byte in to_escape {
29 29 v[*byte as usize].insert(0, b'\\');
30 30 }
31 31 v
32 32 };
33 33 }
34 34
35 35 /// These are matched in order
36 36 const GLOB_REPLACEMENTS: &[(&[u8], &[u8])] =
37 37 &[(b"*/", b"(?:.*/)?"), (b"*", b".*"), (b"", b"[^/]*")];
38 38
39 39 /// Appended to the regexp of globs
40 40 const GLOB_SUFFIX: &[u8; 7] = b"(?:/|$)";
41 41
42 42 #[derive(Debug, Clone, PartialEq, Eq)]
43 43 pub enum PatternSyntax {
44 44 /// A regular expression
45 45 Regexp,
46 46 /// Glob that matches at the front of the path
47 47 RootGlob,
48 48 /// Glob that matches at any suffix of the path (still anchored at
49 49 /// slashes)
50 50 Glob,
51 51 /// a path relative to repository root, which is matched recursively
52 52 Path,
53 53 /// a single exact path relative to repository root
54 54 FilePath,
55 55 /// A path relative to cwd
56 56 RelPath,
57 57 /// an unrooted glob (*.rs matches Rust files in all dirs)
58 58 RelGlob,
59 59 /// A regexp that needn't match the start of a name
60 60 RelRegexp,
61 61 /// A path relative to repository root, which is matched non-recursively
62 62 /// (will not match subdirectories)
63 63 RootFiles,
64 64 /// A file of patterns to read and include
65 65 Include,
66 66 /// A file of patterns to match against files under the same directory
67 67 SubInclude,
68 68 /// SubInclude with the result of parsing the included file
69 69 ///
70 70 /// Note: there is no ExpandedInclude because that expansion can be done
71 71 /// in place by replacing the Include pattern by the included patterns.
72 72 /// SubInclude requires more handling.
73 73 ///
74 74 /// Note: `Box` is used to minimize size impact on other enum variants
75 75 ExpandedSubInclude(Box<SubInclude>),
76 76 }
77 77
78 78 /// Transforms a glob pattern into a regex
79 79 fn glob_to_re(pat: &[u8]) -> Vec<u8> {
80 80 let mut input = pat;
81 81 let mut res: Vec<u8> = vec![];
82 82 let mut group_depth = 0;
83 83
84 84 while let Some((c, rest)) = input.split_first() {
85 85 input = rest;
86 86
87 87 match c {
88 88 b'*' => {
89 89 for (source, repl) in GLOB_REPLACEMENTS {
90 90 if let Some(rest) = input.drop_prefix(source) {
91 91 input = rest;
92 92 res.extend(*repl);
93 93 break;
94 94 }
95 95 }
96 96 }
97 97 b'?' => res.extend(b"."),
98 98 b'[' => {
99 99 match input.iter().skip(1).position(|b| *b == b']') {
100 100 None => res.extend(b"\\["),
101 101 Some(end) => {
102 102 // Account for the one we skipped
103 103 let end = end + 1;
104 104
105 105 res.extend(b"[");
106 106
107 107 for (i, b) in input[..end].iter().enumerate() {
108 108 if *b == b'!' && i == 0 {
109 109 res.extend(b"^")
110 110 } else if *b == b'^' && i == 0 {
111 111 res.extend(b"\\^")
112 112 } else if *b == b'\\' {
113 113 res.extend(b"\\\\")
114 114 } else {
115 115 res.push(*b)
116 116 }
117 117 }
118 118 res.extend(b"]");
119 119 input = &input[end + 1..];
120 120 }
121 121 }
122 122 }
123 123 b'{' => {
124 124 group_depth += 1;
125 125 res.extend(b"(?:")
126 126 }
127 127 b'}' if group_depth > 0 => {
128 128 group_depth -= 1;
129 129 res.extend(b")");
130 130 }
131 131 b',' if group_depth > 0 => res.extend(b"|"),
132 132 b'\\' => {
133 133 let c = {
134 134 if let Some((c, rest)) = input.split_first() {
135 135 input = rest;
136 136 c
137 137 } else {
138 138 c
139 139 }
140 140 };
141 141 res.extend(&RE_ESCAPE[*c as usize])
142 142 }
143 143 _ => res.extend(&RE_ESCAPE[*c as usize]),
144 144 }
145 145 }
146 146 res
147 147 }
148 148
149 149 fn escape_pattern(pattern: &[u8]) -> Vec<u8> {
150 150 pattern
151 151 .iter()
152 152 .flat_map(|c| RE_ESCAPE[*c as usize].clone())
153 153 .collect()
154 154 }
155 155
156 156 pub fn parse_pattern_syntax(
157 157 kind: &[u8],
158 158 ) -> Result<PatternSyntax, PatternError> {
159 159 match kind {
160 160 b"re:" => Ok(PatternSyntax::Regexp),
161 161 b"path:" => Ok(PatternSyntax::Path),
162 162 b"filepath:" => Ok(PatternSyntax::FilePath),
163 163 b"relpath:" => Ok(PatternSyntax::RelPath),
164 164 b"rootfilesin:" => Ok(PatternSyntax::RootFiles),
165 165 b"relglob:" => Ok(PatternSyntax::RelGlob),
166 166 b"relre:" => Ok(PatternSyntax::RelRegexp),
167 167 b"glob:" => Ok(PatternSyntax::Glob),
168 168 b"rootglob:" => Ok(PatternSyntax::RootGlob),
169 169 b"include:" => Ok(PatternSyntax::Include),
170 170 b"subinclude:" => Ok(PatternSyntax::SubInclude),
171 171 _ => Err(PatternError::UnsupportedSyntax(
172 172 String::from_utf8_lossy(kind).to_string(),
173 173 )),
174 174 }
175 175 }
176 176
177 177 lazy_static! {
178 178 static ref FLAG_RE: Regex = Regex::new(r"^\(\?[aiLmsux]+\)").unwrap();
179 179 }
180 180
181 181 /// Builds the regex that corresponds to the given pattern.
182 182 /// If within a `syntax: regexp` context, returns the pattern,
183 183 /// otherwise, returns the corresponding regex.
184 184 fn _build_single_regex(entry: &IgnorePattern) -> Vec<u8> {
185 185 let IgnorePattern {
186 186 syntax, pattern, ..
187 187 } = entry;
188 188 if pattern.is_empty() {
189 189 return vec![];
190 190 }
191 191 match syntax {
192 192 PatternSyntax::Regexp => pattern.to_owned(),
193 193 PatternSyntax::RelRegexp => {
194 194 // The `regex` crate accepts `**` while `re2` and Python's `re`
195 195 // do not. Checking for `*` correctly triggers the same error all
196 196 // engines.
197 197 if pattern[0] == b'^'
198 198 || pattern[0] == b'*'
199 199 || pattern.starts_with(b".*")
200 200 {
201 201 return pattern.to_owned();
202 202 }
203 203 match FLAG_RE.find(pattern) {
204 204 Some(mat) => {
205 205 let s = mat.start();
206 206 let e = mat.end();
207 207 [
208 208 &b"(?"[..],
209 209 &pattern[s + 2..e - 1],
210 210 &b":"[..],
211 211 if pattern[e] == b'^'
212 212 || pattern[e] == b'*'
213 213 || pattern[e..].starts_with(b".*")
214 214 {
215 215 &b""[..]
216 216 } else {
217 217 &b".*"[..]
218 218 },
219 219 &pattern[e..],
220 220 &b")"[..],
221 221 ]
222 222 .concat()
223 223 }
224 224 None => [&b".*"[..], pattern].concat(),
225 225 }
226 226 }
227 227 PatternSyntax::Path | PatternSyntax::RelPath => {
228 228 if pattern == b"." {
229 229 return vec![];
230 230 }
231 231 [escape_pattern(pattern).as_slice(), b"(?:/|$)"].concat()
232 232 }
233 233 PatternSyntax::RootFiles => {
234 234 let mut res = if pattern == b"." {
235 235 vec![]
236 236 } else {
237 237 // Pattern is a directory name.
238 238 [escape_pattern(pattern).as_slice(), b"/"].concat()
239 239 };
240 240
241 241 // Anything after the pattern must be a non-directory.
242 242 res.extend(b"[^/]+$");
243 243 res
244 244 }
245 245 PatternSyntax::RelGlob => {
246 246 let glob_re = glob_to_re(pattern);
247 247 if let Some(rest) = glob_re.drop_prefix(b"[^/]*") {
248 248 [b".*", rest, GLOB_SUFFIX].concat()
249 249 } else {
250 250 [b"(?:.*/)?", glob_re.as_slice(), GLOB_SUFFIX].concat()
251 251 }
252 252 }
253 253 PatternSyntax::Glob | PatternSyntax::RootGlob => {
254 254 [glob_to_re(pattern).as_slice(), GLOB_SUFFIX].concat()
255 255 }
256 256 PatternSyntax::Include
257 257 | PatternSyntax::SubInclude
258 258 | PatternSyntax::ExpandedSubInclude(_)
259 259 | PatternSyntax::FilePath => unreachable!(),
260 260 }
261 261 }
262 262
263 263 const GLOB_SPECIAL_CHARACTERS: [u8; 7] =
264 264 [b'*', b'?', b'[', b']', b'{', b'}', b'\\'];
265 265
266 266 /// TODO support other platforms
267 267 #[cfg(unix)]
268 268 pub fn normalize_path_bytes(bytes: &[u8]) -> Vec<u8> {
269 269 if bytes.is_empty() {
270 270 return b".".to_vec();
271 271 }
272 272 let sep = b'/';
273 273
274 274 let mut initial_slashes = bytes.iter().take_while(|b| **b == sep).count();
275 275 if initial_slashes > 2 {
276 276 // POSIX allows one or two initial slashes, but treats three or more
277 277 // as single slash.
278 278 initial_slashes = 1;
279 279 }
280 280 let components = bytes
281 281 .split(|b| *b == sep)
282 282 .filter(|c| !(c.is_empty() || c == b"."))
283 283 .fold(vec![], |mut acc, component| {
284 284 if component != b".."
285 285 || (initial_slashes == 0 && acc.is_empty())
286 286 || (!acc.is_empty() && acc[acc.len() - 1] == b"..")
287 287 {
288 288 acc.push(component)
289 289 } else if !acc.is_empty() {
290 290 acc.pop();
291 291 }
292 292 acc
293 293 });
294 294 let mut new_bytes = components.join(&sep);
295 295
296 296 if initial_slashes > 0 {
297 297 let mut buf: Vec<_> = (0..initial_slashes).map(|_| sep).collect();
298 298 buf.extend(new_bytes);
299 299 new_bytes = buf;
300 300 }
301 301 if new_bytes.is_empty() {
302 302 b".".to_vec()
303 303 } else {
304 304 new_bytes
305 305 }
306 306 }
307 307
308 308 /// Wrapper function to `_build_single_regex` that short-circuits 'exact' globs
309 309 /// that don't need to be transformed into a regex.
310 310 pub fn build_single_regex(
311 311 entry: &IgnorePattern,
312 312 ) -> Result<Option<Vec<u8>>, PatternError> {
313 313 let IgnorePattern {
314 314 pattern, syntax, ..
315 315 } = entry;
316 316 let pattern = match syntax {
317 317 PatternSyntax::RootGlob
318 318 | PatternSyntax::Path
319 319 | PatternSyntax::RelGlob
320 320 | PatternSyntax::RootFiles => normalize_path_bytes(pattern),
321 321 PatternSyntax::Include | PatternSyntax::SubInclude => {
322 322 return Err(PatternError::NonRegexPattern(entry.clone()))
323 323 }
324 324 _ => pattern.to_owned(),
325 325 };
326 326 let is_simple_rootglob = *syntax == PatternSyntax::RootGlob
327 327 && !pattern.iter().any(|b| GLOB_SPECIAL_CHARACTERS.contains(b));
328 328 if is_simple_rootglob || syntax == &PatternSyntax::FilePath {
329 329 Ok(None)
330 330 } else {
331 331 let mut entry = entry.clone();
332 332 entry.pattern = pattern;
333 333 Ok(Some(_build_single_regex(&entry)))
334 334 }
335 335 }
336 336
337 337 lazy_static! {
338 static ref SYNTAXES: FastHashMap<&'static [u8], &'static [u8]> = {
338 static ref SYNTAXES: FastHashMap<&'static [u8], PatternSyntax> = {
339 339 let mut m = FastHashMap::default();
340 340
341 m.insert(b"re".as_ref(), b"relre:".as_ref());
342 m.insert(b"regexp".as_ref(), b"relre:".as_ref());
343 m.insert(b"glob".as_ref(), b"relglob:".as_ref());
344 m.insert(b"rootglob".as_ref(), b"rootglob:".as_ref());
345 m.insert(b"include".as_ref(), b"include:".as_ref());
346 m.insert(b"subinclude".as_ref(), b"subinclude:".as_ref());
347 m.insert(b"path".as_ref(), b"path:".as_ref());
348 m.insert(b"rootfilesin".as_ref(), b"rootfilesin:".as_ref());
341 m.insert(b"re:".as_ref(), PatternSyntax::Regexp);
342 m.insert(b"regexp:".as_ref(), PatternSyntax::Regexp);
343 m.insert(b"path:".as_ref(), PatternSyntax::Path);
344 m.insert(b"filepath:".as_ref(), PatternSyntax::FilePath);
345 m.insert(b"relpath:".as_ref(), PatternSyntax::RelPath);
346 m.insert(b"rootfilesin:".as_ref(), PatternSyntax::RootFiles);
347 m.insert(b"relglob:".as_ref(), PatternSyntax::RelGlob);
348 m.insert(b"relre:".as_ref(), PatternSyntax::RelRegexp);
349 m.insert(b"glob:".as_ref(), PatternSyntax::Glob);
350 m.insert(b"rootglob:".as_ref(), PatternSyntax::RootGlob);
351 m.insert(b"include:".as_ref(), PatternSyntax::Include);
352 m.insert(b"subinclude:".as_ref(), PatternSyntax::SubInclude);
353
349 354 m
350 355 };
351 356 }
352 357
353 358 #[derive(Debug)]
354 359 pub enum PatternFileWarning {
355 360 /// (file path, syntax bytes)
356 361 InvalidSyntax(PathBuf, Vec<u8>),
357 362 /// File path
358 363 NoSuchFile(PathBuf),
359 364 }
360 365
366 pub fn parse_one_pattern(
367 pattern: &[u8],
368 source: &Path,
369 default: PatternSyntax,
370 ) -> IgnorePattern {
371 let mut pattern_bytes: &[u8] = pattern;
372 let mut syntax = default;
373
374 for (s, val) in SYNTAXES.iter() {
375 if let Some(rest) = pattern_bytes.drop_prefix(s) {
376 syntax = val.clone();
377 pattern_bytes = rest;
378 break;
379 }
380 }
381
382 let pattern = pattern_bytes.to_vec();
383
384 IgnorePattern {
385 syntax,
386 pattern,
387 source: source.to_owned(),
388 }
389 }
390
361 391 pub fn parse_pattern_file_contents(
362 392 lines: &[u8],
363 393 file_path: &Path,
364 default_syntax_override: Option<&[u8]>,
394 default_syntax_override: Option<PatternSyntax>,
365 395 warn: bool,
396 relativize: bool,
366 397 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
367 398 let comment_regex = Regex::new(r"((?:^|[^\\])(?:\\\\)*)#.*").unwrap();
368 399
369 400 #[allow(clippy::trivial_regex)]
370 401 let comment_escape_regex = Regex::new(r"\\#").unwrap();
371 402 let mut inputs: Vec<IgnorePattern> = vec![];
372 403 let mut warnings: Vec<PatternFileWarning> = vec![];
373 404
374 405 let mut current_syntax =
375 default_syntax_override.unwrap_or_else(|| b"relre:".as_ref());
406 default_syntax_override.unwrap_or(PatternSyntax::RelRegexp);
376 407
377 for (line_number, mut line) in lines.split(|c| *c == b'\n').enumerate() {
378 let line_number = line_number + 1;
379
408 for mut line in lines.split(|c| *c == b'\n') {
380 409 let line_buf;
381 410 if line.contains(&b'#') {
382 411 if let Some(cap) = comment_regex.captures(line) {
383 412 line = &line[..cap.get(1).unwrap().end()]
384 413 }
385 414 line_buf = comment_escape_regex.replace_all(line, NoExpand(b"#"));
386 415 line = &line_buf;
387 416 }
388 417
389 let mut line = line.trim_end();
418 let line = line.trim_end();
390 419
391 420 if line.is_empty() {
392 421 continue;
393 422 }
394 423
395 424 if let Some(syntax) = line.drop_prefix(b"syntax:") {
396 425 let syntax = syntax.trim();
397 426
398 if let Some(rel_syntax) = SYNTAXES.get(syntax) {
399 current_syntax = rel_syntax;
427 if let Some(parsed) =
428 SYNTAXES.get([syntax, &b":"[..]].concat().as_slice())
429 {
430 current_syntax = parsed.clone();
400 431 } else if warn {
401 432 warnings.push(PatternFileWarning::InvalidSyntax(
402 433 file_path.to_owned(),
403 434 syntax.to_owned(),
404 435 ));
405 436 }
406 continue;
437 } else {
438 let pattern = parse_one_pattern(
439 line,
440 file_path,
441 current_syntax.clone(),
442 );
443 inputs.push(if relativize {
444 pattern.to_relative()
445 } else {
446 pattern
447 })
407 448 }
408
409 let mut line_syntax: &[u8] = current_syntax;
410
411 for (s, rels) in SYNTAXES.iter() {
412 if let Some(rest) = line.drop_prefix(rels) {
413 line_syntax = rels;
414 line = rest;
415 break;
416 }
417 if let Some(rest) = line.drop_prefix(&[s, &b":"[..]].concat()) {
418 line_syntax = rels;
419 line = rest;
420 break;
421 }
422 }
423
424 inputs.push(IgnorePattern::new(
425 parse_pattern_syntax(line_syntax).map_err(|e| match e {
426 PatternError::UnsupportedSyntax(syntax) => {
427 PatternError::UnsupportedSyntaxInFile(
428 syntax,
429 file_path.to_string_lossy().into(),
430 line_number,
431 )
432 }
433 _ => e,
434 })?,
435 line,
436 file_path,
437 ));
438 449 }
439 450 Ok((inputs, warnings))
440 451 }
441 452
442 453 pub fn read_pattern_file(
443 454 file_path: &Path,
444 455 warn: bool,
445 456 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
446 457 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
447 458 match std::fs::read(file_path) {
448 459 Ok(contents) => {
449 460 inspect_pattern_bytes(file_path, &contents);
450 parse_pattern_file_contents(&contents, file_path, None, warn)
461 parse_pattern_file_contents(&contents, file_path, None, warn, true)
451 462 }
452 463 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok((
453 464 vec![],
454 465 vec![PatternFileWarning::NoSuchFile(file_path.to_owned())],
455 466 )),
456 467 Err(e) => Err(e.into()),
457 468 }
458 469 }
459 470
460 471 /// Represents an entry in an "ignore" file.
461 472 #[derive(Debug, Eq, PartialEq, Clone)]
462 473 pub struct IgnorePattern {
463 474 pub syntax: PatternSyntax,
464 475 pub pattern: Vec<u8>,
465 476 pub source: PathBuf,
466 477 }
467 478
468 479 impl IgnorePattern {
469 480 pub fn new(syntax: PatternSyntax, pattern: &[u8], source: &Path) -> Self {
470 481 Self {
471 482 syntax,
472 483 pattern: pattern.to_owned(),
473 484 source: source.to_owned(),
474 485 }
475 486 }
487
488 pub fn to_relative(self) -> Self {
489 let Self {
490 syntax,
491 pattern,
492 source,
493 } = self;
494 Self {
495 syntax: match syntax {
496 PatternSyntax::Regexp => PatternSyntax::RelRegexp,
497 PatternSyntax::Glob => PatternSyntax::RelGlob,
498 x => x,
499 },
500 pattern,
501 source,
502 }
503 }
476 504 }
477 505
478 506 pub type PatternResult<T> = Result<T, PatternError>;
479 507
480 508 /// Wrapper for `read_pattern_file` that also recursively expands `include:`
481 509 /// and `subinclude:` patterns.
482 510 ///
483 511 /// The former are expanded in place, while `PatternSyntax::ExpandedSubInclude`
484 512 /// is used for the latter to form a tree of patterns.
485 513 pub fn get_patterns_from_file(
486 514 pattern_file: &Path,
487 515 root_dir: &Path,
488 516 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
489 517 ) -> PatternResult<(Vec<IgnorePattern>, Vec<PatternFileWarning>)> {
490 518 let (patterns, mut warnings) =
491 519 read_pattern_file(pattern_file, true, inspect_pattern_bytes)?;
492 520 let patterns = patterns
493 521 .into_iter()
494 522 .flat_map(|entry| -> PatternResult<_> {
495 523 Ok(match &entry.syntax {
496 524 PatternSyntax::Include => {
497 525 let inner_include =
498 526 root_dir.join(get_path_from_bytes(&entry.pattern));
499 527 let (inner_pats, inner_warnings) = get_patterns_from_file(
500 528 &inner_include,
501 529 root_dir,
502 530 inspect_pattern_bytes,
503 531 )?;
504 532 warnings.extend(inner_warnings);
505 533 inner_pats
506 534 }
507 535 PatternSyntax::SubInclude => {
508 536 let mut sub_include = SubInclude::new(
509 537 root_dir,
510 538 &entry.pattern,
511 539 &entry.source,
512 540 )?;
513 541 let (inner_patterns, inner_warnings) =
514 542 get_patterns_from_file(
515 543 &sub_include.path,
516 544 &sub_include.root,
517 545 inspect_pattern_bytes,
518 546 )?;
519 547 sub_include.included_patterns = inner_patterns;
520 548 warnings.extend(inner_warnings);
521 549 vec![IgnorePattern {
522 550 syntax: PatternSyntax::ExpandedSubInclude(Box::new(
523 551 sub_include,
524 552 )),
525 553 ..entry
526 554 }]
527 555 }
528 556 _ => vec![entry],
529 557 })
530 558 })
531 559 .flatten()
532 560 .collect();
533 561
534 562 Ok((patterns, warnings))
535 563 }
536 564
537 565 /// Holds all the information needed to handle a `subinclude:` pattern.
538 566 #[derive(Debug, PartialEq, Eq, Clone)]
539 567 pub struct SubInclude {
540 568 /// Will be used for repository (hg) paths that start with this prefix.
541 569 /// It is relative to the current working directory, so comparing against
542 570 /// repository paths is painless.
543 571 pub prefix: HgPathBuf,
544 572 /// The file itself, containing the patterns
545 573 pub path: PathBuf,
546 574 /// Folder in the filesystem where this it applies
547 575 pub root: PathBuf,
548 576
549 577 pub included_patterns: Vec<IgnorePattern>,
550 578 }
551 579
552 580 impl SubInclude {
553 581 pub fn new(
554 582 root_dir: &Path,
555 583 pattern: &[u8],
556 584 source: &Path,
557 585 ) -> Result<SubInclude, HgPathError> {
558 586 let normalized_source =
559 587 normalize_path_bytes(&get_bytes_from_path(source));
560 588
561 589 let source_root = get_path_from_bytes(&normalized_source);
562 590 let source_root =
563 591 source_root.parent().unwrap_or_else(|| source_root.deref());
564 592
565 593 let path = source_root.join(get_path_from_bytes(pattern));
566 594 let new_root = path.parent().unwrap_or_else(|| path.deref());
567 595
568 596 let prefix = canonical_path(root_dir, root_dir, new_root)?;
569 597
570 598 Ok(Self {
571 599 prefix: path_to_hg_path_buf(prefix).map(|mut p| {
572 600 if !p.is_empty() {
573 601 p.push_byte(b'/');
574 602 }
575 603 p
576 604 })?,
577 605 path: path.to_owned(),
578 606 root: new_root.to_owned(),
579 607 included_patterns: Vec::new(),
580 608 })
581 609 }
582 610 }
583 611
584 612 /// Separate and pre-process subincludes from other patterns for the "ignore"
585 613 /// phase.
586 614 pub fn filter_subincludes(
587 615 ignore_patterns: Vec<IgnorePattern>,
588 616 ) -> Result<(Vec<SubInclude>, Vec<IgnorePattern>), HgPathError> {
589 617 let mut subincludes = vec![];
590 618 let mut others = vec![];
591 619
592 620 for pattern in ignore_patterns {
593 621 if let PatternSyntax::ExpandedSubInclude(sub_include) = pattern.syntax
594 622 {
595 623 subincludes.push(*sub_include);
596 624 } else {
597 625 others.push(pattern)
598 626 }
599 627 }
600 628 Ok((subincludes, others))
601 629 }
602 630
603 631 #[cfg(test)]
604 632 mod tests {
605 633 use super::*;
606 634 use pretty_assertions::assert_eq;
607 635
608 636 #[test]
609 637 fn escape_pattern_test() {
610 638 let untouched =
611 639 br#"!"%',/0123456789:;<=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz"#;
612 640 assert_eq!(escape_pattern(untouched), untouched.to_vec());
613 641 // All escape codes
614 642 assert_eq!(
615 643 escape_pattern(br#"()[]{}?*+-|^$\\.&~# \t\n\r\v\f"#),
616 644 br#"\(\)\[\]\{\}\?\*\+\-\|\^\$\\\\\.\&\~\#\ \\t\\n\\r\\v\\f"#
617 645 .to_vec()
618 646 );
619 647 }
620 648
621 649 #[test]
622 650 fn glob_test() {
623 651 assert_eq!(glob_to_re(br#"?"#), br#"."#);
624 652 assert_eq!(glob_to_re(br#"*"#), br#"[^/]*"#);
625 653 assert_eq!(glob_to_re(br#"**"#), br#".*"#);
626 654 assert_eq!(glob_to_re(br#"**/a"#), br#"(?:.*/)?a"#);
627 655 assert_eq!(glob_to_re(br#"a/**/b"#), br#"a/(?:.*/)?b"#);
628 656 assert_eq!(glob_to_re(br#"[a*?!^][^b][!c]"#), br#"[a*?!^][\^b][^c]"#);
629 657 assert_eq!(glob_to_re(br#"{a,b}"#), br#"(?:a|b)"#);
630 658 assert_eq!(glob_to_re(br#".\*\?"#), br#"\.\*\?"#);
631 659 }
632 660
633 661 #[test]
634 662 fn test_parse_pattern_file_contents() {
635 663 let lines = b"syntax: glob\n*.elc";
636 664
637 665 assert_eq!(
638 666 parse_pattern_file_contents(
639 667 lines,
640 668 Path::new("file_path"),
641 669 None,
642 false
670 false,
671 true,
643 672 )
644 673 .unwrap()
645 674 .0,
646 675 vec![IgnorePattern::new(
647 676 PatternSyntax::RelGlob,
648 677 b"*.elc",
649 678 Path::new("file_path")
650 679 )],
651 680 );
652 681
653 682 let lines = b"syntax: include\nsyntax: glob";
654 683
655 684 assert_eq!(
656 685 parse_pattern_file_contents(
657 686 lines,
658 687 Path::new("file_path"),
659 688 None,
660 false
689 false,
690 true,
661 691 )
662 692 .unwrap()
663 693 .0,
664 694 vec![]
665 695 );
666 696 let lines = b"glob:**.o";
667 697 assert_eq!(
668 698 parse_pattern_file_contents(
669 699 lines,
670 700 Path::new("file_path"),
671 701 None,
672 false
702 false,
703 true,
673 704 )
674 705 .unwrap()
675 706 .0,
676 707 vec![IgnorePattern::new(
677 708 PatternSyntax::RelGlob,
678 709 b"**.o",
679 710 Path::new("file_path")
680 711 )]
681 712 );
682 713 }
683 714
684 715 #[test]
685 716 fn test_build_single_regex() {
686 717 assert_eq!(
687 718 build_single_regex(&IgnorePattern::new(
688 719 PatternSyntax::RelGlob,
689 720 b"rust/target/",
690 721 Path::new("")
691 722 ))
692 723 .unwrap(),
693 724 Some(br"(?:.*/)?rust/target(?:/|$)".to_vec()),
694 725 );
695 726 assert_eq!(
696 727 build_single_regex(&IgnorePattern::new(
697 728 PatternSyntax::Regexp,
698 729 br"rust/target/\d+",
699 730 Path::new("")
700 731 ))
701 732 .unwrap(),
702 733 Some(br"rust/target/\d+".to_vec()),
703 734 );
704 735 }
705 736
706 737 #[test]
707 738 fn test_build_single_regex_shortcut() {
708 739 assert_eq!(
709 740 build_single_regex(&IgnorePattern::new(
710 741 PatternSyntax::RootGlob,
711 742 b"",
712 743 Path::new("")
713 744 ))
714 745 .unwrap(),
715 746 None,
716 747 );
717 748 assert_eq!(
718 749 build_single_regex(&IgnorePattern::new(
719 750 PatternSyntax::RootGlob,
720 751 b"whatever",
721 752 Path::new("")
722 753 ))
723 754 .unwrap(),
724 755 None,
725 756 );
726 757 assert_eq!(
727 758 build_single_regex(&IgnorePattern::new(
728 759 PatternSyntax::RootGlob,
729 760 b"*.o",
730 761 Path::new("")
731 762 ))
732 763 .unwrap(),
733 764 Some(br"[^/]*\.o(?:/|$)".to_vec()),
734 765 );
735 766 }
736 767
737 768 #[test]
738 769 fn test_build_single_relregex() {
739 770 assert_eq!(
740 771 build_single_regex(&IgnorePattern::new(
741 772 PatternSyntax::RelRegexp,
742 773 b"^ba{2}r",
743 774 Path::new("")
744 775 ))
745 776 .unwrap(),
746 777 Some(b"^ba{2}r".to_vec()),
747 778 );
748 779 assert_eq!(
749 780 build_single_regex(&IgnorePattern::new(
750 781 PatternSyntax::RelRegexp,
751 782 b"ba{2}r",
752 783 Path::new("")
753 784 ))
754 785 .unwrap(),
755 786 Some(b".*ba{2}r".to_vec()),
756 787 );
757 788 assert_eq!(
758 789 build_single_regex(&IgnorePattern::new(
759 790 PatternSyntax::RelRegexp,
760 791 b"(?ia)ba{2}r",
761 792 Path::new("")
762 793 ))
763 794 .unwrap(),
764 795 Some(b"(?ia:.*ba{2}r)".to_vec()),
765 796 );
766 797 assert_eq!(
767 798 build_single_regex(&IgnorePattern::new(
768 799 PatternSyntax::RelRegexp,
769 800 b"(?ia)^ba{2}r",
770 801 Path::new("")
771 802 ))
772 803 .unwrap(),
773 804 Some(b"(?ia:^ba{2}r)".to_vec()),
774 805 );
775 806 }
776 807 }
@@ -1,113 +1,115 b''
1 1 use std::path::Path;
2 2
3 3 use crate::{
4 4 errors::HgError,
5 5 exit_codes,
6 6 filepatterns::parse_pattern_file_contents,
7 7 matchers::{
8 8 AlwaysMatcher, DifferenceMatcher, IncludeMatcher, Matcher,
9 9 NeverMatcher,
10 10 },
11 11 repo::Repo,
12 12 requirements::NARROW_REQUIREMENT,
13 13 sparse::{self, SparseConfigError, SparseWarning},
14 14 };
15 15
16 16 /// The file in .hg/store/ that indicates which paths exit in the store
17 17 const FILENAME: &str = "narrowspec";
18 18 /// The file in .hg/ that indicates which paths exit in the dirstate
19 19 const DIRSTATE_FILENAME: &str = "narrowspec.dirstate";
20 20
21 21 /// Pattern prefixes that are allowed in narrow patterns. This list MUST
22 22 /// only contain patterns that are fast and safe to evaluate. Keep in mind
23 23 /// that patterns are supplied by clients and executed on remote servers
24 24 /// as part of wire protocol commands. That means that changes to this
25 25 /// data structure influence the wire protocol and should not be taken
26 26 /// lightly - especially removals.
27 27 const VALID_PREFIXES: [&str; 2] = ["path:", "rootfilesin:"];
28 28
29 29 /// Return the matcher for the current narrow spec, and all configuration
30 30 /// warnings to display.
31 31 pub fn matcher(
32 32 repo: &Repo,
33 33 ) -> Result<(Box<dyn Matcher + Sync>, Vec<SparseWarning>), SparseConfigError> {
34 34 let mut warnings = vec![];
35 35 if !repo.requirements().contains(NARROW_REQUIREMENT) {
36 36 return Ok((Box::new(AlwaysMatcher), warnings));
37 37 }
38 38 // Treat "narrowspec does not exist" the same as "narrowspec file exists
39 39 // and is empty".
40 40 let store_spec = repo.store_vfs().try_read(FILENAME)?.unwrap_or_default();
41 41 let working_copy_spec = repo
42 42 .hg_vfs()
43 43 .try_read(DIRSTATE_FILENAME)?
44 44 .unwrap_or_default();
45 45 if store_spec != working_copy_spec {
46 46 return Err(HgError::abort(
47 47 "abort: working copy's narrowspec is stale",
48 48 exit_codes::STATE_ERROR,
49 49 Some("run 'hg tracked --update-working-copy'".into()),
50 50 )
51 51 .into());
52 52 }
53 53
54 54 let config = sparse::parse_config(
55 55 &store_spec,
56 56 sparse::SparseConfigContext::Narrow,
57 57 )?;
58 58
59 59 warnings.extend(config.warnings);
60 60
61 61 if !config.profiles.is_empty() {
62 62 // TODO (from Python impl) maybe do something with profiles?
63 63 return Err(SparseConfigError::IncludesInNarrow);
64 64 }
65 65 validate_patterns(&config.includes)?;
66 66 validate_patterns(&config.excludes)?;
67 67
68 68 if config.includes.is_empty() {
69 69 return Ok((Box::new(NeverMatcher), warnings));
70 70 }
71 71
72 72 let (patterns, subwarnings) = parse_pattern_file_contents(
73 73 &config.includes,
74 74 Path::new(""),
75 75 None,
76 76 false,
77 true,
77 78 )?;
78 79 warnings.extend(subwarnings.into_iter().map(From::from));
79 80
80 81 let mut m: Box<dyn Matcher + Sync> =
81 82 Box::new(IncludeMatcher::new(patterns)?);
82 83
83 84 let (patterns, subwarnings) = parse_pattern_file_contents(
84 85 &config.excludes,
85 86 Path::new(""),
86 87 None,
87 88 false,
89 true,
88 90 )?;
89 91 if !patterns.is_empty() {
90 92 warnings.extend(subwarnings.into_iter().map(From::from));
91 93 let exclude_matcher = Box::new(IncludeMatcher::new(patterns)?);
92 94 m = Box::new(DifferenceMatcher::new(m, exclude_matcher));
93 95 }
94 96
95 97 Ok((m, warnings))
96 98 }
97 99
98 100 fn validate_patterns(patterns: &[u8]) -> Result<(), SparseConfigError> {
99 101 for pattern in patterns.split(|c| *c == b'\n') {
100 102 if pattern.is_empty() {
101 103 continue;
102 104 }
103 105 for prefix in VALID_PREFIXES.iter() {
104 106 if pattern.starts_with(prefix.as_bytes()) {
105 107 return Ok(());
106 108 }
107 109 }
108 110 return Err(SparseConfigError::InvalidNarrowPrefix(
109 111 pattern.to_owned(),
110 112 ));
111 113 }
112 114 Ok(())
113 115 }
@@ -1,339 +1,341 b''
1 1 use std::{collections::HashSet, path::Path};
2 2
3 3 use format_bytes::{write_bytes, DisplayBytes};
4 4
5 5 use crate::{
6 6 errors::HgError,
7 7 filepatterns::parse_pattern_file_contents,
8 8 matchers::{
9 9 AlwaysMatcher, DifferenceMatcher, IncludeMatcher, Matcher,
10 10 UnionMatcher,
11 11 },
12 12 operations::cat,
13 13 repo::Repo,
14 14 requirements::SPARSE_REQUIREMENT,
15 15 utils::{hg_path::HgPath, SliceExt},
16 16 IgnorePattern, PatternError, PatternFileWarning, PatternSyntax, Revision,
17 17 NULL_REVISION,
18 18 };
19 19
20 20 /// Command which is triggering the config read
21 21 #[derive(Copy, Clone, Debug)]
22 22 pub enum SparseConfigContext {
23 23 Sparse,
24 24 Narrow,
25 25 }
26 26
27 27 impl DisplayBytes for SparseConfigContext {
28 28 fn display_bytes(
29 29 &self,
30 30 output: &mut dyn std::io::Write,
31 31 ) -> std::io::Result<()> {
32 32 match self {
33 33 SparseConfigContext::Sparse => write_bytes!(output, b"sparse"),
34 34 SparseConfigContext::Narrow => write_bytes!(output, b"narrow"),
35 35 }
36 36 }
37 37 }
38 38
39 39 /// Possible warnings when reading sparse configuration
40 40 #[derive(Debug, derive_more::From)]
41 41 pub enum SparseWarning {
42 42 /// Warns about improper paths that start with "/"
43 43 RootWarning {
44 44 context: SparseConfigContext,
45 45 line: Vec<u8>,
46 46 },
47 47 /// Warns about a profile missing from the given changelog revision
48 48 ProfileNotFound { profile: Vec<u8>, rev: Revision },
49 49 #[from]
50 50 Pattern(PatternFileWarning),
51 51 }
52 52
53 53 /// Parsed sparse config
54 54 #[derive(Debug, Default)]
55 55 pub struct SparseConfig {
56 56 // Line-separated
57 57 pub(crate) includes: Vec<u8>,
58 58 // Line-separated
59 59 pub(crate) excludes: Vec<u8>,
60 60 pub(crate) profiles: HashSet<Vec<u8>>,
61 61 pub(crate) warnings: Vec<SparseWarning>,
62 62 }
63 63
64 64 /// All possible errors when reading sparse/narrow config
65 65 #[derive(Debug, derive_more::From)]
66 66 pub enum SparseConfigError {
67 67 IncludesAfterExcludes {
68 68 context: SparseConfigContext,
69 69 },
70 70 EntryOutsideSection {
71 71 context: SparseConfigContext,
72 72 line: Vec<u8>,
73 73 },
74 74 /// Narrow config does not support '%include' directives
75 75 IncludesInNarrow,
76 76 /// An invalid pattern prefix was given to the narrow spec. Includes the
77 77 /// entire pattern for context.
78 78 InvalidNarrowPrefix(Vec<u8>),
79 79 #[from]
80 80 HgError(HgError),
81 81 #[from]
82 82 PatternError(PatternError),
83 83 }
84 84
85 85 /// Parse sparse config file content.
86 86 pub(crate) fn parse_config(
87 87 raw: &[u8],
88 88 context: SparseConfigContext,
89 89 ) -> Result<SparseConfig, SparseConfigError> {
90 90 let mut includes = vec![];
91 91 let mut excludes = vec![];
92 92 let mut profiles = HashSet::new();
93 93 let mut warnings = vec![];
94 94
95 95 #[derive(PartialEq, Eq)]
96 96 enum Current {
97 97 Includes,
98 98 Excludes,
99 99 None,
100 100 }
101 101
102 102 let mut current = Current::None;
103 103 let mut in_section = false;
104 104
105 105 for line in raw.split(|c| *c == b'\n') {
106 106 let line = line.trim();
107 107 if line.is_empty() || line[0] == b'#' {
108 108 // empty or comment line, skip
109 109 continue;
110 110 }
111 111 if line.starts_with(b"%include ") {
112 112 let profile = line[b"%include ".len()..].trim();
113 113 if !profile.is_empty() {
114 114 profiles.insert(profile.into());
115 115 }
116 116 } else if line == b"[include]" {
117 117 if in_section && current == Current::Includes {
118 118 return Err(SparseConfigError::IncludesAfterExcludes {
119 119 context,
120 120 });
121 121 }
122 122 in_section = true;
123 123 current = Current::Includes;
124 124 continue;
125 125 } else if line == b"[exclude]" {
126 126 in_section = true;
127 127 current = Current::Excludes;
128 128 } else {
129 129 if current == Current::None {
130 130 return Err(SparseConfigError::EntryOutsideSection {
131 131 context,
132 132 line: line.into(),
133 133 });
134 134 }
135 135 if line.trim().starts_with(b"/") {
136 136 warnings.push(SparseWarning::RootWarning {
137 137 context,
138 138 line: line.into(),
139 139 });
140 140 continue;
141 141 }
142 142 match current {
143 143 Current::Includes => {
144 144 includes.push(b'\n');
145 145 includes.extend(line.iter());
146 146 }
147 147 Current::Excludes => {
148 148 excludes.push(b'\n');
149 149 excludes.extend(line.iter());
150 150 }
151 151 Current::None => unreachable!(),
152 152 }
153 153 }
154 154 }
155 155
156 156 Ok(SparseConfig {
157 157 includes,
158 158 excludes,
159 159 profiles,
160 160 warnings,
161 161 })
162 162 }
163 163
164 164 fn read_temporary_includes(
165 165 repo: &Repo,
166 166 ) -> Result<Vec<Vec<u8>>, SparseConfigError> {
167 167 let raw = repo.hg_vfs().try_read("tempsparse")?.unwrap_or_default();
168 168 if raw.is_empty() {
169 169 return Ok(vec![]);
170 170 }
171 171 Ok(raw.split(|c| *c == b'\n').map(ToOwned::to_owned).collect())
172 172 }
173 173
174 174 /// Obtain sparse checkout patterns for the given revision
175 175 fn patterns_for_rev(
176 176 repo: &Repo,
177 177 rev: Revision,
178 178 ) -> Result<Option<SparseConfig>, SparseConfigError> {
179 179 if !repo.has_sparse() {
180 180 return Ok(None);
181 181 }
182 182 let raw = repo.hg_vfs().try_read("sparse")?.unwrap_or_default();
183 183
184 184 if raw.is_empty() {
185 185 return Ok(None);
186 186 }
187 187
188 188 let mut config = parse_config(&raw, SparseConfigContext::Sparse)?;
189 189
190 190 if !config.profiles.is_empty() {
191 191 let mut profiles: Vec<Vec<u8>> = config.profiles.into_iter().collect();
192 192 let mut visited = HashSet::new();
193 193
194 194 while let Some(profile) = profiles.pop() {
195 195 if visited.contains(&profile) {
196 196 continue;
197 197 }
198 198 visited.insert(profile.to_owned());
199 199
200 200 let output =
201 201 cat(repo, &rev.to_string(), vec![HgPath::new(&profile)])
202 202 .map_err(|_| {
203 203 HgError::corrupted(
204 204 "dirstate points to non-existent parent node"
205 205 .to_string(),
206 206 )
207 207 })?;
208 208 if output.results.is_empty() {
209 209 config.warnings.push(SparseWarning::ProfileNotFound {
210 210 profile: profile.to_owned(),
211 211 rev,
212 212 })
213 213 }
214 214
215 215 let subconfig = parse_config(
216 216 &output.results[0].1,
217 217 SparseConfigContext::Sparse,
218 218 )?;
219 219 if !subconfig.includes.is_empty() {
220 220 config.includes.push(b'\n');
221 221 config.includes.extend(&subconfig.includes);
222 222 }
223 223 if !subconfig.includes.is_empty() {
224 224 config.includes.push(b'\n');
225 225 config.excludes.extend(&subconfig.excludes);
226 226 }
227 227 config.warnings.extend(subconfig.warnings.into_iter());
228 228 profiles.extend(subconfig.profiles.into_iter());
229 229 }
230 230
231 231 config.profiles = visited;
232 232 }
233 233
234 234 if !config.includes.is_empty() {
235 235 config.includes.extend(b"\n.hg*");
236 236 }
237 237
238 238 Ok(Some(config))
239 239 }
240 240
241 241 /// Obtain a matcher for sparse working directories.
242 242 pub fn matcher(
243 243 repo: &Repo,
244 244 ) -> Result<(Box<dyn Matcher + Sync>, Vec<SparseWarning>), SparseConfigError> {
245 245 let mut warnings = vec![];
246 246 if !repo.requirements().contains(SPARSE_REQUIREMENT) {
247 247 return Ok((Box::new(AlwaysMatcher), warnings));
248 248 }
249 249
250 250 let parents = repo.dirstate_parents()?;
251 251 let mut revs = vec![];
252 252 let p1_rev =
253 253 repo.changelog()?
254 254 .rev_from_node(parents.p1.into())
255 255 .map_err(|_| {
256 256 HgError::corrupted(
257 257 "dirstate points to non-existent parent node".to_string(),
258 258 )
259 259 })?;
260 260 if p1_rev != NULL_REVISION {
261 261 revs.push(p1_rev)
262 262 }
263 263 let p2_rev =
264 264 repo.changelog()?
265 265 .rev_from_node(parents.p2.into())
266 266 .map_err(|_| {
267 267 HgError::corrupted(
268 268 "dirstate points to non-existent parent node".to_string(),
269 269 )
270 270 })?;
271 271 if p2_rev != NULL_REVISION {
272 272 revs.push(p2_rev)
273 273 }
274 274 let mut matchers = vec![];
275 275
276 276 for rev in revs.iter() {
277 277 let config = patterns_for_rev(repo, *rev);
278 278 if let Ok(Some(config)) = config {
279 279 warnings.extend(config.warnings);
280 280 let mut m: Box<dyn Matcher + Sync> = Box::new(AlwaysMatcher);
281 281 if !config.includes.is_empty() {
282 282 let (patterns, subwarnings) = parse_pattern_file_contents(
283 283 &config.includes,
284 284 Path::new(""),
285 Some(b"glob:".as_ref()),
285 Some(PatternSyntax::Glob),
286 false,
286 287 false,
287 288 )?;
288 289 warnings.extend(subwarnings.into_iter().map(From::from));
289 290 m = Box::new(IncludeMatcher::new(patterns)?);
290 291 }
291 292 if !config.excludes.is_empty() {
292 293 let (patterns, subwarnings) = parse_pattern_file_contents(
293 294 &config.excludes,
294 295 Path::new(""),
295 Some(b"glob:".as_ref()),
296 Some(PatternSyntax::Glob),
297 false,
296 298 false,
297 299 )?;
298 300 warnings.extend(subwarnings.into_iter().map(From::from));
299 301 m = Box::new(DifferenceMatcher::new(
300 302 m,
301 303 Box::new(IncludeMatcher::new(patterns)?),
302 304 ));
303 305 }
304 306 matchers.push(m);
305 307 }
306 308 }
307 309 let result: Box<dyn Matcher + Sync> = match matchers.len() {
308 310 0 => Box::new(AlwaysMatcher),
309 311 1 => matchers.pop().expect("1 is equal to 0"),
310 312 _ => Box::new(UnionMatcher::new(matchers)),
311 313 };
312 314
313 315 let matcher =
314 316 force_include_matcher(result, &read_temporary_includes(repo)?)?;
315 317 Ok((matcher, warnings))
316 318 }
317 319
318 320 /// Returns a matcher that returns true for any of the forced includes before
319 321 /// testing against the actual matcher
320 322 fn force_include_matcher(
321 323 result: Box<dyn Matcher + Sync>,
322 324 temp_includes: &[Vec<u8>],
323 325 ) -> Result<Box<dyn Matcher + Sync>, PatternError> {
324 326 if temp_includes.is_empty() {
325 327 return Ok(result);
326 328 }
327 329 let forced_include_matcher = IncludeMatcher::new(
328 330 temp_includes
329 331 .iter()
330 332 .map(|include| {
331 333 IgnorePattern::new(PatternSyntax::Path, include, Path::new(""))
332 334 })
333 335 .collect(),
334 336 )?;
335 337 Ok(Box::new(UnionMatcher::new(vec![
336 338 Box::new(forced_include_matcher),
337 339 result,
338 340 ])))
339 341 }
General Comments 0
You need to be logged in to leave comments. Login now