##// END OF EJS Templates
rhg: support "status FILE"...
Spencer Baugh -
r51759:c112cc9e default
parent child Browse files
Show More
@@ -1,833 +1,876 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 #[derive(Debug, Clone, PartialEq, Eq)]
40 40 pub enum PatternSyntax {
41 41 /// A regular expression
42 42 Regexp,
43 43 /// Glob that matches at the front of the path
44 44 RootGlob,
45 45 /// Glob that matches at any suffix of the path (still anchored at
46 46 /// slashes)
47 47 Glob,
48 48 /// a path relative to repository root, which is matched recursively
49 49 Path,
50 50 /// a single exact path relative to repository root
51 51 FilePath,
52 52 /// A path relative to cwd
53 53 RelPath,
54 54 /// an unrooted glob (*.rs matches Rust files in all dirs)
55 55 RelGlob,
56 56 /// A regexp that needn't match the start of a name
57 57 RelRegexp,
58 58 /// A path relative to repository root, which is matched non-recursively
59 59 /// (will not match subdirectories)
60 60 RootFiles,
61 61 /// A file of patterns to read and include
62 62 Include,
63 63 /// A file of patterns to match against files under the same directory
64 64 SubInclude,
65 65 /// SubInclude with the result of parsing the included file
66 66 ///
67 67 /// Note: there is no ExpandedInclude because that expansion can be done
68 68 /// in place by replacing the Include pattern by the included patterns.
69 69 /// SubInclude requires more handling.
70 70 ///
71 71 /// Note: `Box` is used to minimize size impact on other enum variants
72 72 ExpandedSubInclude(Box<SubInclude>),
73 73 }
74 74
75 75 /// Transforms a glob pattern into a regex
76 76 fn glob_to_re(pat: &[u8]) -> Vec<u8> {
77 77 let mut input = pat;
78 78 let mut res: Vec<u8> = vec![];
79 79 let mut group_depth = 0;
80 80
81 81 while let Some((c, rest)) = input.split_first() {
82 82 input = rest;
83 83
84 84 match c {
85 85 b'*' => {
86 86 for (source, repl) in GLOB_REPLACEMENTS {
87 87 if let Some(rest) = input.drop_prefix(source) {
88 88 input = rest;
89 89 res.extend(*repl);
90 90 break;
91 91 }
92 92 }
93 93 }
94 94 b'?' => res.extend(b"."),
95 95 b'[' => {
96 96 match input.iter().skip(1).position(|b| *b == b']') {
97 97 None => res.extend(b"\\["),
98 98 Some(end) => {
99 99 // Account for the one we skipped
100 100 let end = end + 1;
101 101
102 102 res.extend(b"[");
103 103
104 104 for (i, b) in input[..end].iter().enumerate() {
105 105 if *b == b'!' && i == 0 {
106 106 res.extend(b"^")
107 107 } else if *b == b'^' && i == 0 {
108 108 res.extend(b"\\^")
109 109 } else if *b == b'\\' {
110 110 res.extend(b"\\\\")
111 111 } else {
112 112 res.push(*b)
113 113 }
114 114 }
115 115 res.extend(b"]");
116 116 input = &input[end + 1..];
117 117 }
118 118 }
119 119 }
120 120 b'{' => {
121 121 group_depth += 1;
122 122 res.extend(b"(?:")
123 123 }
124 124 b'}' if group_depth > 0 => {
125 125 group_depth -= 1;
126 126 res.extend(b")");
127 127 }
128 128 b',' if group_depth > 0 => res.extend(b"|"),
129 129 b'\\' => {
130 130 let c = {
131 131 if let Some((c, rest)) = input.split_first() {
132 132 input = rest;
133 133 c
134 134 } else {
135 135 c
136 136 }
137 137 };
138 138 res.extend(&RE_ESCAPE[*c as usize])
139 139 }
140 140 _ => res.extend(&RE_ESCAPE[*c as usize]),
141 141 }
142 142 }
143 143 res
144 144 }
145 145
146 146 fn escape_pattern(pattern: &[u8]) -> Vec<u8> {
147 147 pattern
148 148 .iter()
149 149 .flat_map(|c| RE_ESCAPE[*c as usize].clone())
150 150 .collect()
151 151 }
152 152
153 153 pub fn parse_pattern_syntax(
154 154 kind: &[u8],
155 155 ) -> Result<PatternSyntax, PatternError> {
156 156 match kind {
157 157 b"re:" => Ok(PatternSyntax::Regexp),
158 158 b"path:" => Ok(PatternSyntax::Path),
159 159 b"filepath:" => Ok(PatternSyntax::FilePath),
160 160 b"relpath:" => Ok(PatternSyntax::RelPath),
161 161 b"rootfilesin:" => Ok(PatternSyntax::RootFiles),
162 162 b"relglob:" => Ok(PatternSyntax::RelGlob),
163 163 b"relre:" => Ok(PatternSyntax::RelRegexp),
164 164 b"glob:" => Ok(PatternSyntax::Glob),
165 165 b"rootglob:" => Ok(PatternSyntax::RootGlob),
166 166 b"include:" => Ok(PatternSyntax::Include),
167 167 b"subinclude:" => Ok(PatternSyntax::SubInclude),
168 168 _ => Err(PatternError::UnsupportedSyntax(
169 169 String::from_utf8_lossy(kind).to_string(),
170 170 )),
171 171 }
172 172 }
173 173
174 174 lazy_static! {
175 175 static ref FLAG_RE: Regex = Regex::new(r"^\(\?[aiLmsux]+\)").unwrap();
176 176 }
177 177
178 178 /// Builds the regex that corresponds to the given pattern.
179 179 /// If within a `syntax: regexp` context, returns the pattern,
180 180 /// otherwise, returns the corresponding regex.
181 181 fn _build_single_regex(entry: &IgnorePattern, glob_suffix: &[u8]) -> Vec<u8> {
182 182 let IgnorePattern {
183 183 syntax, pattern, ..
184 184 } = entry;
185 185 if pattern.is_empty() {
186 186 return vec![];
187 187 }
188 188 match syntax {
189 189 PatternSyntax::Regexp => pattern.to_owned(),
190 190 PatternSyntax::RelRegexp => {
191 191 // The `regex` crate accepts `**` while `re2` and Python's `re`
192 192 // do not. Checking for `*` correctly triggers the same error all
193 193 // engines.
194 194 if pattern[0] == b'^'
195 195 || pattern[0] == b'*'
196 196 || pattern.starts_with(b".*")
197 197 {
198 198 return pattern.to_owned();
199 199 }
200 200 match FLAG_RE.find(pattern) {
201 201 Some(mat) => {
202 202 let s = mat.start();
203 203 let e = mat.end();
204 204 [
205 205 &b"(?"[..],
206 206 &pattern[s + 2..e - 1],
207 207 &b":"[..],
208 208 if pattern[e] == b'^'
209 209 || pattern[e] == b'*'
210 210 || pattern[e..].starts_with(b".*")
211 211 {
212 212 &b""[..]
213 213 } else {
214 214 &b".*"[..]
215 215 },
216 216 &pattern[e..],
217 217 &b")"[..],
218 218 ]
219 219 .concat()
220 220 }
221 221 None => [&b".*"[..], pattern].concat(),
222 222 }
223 223 }
224 224 PatternSyntax::Path | PatternSyntax::RelPath => {
225 225 if pattern == b"." {
226 226 return vec![];
227 227 }
228 228 [escape_pattern(pattern).as_slice(), b"(?:/|$)"].concat()
229 229 }
230 230 PatternSyntax::RootFiles => {
231 231 let mut res = if pattern == b"." {
232 232 vec![]
233 233 } else {
234 234 // Pattern is a directory name.
235 235 [escape_pattern(pattern).as_slice(), b"/"].concat()
236 236 };
237 237
238 238 // Anything after the pattern must be a non-directory.
239 239 res.extend(b"[^/]+$");
240 240 res
241 241 }
242 242 PatternSyntax::RelGlob => {
243 243 let glob_re = glob_to_re(pattern);
244 244 if let Some(rest) = glob_re.drop_prefix(b"[^/]*") {
245 245 [b".*", rest, glob_suffix].concat()
246 246 } else {
247 247 [b"(?:.*/)?", glob_re.as_slice(), glob_suffix].concat()
248 248 }
249 249 }
250 250 PatternSyntax::Glob | PatternSyntax::RootGlob => {
251 251 [glob_to_re(pattern).as_slice(), glob_suffix].concat()
252 252 }
253 253 PatternSyntax::Include
254 254 | PatternSyntax::SubInclude
255 255 | PatternSyntax::ExpandedSubInclude(_)
256 256 | PatternSyntax::FilePath => unreachable!(),
257 257 }
258 258 }
259 259
260 260 const GLOB_SPECIAL_CHARACTERS: [u8; 7] =
261 261 [b'*', b'?', b'[', b']', b'{', b'}', b'\\'];
262 262
263 263 /// TODO support other platforms
264 264 #[cfg(unix)]
265 265 pub fn normalize_path_bytes(bytes: &[u8]) -> Vec<u8> {
266 266 if bytes.is_empty() {
267 267 return b".".to_vec();
268 268 }
269 269 let sep = b'/';
270 270
271 271 let mut initial_slashes = bytes.iter().take_while(|b| **b == sep).count();
272 272 if initial_slashes > 2 {
273 273 // POSIX allows one or two initial slashes, but treats three or more
274 274 // as single slash.
275 275 initial_slashes = 1;
276 276 }
277 277 let components = bytes
278 278 .split(|b| *b == sep)
279 279 .filter(|c| !(c.is_empty() || c == b"."))
280 280 .fold(vec![], |mut acc, component| {
281 281 if component != b".."
282 282 || (initial_slashes == 0 && acc.is_empty())
283 283 || (!acc.is_empty() && acc[acc.len() - 1] == b"..")
284 284 {
285 285 acc.push(component)
286 286 } else if !acc.is_empty() {
287 287 acc.pop();
288 288 }
289 289 acc
290 290 });
291 291 let mut new_bytes = components.join(&sep);
292 292
293 293 if initial_slashes > 0 {
294 294 let mut buf: Vec<_> = (0..initial_slashes).map(|_| sep).collect();
295 295 buf.extend(new_bytes);
296 296 new_bytes = buf;
297 297 }
298 298 if new_bytes.is_empty() {
299 299 b".".to_vec()
300 300 } else {
301 301 new_bytes
302 302 }
303 303 }
304 304
305 305 /// Wrapper function to `_build_single_regex` that short-circuits 'exact' globs
306 306 /// that don't need to be transformed into a regex.
307 307 pub fn build_single_regex(
308 308 entry: &IgnorePattern,
309 309 glob_suffix: &[u8],
310 310 ) -> Result<Option<Vec<u8>>, PatternError> {
311 311 let IgnorePattern {
312 312 pattern, syntax, ..
313 313 } = entry;
314 314 let pattern = match syntax {
315 315 PatternSyntax::RootGlob
316 316 | PatternSyntax::Path
317 317 | PatternSyntax::RelGlob
318 318 | PatternSyntax::RelPath
319 319 | PatternSyntax::RootFiles => normalize_path_bytes(pattern),
320 320 PatternSyntax::Include | PatternSyntax::SubInclude => {
321 321 return Err(PatternError::NonRegexPattern(entry.clone()))
322 322 }
323 323 _ => pattern.to_owned(),
324 324 };
325 325 let is_simple_rootglob = *syntax == PatternSyntax::RootGlob
326 326 && !pattern.iter().any(|b| GLOB_SPECIAL_CHARACTERS.contains(b));
327 327 if is_simple_rootglob || syntax == &PatternSyntax::FilePath {
328 328 Ok(None)
329 329 } else {
330 330 let mut entry = entry.clone();
331 331 entry.pattern = pattern;
332 332 Ok(Some(_build_single_regex(&entry, glob_suffix)))
333 333 }
334 334 }
335 335
336 336 lazy_static! {
337 337 static ref SYNTAXES: FastHashMap<&'static [u8], PatternSyntax> = {
338 338 let mut m = FastHashMap::default();
339 339
340 340 m.insert(b"re:".as_ref(), PatternSyntax::Regexp);
341 341 m.insert(b"regexp:".as_ref(), PatternSyntax::Regexp);
342 342 m.insert(b"path:".as_ref(), PatternSyntax::Path);
343 343 m.insert(b"filepath:".as_ref(), PatternSyntax::FilePath);
344 344 m.insert(b"relpath:".as_ref(), PatternSyntax::RelPath);
345 345 m.insert(b"rootfilesin:".as_ref(), PatternSyntax::RootFiles);
346 346 m.insert(b"relglob:".as_ref(), PatternSyntax::RelGlob);
347 347 m.insert(b"relre:".as_ref(), PatternSyntax::RelRegexp);
348 348 m.insert(b"glob:".as_ref(), PatternSyntax::Glob);
349 349 m.insert(b"rootglob:".as_ref(), PatternSyntax::RootGlob);
350 350 m.insert(b"include:".as_ref(), PatternSyntax::Include);
351 351 m.insert(b"subinclude:".as_ref(), PatternSyntax::SubInclude);
352 352
353 353 m
354 354 };
355 355 }
356 356
357 357 #[derive(Debug)]
358 358 pub enum PatternFileWarning {
359 359 /// (file path, syntax bytes)
360 360 InvalidSyntax(PathBuf, Vec<u8>),
361 361 /// File path
362 362 NoSuchFile(PathBuf),
363 363 }
364 364
365 365 pub fn parse_one_pattern(
366 366 pattern: &[u8],
367 367 source: &Path,
368 368 default: PatternSyntax,
369 normalize: bool,
369 370 ) -> IgnorePattern {
370 371 let mut pattern_bytes: &[u8] = pattern;
371 372 let mut syntax = default;
372 373
373 374 for (s, val) in SYNTAXES.iter() {
374 375 if let Some(rest) = pattern_bytes.drop_prefix(s) {
375 376 syntax = val.clone();
376 377 pattern_bytes = rest;
377 378 break;
378 379 }
379 380 }
380 381
381 let pattern = pattern_bytes.to_vec();
382 let pattern = match syntax {
383 PatternSyntax::RootGlob
384 | PatternSyntax::Path
385 | PatternSyntax::Glob
386 | PatternSyntax::RelGlob
387 | PatternSyntax::RelPath
388 | PatternSyntax::RootFiles
389 if normalize =>
390 {
391 normalize_path_bytes(pattern_bytes)
392 }
393 _ => pattern_bytes.to_vec(),
394 };
382 395
383 396 IgnorePattern {
384 397 syntax,
385 398 pattern,
386 399 source: source.to_owned(),
387 400 }
388 401 }
389 402
390 403 pub fn parse_pattern_file_contents(
391 404 lines: &[u8],
392 405 file_path: &Path,
393 406 default_syntax_override: Option<PatternSyntax>,
394 407 warn: bool,
395 408 relativize: bool,
396 409 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
397 410 let comment_regex = Regex::new(r"((?:^|[^\\])(?:\\\\)*)#.*").unwrap();
398 411
399 412 #[allow(clippy::trivial_regex)]
400 413 let comment_escape_regex = Regex::new(r"\\#").unwrap();
401 414 let mut inputs: Vec<IgnorePattern> = vec![];
402 415 let mut warnings: Vec<PatternFileWarning> = vec![];
403 416
404 417 let mut current_syntax =
405 418 default_syntax_override.unwrap_or(PatternSyntax::RelRegexp);
406 419
407 420 for mut line in lines.split(|c| *c == b'\n') {
408 421 let line_buf;
409 422 if line.contains(&b'#') {
410 423 if let Some(cap) = comment_regex.captures(line) {
411 424 line = &line[..cap.get(1).unwrap().end()]
412 425 }
413 426 line_buf = comment_escape_regex.replace_all(line, NoExpand(b"#"));
414 427 line = &line_buf;
415 428 }
416 429
417 430 let line = line.trim_end();
418 431
419 432 if line.is_empty() {
420 433 continue;
421 434 }
422 435
423 436 if let Some(syntax) = line.drop_prefix(b"syntax:") {
424 437 let syntax = syntax.trim();
425 438
426 439 if let Some(parsed) =
427 440 SYNTAXES.get([syntax, &b":"[..]].concat().as_slice())
428 441 {
429 442 current_syntax = parsed.clone();
430 443 } else if warn {
431 444 warnings.push(PatternFileWarning::InvalidSyntax(
432 445 file_path.to_owned(),
433 446 syntax.to_owned(),
434 447 ));
435 448 }
436 449 } else {
437 450 let pattern = parse_one_pattern(
438 451 line,
439 452 file_path,
440 453 current_syntax.clone(),
454 false,
441 455 );
442 456 inputs.push(if relativize {
443 457 pattern.to_relative()
444 458 } else {
445 459 pattern
446 460 })
447 461 }
448 462 }
449 463 Ok((inputs, warnings))
450 464 }
451 465
466 pub fn parse_pattern_args(
467 patterns: Vec<Vec<u8>>,
468 cwd: &Path,
469 root: &Path,
470 ) -> Result<Vec<IgnorePattern>, HgPathError> {
471 let mut ignore_patterns: Vec<IgnorePattern> = Vec::new();
472 for pattern in patterns {
473 let pattern = parse_one_pattern(
474 &pattern,
475 Path::new("<args>"),
476 PatternSyntax::RelPath,
477 true,
478 );
479 match pattern.syntax {
480 PatternSyntax::RelGlob | PatternSyntax::RelPath => {
481 let name = get_path_from_bytes(&pattern.pattern);
482 let canon = canonical_path(root, cwd, name)?;
483 ignore_patterns.push(IgnorePattern {
484 syntax: pattern.syntax,
485 pattern: get_bytes_from_path(canon),
486 source: pattern.source,
487 })
488 }
489 _ => ignore_patterns.push(pattern.to_owned()),
490 };
491 }
492 Ok(ignore_patterns)
493 }
494
452 495 pub fn read_pattern_file(
453 496 file_path: &Path,
454 497 warn: bool,
455 498 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
456 499 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
457 500 match std::fs::read(file_path) {
458 501 Ok(contents) => {
459 502 inspect_pattern_bytes(file_path, &contents);
460 503 parse_pattern_file_contents(&contents, file_path, None, warn, true)
461 504 }
462 505 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok((
463 506 vec![],
464 507 vec![PatternFileWarning::NoSuchFile(file_path.to_owned())],
465 508 )),
466 509 Err(e) => Err(e.into()),
467 510 }
468 511 }
469 512
470 513 /// Represents an entry in an "ignore" file.
471 514 #[derive(Debug, Eq, PartialEq, Clone)]
472 515 pub struct IgnorePattern {
473 516 pub syntax: PatternSyntax,
474 517 pub pattern: Vec<u8>,
475 518 pub source: PathBuf,
476 519 }
477 520
478 521 impl IgnorePattern {
479 522 pub fn new(syntax: PatternSyntax, pattern: &[u8], source: &Path) -> Self {
480 523 Self {
481 524 syntax,
482 525 pattern: pattern.to_owned(),
483 526 source: source.to_owned(),
484 527 }
485 528 }
486 529
487 530 pub fn to_relative(self) -> Self {
488 531 let Self {
489 532 syntax,
490 533 pattern,
491 534 source,
492 535 } = self;
493 536 Self {
494 537 syntax: match syntax {
495 538 PatternSyntax::Regexp => PatternSyntax::RelRegexp,
496 539 PatternSyntax::Glob => PatternSyntax::RelGlob,
497 540 x => x,
498 541 },
499 542 pattern,
500 543 source,
501 544 }
502 545 }
503 546 }
504 547
505 548 pub type PatternResult<T> = Result<T, PatternError>;
506 549
507 550 /// Wrapper for `read_pattern_file` that also recursively expands `include:`
508 551 /// and `subinclude:` patterns.
509 552 ///
510 553 /// The former are expanded in place, while `PatternSyntax::ExpandedSubInclude`
511 554 /// is used for the latter to form a tree of patterns.
512 555 pub fn get_patterns_from_file(
513 556 pattern_file: &Path,
514 557 root_dir: &Path,
515 558 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
516 559 ) -> PatternResult<(Vec<IgnorePattern>, Vec<PatternFileWarning>)> {
517 560 let (patterns, mut warnings) =
518 561 read_pattern_file(pattern_file, true, inspect_pattern_bytes)?;
519 562 let patterns = patterns
520 563 .into_iter()
521 564 .flat_map(|entry| -> PatternResult<_> {
522 565 Ok(match &entry.syntax {
523 566 PatternSyntax::Include => {
524 567 let inner_include =
525 568 root_dir.join(get_path_from_bytes(&entry.pattern));
526 569 let (inner_pats, inner_warnings) = get_patterns_from_file(
527 570 &inner_include,
528 571 root_dir,
529 572 inspect_pattern_bytes,
530 573 )?;
531 574 warnings.extend(inner_warnings);
532 575 inner_pats
533 576 }
534 577 PatternSyntax::SubInclude => {
535 578 let mut sub_include = SubInclude::new(
536 579 root_dir,
537 580 &entry.pattern,
538 581 &entry.source,
539 582 )?;
540 583 let (inner_patterns, inner_warnings) =
541 584 get_patterns_from_file(
542 585 &sub_include.path,
543 586 &sub_include.root,
544 587 inspect_pattern_bytes,
545 588 )?;
546 589 sub_include.included_patterns = inner_patterns;
547 590 warnings.extend(inner_warnings);
548 591 vec![IgnorePattern {
549 592 syntax: PatternSyntax::ExpandedSubInclude(Box::new(
550 593 sub_include,
551 594 )),
552 595 ..entry
553 596 }]
554 597 }
555 598 _ => vec![entry],
556 599 })
557 600 })
558 601 .flatten()
559 602 .collect();
560 603
561 604 Ok((patterns, warnings))
562 605 }
563 606
564 607 /// Holds all the information needed to handle a `subinclude:` pattern.
565 608 #[derive(Debug, PartialEq, Eq, Clone)]
566 609 pub struct SubInclude {
567 610 /// Will be used for repository (hg) paths that start with this prefix.
568 611 /// It is relative to the current working directory, so comparing against
569 612 /// repository paths is painless.
570 613 pub prefix: HgPathBuf,
571 614 /// The file itself, containing the patterns
572 615 pub path: PathBuf,
573 616 /// Folder in the filesystem where this it applies
574 617 pub root: PathBuf,
575 618
576 619 pub included_patterns: Vec<IgnorePattern>,
577 620 }
578 621
579 622 impl SubInclude {
580 623 pub fn new(
581 624 root_dir: &Path,
582 625 pattern: &[u8],
583 626 source: &Path,
584 627 ) -> Result<SubInclude, HgPathError> {
585 628 let normalized_source =
586 629 normalize_path_bytes(&get_bytes_from_path(source));
587 630
588 631 let source_root = get_path_from_bytes(&normalized_source);
589 632 let source_root =
590 633 source_root.parent().unwrap_or_else(|| source_root.deref());
591 634
592 635 let path = source_root.join(get_path_from_bytes(pattern));
593 636 let new_root = path.parent().unwrap_or_else(|| path.deref());
594 637
595 638 let prefix = canonical_path(root_dir, root_dir, new_root)?;
596 639
597 640 Ok(Self {
598 641 prefix: path_to_hg_path_buf(prefix).map(|mut p| {
599 642 if !p.is_empty() {
600 643 p.push_byte(b'/');
601 644 }
602 645 p
603 646 })?,
604 647 path: path.to_owned(),
605 648 root: new_root.to_owned(),
606 649 included_patterns: Vec::new(),
607 650 })
608 651 }
609 652 }
610 653
611 654 /// Separate and pre-process subincludes from other patterns for the "ignore"
612 655 /// phase.
613 656 pub fn filter_subincludes(
614 657 ignore_patterns: Vec<IgnorePattern>,
615 658 ) -> Result<(Vec<SubInclude>, Vec<IgnorePattern>), HgPathError> {
616 659 let mut subincludes = vec![];
617 660 let mut others = vec![];
618 661
619 662 for pattern in ignore_patterns {
620 663 if let PatternSyntax::ExpandedSubInclude(sub_include) = pattern.syntax
621 664 {
622 665 subincludes.push(*sub_include);
623 666 } else {
624 667 others.push(pattern)
625 668 }
626 669 }
627 670 Ok((subincludes, others))
628 671 }
629 672
630 673 #[cfg(test)]
631 674 mod tests {
632 675 use super::*;
633 676 use pretty_assertions::assert_eq;
634 677
635 678 #[test]
636 679 fn escape_pattern_test() {
637 680 let untouched =
638 681 br#"!"%',/0123456789:;<=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz"#;
639 682 assert_eq!(escape_pattern(untouched), untouched.to_vec());
640 683 // All escape codes
641 684 assert_eq!(
642 685 escape_pattern(br#"()[]{}?*+-|^$\\.&~#\t\n\r\v\f"#),
643 686 br#"\(\)\[\]\{\}\?\*\+\-\|\^\$\\\\\.\&\~\#\\t\\n\\r\\v\\f"#
644 687 .to_vec()
645 688 );
646 689 }
647 690
648 691 #[test]
649 692 fn glob_test() {
650 693 assert_eq!(glob_to_re(br#"?"#), br#"."#);
651 694 assert_eq!(glob_to_re(br#"*"#), br#"[^/]*"#);
652 695 assert_eq!(glob_to_re(br#"**"#), br#".*"#);
653 696 assert_eq!(glob_to_re(br#"**/a"#), br#"(?:.*/)?a"#);
654 697 assert_eq!(glob_to_re(br#"a/**/b"#), br#"a/(?:.*/)?b"#);
655 698 assert_eq!(glob_to_re(br#"[a*?!^][^b][!c]"#), br#"[a*?!^][\^b][^c]"#);
656 699 assert_eq!(glob_to_re(br#"{a,b}"#), br#"(?:a|b)"#);
657 700 assert_eq!(glob_to_re(br#".\*\?"#), br#"\.\*\?"#);
658 701 }
659 702
660 703 #[test]
661 704 fn test_parse_pattern_file_contents() {
662 705 let lines = b"syntax: glob\n*.elc";
663 706
664 707 assert_eq!(
665 708 parse_pattern_file_contents(
666 709 lines,
667 710 Path::new("file_path"),
668 711 None,
669 712 false,
670 713 true,
671 714 )
672 715 .unwrap()
673 716 .0,
674 717 vec![IgnorePattern::new(
675 718 PatternSyntax::RelGlob,
676 719 b"*.elc",
677 720 Path::new("file_path")
678 721 )],
679 722 );
680 723
681 724 let lines = b"syntax: include\nsyntax: glob";
682 725
683 726 assert_eq!(
684 727 parse_pattern_file_contents(
685 728 lines,
686 729 Path::new("file_path"),
687 730 None,
688 731 false,
689 732 true,
690 733 )
691 734 .unwrap()
692 735 .0,
693 736 vec![]
694 737 );
695 738 let lines = b"glob:**.o";
696 739 assert_eq!(
697 740 parse_pattern_file_contents(
698 741 lines,
699 742 Path::new("file_path"),
700 743 None,
701 744 false,
702 745 true,
703 746 )
704 747 .unwrap()
705 748 .0,
706 749 vec![IgnorePattern::new(
707 750 PatternSyntax::RelGlob,
708 751 b"**.o",
709 752 Path::new("file_path")
710 753 )]
711 754 );
712 755 }
713 756
714 757 #[test]
715 758 fn test_build_single_regex() {
716 759 assert_eq!(
717 760 build_single_regex(
718 761 &IgnorePattern::new(
719 762 PatternSyntax::RelGlob,
720 763 b"rust/target/",
721 764 Path::new("")
722 765 ),
723 766 b"(?:/|$)"
724 767 )
725 768 .unwrap(),
726 769 Some(br"(?:.*/)?rust/target(?:/|$)".to_vec()),
727 770 );
728 771 assert_eq!(
729 772 build_single_regex(
730 773 &IgnorePattern::new(
731 774 PatternSyntax::Regexp,
732 775 br"rust/target/\d+",
733 776 Path::new("")
734 777 ),
735 778 b"(?:/|$)"
736 779 )
737 780 .unwrap(),
738 781 Some(br"rust/target/\d+".to_vec()),
739 782 );
740 783 }
741 784
742 785 #[test]
743 786 fn test_build_single_regex_shortcut() {
744 787 assert_eq!(
745 788 build_single_regex(
746 789 &IgnorePattern::new(
747 790 PatternSyntax::RootGlob,
748 791 b"",
749 792 Path::new("")
750 793 ),
751 794 b"(?:/|$)"
752 795 )
753 796 .unwrap(),
754 797 None,
755 798 );
756 799 assert_eq!(
757 800 build_single_regex(
758 801 &IgnorePattern::new(
759 802 PatternSyntax::RootGlob,
760 803 b"whatever",
761 804 Path::new("")
762 805 ),
763 806 b"(?:/|$)"
764 807 )
765 808 .unwrap(),
766 809 None,
767 810 );
768 811 assert_eq!(
769 812 build_single_regex(
770 813 &IgnorePattern::new(
771 814 PatternSyntax::RootGlob,
772 815 b"*.o",
773 816 Path::new("")
774 817 ),
775 818 b"(?:/|$)"
776 819 )
777 820 .unwrap(),
778 821 Some(br"[^/]*\.o(?:/|$)".to_vec()),
779 822 );
780 823 }
781 824
782 825 #[test]
783 826 fn test_build_single_relregex() {
784 827 assert_eq!(
785 828 build_single_regex(
786 829 &IgnorePattern::new(
787 830 PatternSyntax::RelRegexp,
788 831 b"^ba{2}r",
789 832 Path::new("")
790 833 ),
791 834 b"(?:/|$)"
792 835 )
793 836 .unwrap(),
794 837 Some(b"^ba{2}r".to_vec()),
795 838 );
796 839 assert_eq!(
797 840 build_single_regex(
798 841 &IgnorePattern::new(
799 842 PatternSyntax::RelRegexp,
800 843 b"ba{2}r",
801 844 Path::new("")
802 845 ),
803 846 b"(?:/|$)"
804 847 )
805 848 .unwrap(),
806 849 Some(b".*ba{2}r".to_vec()),
807 850 );
808 851 assert_eq!(
809 852 build_single_regex(
810 853 &IgnorePattern::new(
811 854 PatternSyntax::RelRegexp,
812 855 b"(?ia)ba{2}r",
813 856 Path::new("")
814 857 ),
815 858 b"(?:/|$)"
816 859 )
817 860 .unwrap(),
818 861 Some(b"(?ia:.*ba{2}r)".to_vec()),
819 862 );
820 863 assert_eq!(
821 864 build_single_regex(
822 865 &IgnorePattern::new(
823 866 PatternSyntax::RelRegexp,
824 867 b"(?ia)^ba{2}r",
825 868 Path::new("")
826 869 ),
827 870 b"(?:/|$)"
828 871 )
829 872 .unwrap(),
830 873 Some(b"(?ia:^ba{2}r)".to_vec()),
831 874 );
832 875 }
833 876 }
@@ -1,144 +1,144 b''
1 1 // Copyright 2018-2020 Georges Racinet <georges.racinet@octobus.net>
2 2 // and Mercurial contributors
3 3 //
4 4 // This software may be used and distributed according to the terms of the
5 5 // GNU General Public License version 2 or any later version.
6 6
7 7 mod ancestors;
8 8 pub mod dagops;
9 9 pub mod errors;
10 10 pub mod narrow;
11 11 pub mod sparse;
12 12 pub use ancestors::{AncestorsIterator, MissingAncestors};
13 13 pub mod dirstate;
14 14 pub mod dirstate_tree;
15 15 pub mod discovery;
16 16 pub mod exit_codes;
17 17 pub mod requirements;
18 18 pub mod testing; // unconditionally built, for use from integration tests
19 19 pub use dirstate::{
20 20 dirs_multiset::{DirsMultiset, DirsMultisetIter},
21 21 status::{
22 22 BadMatch, BadType, DirstateStatus, HgPathCow, StatusError,
23 23 StatusOptions,
24 24 },
25 25 DirstateEntry, DirstateParents, EntryState,
26 26 };
27 27 pub mod copy_tracing;
28 mod filepatterns;
28 pub mod filepatterns;
29 29 pub mod matchers;
30 30 pub mod repo;
31 31 pub mod revlog;
32 32 pub use revlog::*;
33 33 pub mod checkexec;
34 34 pub mod config;
35 35 pub mod lock;
36 36 pub mod logging;
37 37 pub mod operations;
38 38 pub mod revset;
39 39 pub mod utils;
40 40 pub mod vfs;
41 41
42 42 use crate::utils::hg_path::{HgPathBuf, HgPathError};
43 43 pub use filepatterns::{
44 44 parse_pattern_syntax, read_pattern_file, IgnorePattern,
45 45 PatternFileWarning, PatternSyntax,
46 46 };
47 47 use std::collections::HashMap;
48 48 use std::fmt;
49 49 use twox_hash::RandomXxHashBuilder64;
50 50
51 51 pub type LineNumber = usize;
52 52
53 53 /// Rust's default hasher is too slow because it tries to prevent collision
54 54 /// attacks. We are not concerned about those: if an ill-minded person has
55 55 /// write access to your repository, you have other issues.
56 56 pub type FastHashMap<K, V> = HashMap<K, V, RandomXxHashBuilder64>;
57 57
58 58 // TODO: should this be the default `FastHashMap` for all of hg-core, not just
59 59 // dirstate_tree? How does XxHash compare with AHash, hashbrown’s default?
60 60 pub type FastHashbrownMap<K, V> =
61 61 hashbrown::HashMap<K, V, RandomXxHashBuilder64>;
62 62
63 63 #[derive(Debug, PartialEq)]
64 64 pub enum DirstateMapError {
65 65 PathNotFound(HgPathBuf),
66 66 InvalidPath(HgPathError),
67 67 }
68 68
69 69 impl From<HgPathError> for DirstateMapError {
70 70 fn from(error: HgPathError) -> Self {
71 71 Self::InvalidPath(error)
72 72 }
73 73 }
74 74
75 75 impl fmt::Display for DirstateMapError {
76 76 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
77 77 match self {
78 78 DirstateMapError::PathNotFound(_) => {
79 79 f.write_str("expected a value, found none")
80 80 }
81 81 DirstateMapError::InvalidPath(path_error) => path_error.fmt(f),
82 82 }
83 83 }
84 84 }
85 85
86 86 #[derive(Debug, derive_more::From)]
87 87 pub enum DirstateError {
88 88 Map(DirstateMapError),
89 89 Common(errors::HgError),
90 90 }
91 91
92 92 impl From<HgPathError> for DirstateError {
93 93 fn from(error: HgPathError) -> Self {
94 94 Self::Map(DirstateMapError::InvalidPath(error))
95 95 }
96 96 }
97 97
98 98 impl fmt::Display for DirstateError {
99 99 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
100 100 match self {
101 101 DirstateError::Map(error) => error.fmt(f),
102 102 DirstateError::Common(error) => error.fmt(f),
103 103 }
104 104 }
105 105 }
106 106
107 107 #[derive(Debug, derive_more::From)]
108 108 pub enum PatternError {
109 109 #[from]
110 110 Path(HgPathError),
111 111 UnsupportedSyntax(String),
112 112 UnsupportedSyntaxInFile(String, String, usize),
113 113 TooLong(usize),
114 114 #[from]
115 115 IO(std::io::Error),
116 116 /// Needed a pattern that can be turned into a regex but got one that
117 117 /// can't. This should only happen through programmer error.
118 118 NonRegexPattern(IgnorePattern),
119 119 }
120 120
121 121 impl fmt::Display for PatternError {
122 122 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
123 123 match self {
124 124 PatternError::UnsupportedSyntax(syntax) => {
125 125 write!(f, "Unsupported syntax {}", syntax)
126 126 }
127 127 PatternError::UnsupportedSyntaxInFile(syntax, file_path, line) => {
128 128 write!(
129 129 f,
130 130 "{}:{}: unsupported syntax {}",
131 131 file_path, line, syntax
132 132 )
133 133 }
134 134 PatternError::TooLong(size) => {
135 135 write!(f, "matcher pattern is too long ({} bytes)", size)
136 136 }
137 137 PatternError::IO(error) => error.fmt(f),
138 138 PatternError::Path(error) => error.fmt(f),
139 139 PatternError::NonRegexPattern(pattern) => {
140 140 write!(f, "'{:?}' cannot be turned into a regex", pattern)
141 141 }
142 142 }
143 143 }
144 144 }
@@ -1,689 +1,718 b''
1 1 // status.rs
2 2 //
3 3 // Copyright 2020, Georges Racinet <georges.racinets@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 use crate::error::CommandError;
9 9 use crate::ui::{
10 10 format_pattern_file_warning, print_narrow_sparse_warnings, relative_paths,
11 11 RelativePaths, Ui,
12 12 };
13 13 use crate::utils::path_utils::RelativizePaths;
14 14 use clap::Arg;
15 15 use format_bytes::format_bytes;
16 16 use hg::config::Config;
17 17 use hg::dirstate::has_exec_bit;
18 18 use hg::dirstate::status::StatusPath;
19 19 use hg::dirstate::TruncatedTimestamp;
20 20 use hg::errors::{HgError, IoResultExt};
21 use hg::filepatterns::parse_pattern_args;
21 22 use hg::lock::LockError;
22 23 use hg::manifest::Manifest;
23 24 use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
24 25 use hg::repo::Repo;
25 26 use hg::utils::debug::debug_wait_for_file;
26 use hg::utils::files::get_bytes_from_os_string;
27 use hg::utils::files::get_path_from_bytes;
27 use hg::utils::files::{
28 get_bytes_from_os_str, get_bytes_from_os_string, get_path_from_bytes,
29 };
28 30 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
29 31 use hg::DirstateStatus;
30 32 use hg::PatternFileWarning;
31 33 use hg::StatusError;
32 34 use hg::StatusOptions;
33 35 use hg::{self, narrow, sparse};
34 36 use log::info;
35 37 use rayon::prelude::*;
36 38 use std::io;
37 39 use std::path::PathBuf;
38 40
39 41 pub const HELP_TEXT: &str = "
40 42 Show changed files in the working directory
41 43
42 44 This is a pure Rust version of `hg status`.
43 45
44 46 Some options might be missing, check the list below.
45 47 ";
46 48
47 49 pub fn args() -> clap::Command {
48 50 clap::command!("status")
49 51 .alias("st")
50 52 .about(HELP_TEXT)
51 53 .arg(
54 Arg::new("file")
55 .value_parser(clap::value_parser!(std::ffi::OsString))
56 .help("show only these files")
57 .action(clap::ArgAction::Append),
58 )
59 .arg(
52 60 Arg::new("all")
53 61 .help("show status of all files")
54 62 .short('A')
55 63 .action(clap::ArgAction::SetTrue)
56 64 .long("all"),
57 65 )
58 66 .arg(
59 67 Arg::new("modified")
60 68 .help("show only modified files")
61 69 .short('m')
62 70 .action(clap::ArgAction::SetTrue)
63 71 .long("modified"),
64 72 )
65 73 .arg(
66 74 Arg::new("added")
67 75 .help("show only added files")
68 76 .short('a')
69 77 .action(clap::ArgAction::SetTrue)
70 78 .long("added"),
71 79 )
72 80 .arg(
73 81 Arg::new("removed")
74 82 .help("show only removed files")
75 83 .short('r')
76 84 .action(clap::ArgAction::SetTrue)
77 85 .long("removed"),
78 86 )
79 87 .arg(
80 88 Arg::new("clean")
81 89 .help("show only clean files")
82 90 .short('c')
83 91 .action(clap::ArgAction::SetTrue)
84 92 .long("clean"),
85 93 )
86 94 .arg(
87 95 Arg::new("deleted")
88 96 .help("show only deleted files")
89 97 .short('d')
90 98 .action(clap::ArgAction::SetTrue)
91 99 .long("deleted"),
92 100 )
93 101 .arg(
94 102 Arg::new("unknown")
95 103 .help("show only unknown (not tracked) files")
96 104 .short('u')
97 105 .action(clap::ArgAction::SetTrue)
98 106 .long("unknown"),
99 107 )
100 108 .arg(
101 109 Arg::new("ignored")
102 110 .help("show only ignored files")
103 111 .short('i')
104 112 .action(clap::ArgAction::SetTrue)
105 113 .long("ignored"),
106 114 )
107 115 .arg(
108 116 Arg::new("copies")
109 117 .help("show source of copied files (DEFAULT: ui.statuscopies)")
110 118 .short('C')
111 119 .action(clap::ArgAction::SetTrue)
112 120 .long("copies"),
113 121 )
114 122 .arg(
115 123 Arg::new("print0")
116 124 .help("end filenames with NUL, for use with xargs")
117 125 .short('0')
118 126 .action(clap::ArgAction::SetTrue)
119 127 .long("print0"),
120 128 )
121 129 .arg(
122 130 Arg::new("no-status")
123 131 .help("hide status prefix")
124 132 .short('n')
125 133 .action(clap::ArgAction::SetTrue)
126 134 .long("no-status"),
127 135 )
128 136 .arg(
129 137 Arg::new("verbose")
130 138 .help("enable additional output")
131 139 .short('v')
132 140 .action(clap::ArgAction::SetTrue)
133 141 .long("verbose"),
134 142 )
135 143 }
136 144
137 145 /// Pure data type allowing the caller to specify file states to display
138 146 #[derive(Copy, Clone, Debug)]
139 147 pub struct DisplayStates {
140 148 pub modified: bool,
141 149 pub added: bool,
142 150 pub removed: bool,
143 151 pub clean: bool,
144 152 pub deleted: bool,
145 153 pub unknown: bool,
146 154 pub ignored: bool,
147 155 }
148 156
149 157 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
150 158 modified: true,
151 159 added: true,
152 160 removed: true,
153 161 clean: false,
154 162 deleted: true,
155 163 unknown: true,
156 164 ignored: false,
157 165 };
158 166
159 167 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
160 168 modified: true,
161 169 added: true,
162 170 removed: true,
163 171 clean: true,
164 172 deleted: true,
165 173 unknown: true,
166 174 ignored: true,
167 175 };
168 176
169 177 impl DisplayStates {
170 178 pub fn is_empty(&self) -> bool {
171 179 !(self.modified
172 180 || self.added
173 181 || self.removed
174 182 || self.clean
175 183 || self.deleted
176 184 || self.unknown
177 185 || self.ignored)
178 186 }
179 187 }
180 188
181 189 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
182 190 Ok(repo.dirstate_parents()?.is_merge())
183 191 }
184 192
185 193 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
186 194 // These are all the known values for the [fname] argument of
187 195 // [addunfinished] function in [state.py]
188 196 let known_state_files: &[&str] = &[
189 197 "bisect.state",
190 198 "graftstate",
191 199 "histedit-state",
192 200 "rebasestate",
193 201 "shelvedstate",
194 202 "transplant/journal",
195 203 "updatestate",
196 204 ];
197 205 if has_unfinished_merge(repo)? {
198 206 return Ok(true);
199 207 };
200 208 for f in known_state_files {
201 209 if repo.hg_vfs().join(f).exists() {
202 210 return Ok(true);
203 211 }
204 212 }
205 213 Ok(false)
206 214 }
207 215
208 216 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
209 217 // TODO: lift these limitations
210 218 if invocation
211 219 .config
212 220 .get(b"commands", b"status.terse")
213 221 .is_some()
214 222 {
215 223 return Err(CommandError::unsupported(
216 224 "status.terse is not yet supported with rhg status",
217 225 ));
218 226 }
219 227
220 228 let ui = invocation.ui;
221 229 let config = invocation.config;
222 230 let args = invocation.subcommand_args;
223 231
224 232 let print0 = args.get_flag("print0");
225 233 let verbose = args.get_flag("verbose")
226 234 || config.get_bool(b"ui", b"verbose")?
227 235 || config.get_bool(b"commands", b"status.verbose")?;
228 236 let verbose = verbose && !print0;
229 237
230 238 let all = args.get_flag("all");
231 239 let display_states = if all {
232 240 // TODO when implementing `--quiet`: it excludes clean files
233 241 // from `--all`
234 242 ALL_DISPLAY_STATES
235 243 } else {
236 244 let requested = DisplayStates {
237 245 modified: args.get_flag("modified"),
238 246 added: args.get_flag("added"),
239 247 removed: args.get_flag("removed"),
240 248 clean: args.get_flag("clean"),
241 249 deleted: args.get_flag("deleted"),
242 250 unknown: args.get_flag("unknown"),
243 251 ignored: args.get_flag("ignored"),
244 252 };
245 253 if requested.is_empty() {
246 254 DEFAULT_DISPLAY_STATES
247 255 } else {
248 256 requested
249 257 }
250 258 };
251 259 let no_status = args.get_flag("no-status");
252 260 let list_copies = all
253 261 || args.get_flag("copies")
254 262 || config.get_bool(b"ui", b"statuscopies")?;
255 263
256 264 let repo = invocation.repo?;
257 265
258 266 if verbose && has_unfinished_state(repo)? {
259 267 return Err(CommandError::unsupported(
260 268 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
261 269 ));
262 270 }
263 271
264 272 let mut dmap = repo.dirstate_map_mut()?;
265 273
266 274 let check_exec = hg::checkexec::check_exec(repo.working_directory_path());
267 275
268 276 let options = StatusOptions {
269 277 check_exec,
270 278 list_clean: display_states.clean,
271 279 list_unknown: display_states.unknown,
272 280 list_ignored: display_states.ignored,
273 281 list_copies,
274 282 collect_traversed_dirs: false,
275 283 };
276 284
277 285 type StatusResult<'a> =
278 286 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
279 287
280 288 let after_status = |res: StatusResult| -> Result<_, CommandError> {
281 289 let (mut ds_status, pattern_warnings) = res?;
282 290 for warning in pattern_warnings {
283 291 ui.write_stderr(&format_pattern_file_warning(&warning, repo))?;
284 292 }
285 293
286 294 for (path, error) in ds_status.bad {
287 295 let error = match error {
288 296 hg::BadMatch::OsError(code) => {
289 297 std::io::Error::from_raw_os_error(code).to_string()
290 298 }
291 299 hg::BadMatch::BadType(ty) => {
292 300 format!("unsupported file type (type is {})", ty)
293 301 }
294 302 };
295 303 ui.write_stderr(&format_bytes!(
296 304 b"{}: {}\n",
297 305 path.as_bytes(),
298 306 error.as_bytes()
299 307 ))?
300 308 }
301 309 if !ds_status.unsure.is_empty() {
302 310 info!(
303 311 "Files to be rechecked by retrieval from filelog: {:?}",
304 312 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
305 313 );
306 314 }
307 315 let mut fixup = Vec::new();
308 316 if !ds_status.unsure.is_empty()
309 317 && (display_states.modified || display_states.clean)
310 318 {
311 319 let p1 = repo.dirstate_parents()?.p1;
312 320 let manifest = repo.manifest_for_node(p1).map_err(|e| {
313 321 CommandError::from((e, &*format!("{:x}", p1.short())))
314 322 })?;
315 323 let working_directory_vfs = repo.working_directory_vfs();
316 324 let store_vfs = repo.store_vfs();
317 325 let res: Vec<_> = ds_status
318 326 .unsure
319 327 .into_par_iter()
320 328 .map(|to_check| {
321 329 // The compiler seems to get a bit confused with complex
322 330 // inference when using a parallel iterator + map
323 331 // + map_err + collect, so let's just inline some of the
324 332 // logic.
325 333 match unsure_is_modified(
326 334 working_directory_vfs,
327 335 store_vfs,
328 336 check_exec,
329 337 &manifest,
330 338 &to_check.path,
331 339 ) {
332 340 Err(HgError::IoError { .. }) => {
333 341 // IO errors most likely stem from the file being
334 342 // deleted even though we know it's in the
335 343 // dirstate.
336 344 Ok((to_check, UnsureOutcome::Deleted))
337 345 }
338 346 Ok(outcome) => Ok((to_check, outcome)),
339 347 Err(e) => Err(e),
340 348 }
341 349 })
342 350 .collect::<Result<_, _>>()?;
343 351 for (status_path, outcome) in res.into_iter() {
344 352 match outcome {
345 353 UnsureOutcome::Clean => {
346 354 if display_states.clean {
347 355 ds_status.clean.push(status_path.clone());
348 356 }
349 357 fixup.push(status_path.path.into_owned())
350 358 }
351 359 UnsureOutcome::Modified => {
352 360 if display_states.modified {
353 361 ds_status.modified.push(status_path);
354 362 }
355 363 }
356 364 UnsureOutcome::Deleted => {
357 365 if display_states.deleted {
358 366 ds_status.deleted.push(status_path);
359 367 }
360 368 }
361 369 }
362 370 }
363 371 }
364 372
365 373 let relative_status = config
366 374 .get_option(b"commands", b"status.relative")?
367 375 .expect("commands.status.relative should have a default value");
368 376
369 377 let relativize_paths = relative_status || {
370 // TODO should be dependent on whether patterns are passed once
371 // we support those.
372 378 // See in Python code with `getuipathfn` usage in `commands.py`.
373 let legacy_relative_behavior = false;
379 let legacy_relative_behavior = args.contains_id("file");
374 380 match relative_paths(invocation.config)? {
375 381 RelativePaths::Legacy => legacy_relative_behavior,
376 382 RelativePaths::Bool(v) => v,
377 383 }
378 384 };
379 385
380 386 let output = DisplayStatusPaths {
381 387 ui,
382 388 no_status,
383 389 relativize: if relativize_paths {
384 390 Some(RelativizePaths::new(repo)?)
385 391 } else {
386 392 None
387 393 },
388 394 print0,
389 395 };
390 396 if display_states.modified {
391 397 output.display(b"M ", "status.modified", ds_status.modified)?;
392 398 }
393 399 if display_states.added {
394 400 output.display(b"A ", "status.added", ds_status.added)?;
395 401 }
396 402 if display_states.removed {
397 403 output.display(b"R ", "status.removed", ds_status.removed)?;
398 404 }
399 405 if display_states.deleted {
400 406 output.display(b"! ", "status.deleted", ds_status.deleted)?;
401 407 }
402 408 if display_states.unknown {
403 409 output.display(b"? ", "status.unknown", ds_status.unknown)?;
404 410 }
405 411 if display_states.ignored {
406 412 output.display(b"I ", "status.ignored", ds_status.ignored)?;
407 413 }
408 414 if display_states.clean {
409 415 output.display(b"C ", "status.clean", ds_status.clean)?;
410 416 }
411 417
412 418 let dirstate_write_needed = ds_status.dirty;
413 419 let filesystem_time_at_status_start =
414 420 ds_status.filesystem_time_at_status_start;
415 421
416 422 Ok((
417 423 fixup,
418 424 dirstate_write_needed,
419 425 filesystem_time_at_status_start,
420 426 ))
421 427 };
422 428 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
423 429 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
424 430 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
425 431 (true, true) => {
426 432 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
427 433 }
428 434 (true, false) => narrow_matcher,
429 435 (false, true) => sparse_matcher,
430 436 (false, false) => Box::new(AlwaysMatcher),
431 437 };
438 let matcher = match args.get_many::<std::ffi::OsString>("file") {
439 None => matcher,
440 Some(files) => {
441 let patterns: Vec<Vec<u8>> = files
442 .filter(|s| !s.is_empty())
443 .map(get_bytes_from_os_str)
444 .collect();
445 for file in &patterns {
446 if file.starts_with(b"set:") {
447 return Err(CommandError::unsupported("fileset"));
448 }
449 }
450 let cwd = hg::utils::current_dir()?;
451 let root = repo.working_directory_path();
452 let ignore_patterns = parse_pattern_args(patterns, &cwd, root)?;
453 let files_matcher =
454 hg::matchers::PatternMatcher::new(ignore_patterns)?;
455 Box::new(IntersectionMatcher::new(
456 Box::new(files_matcher),
457 matcher,
458 ))
459 }
460 };
432 461
433 462 print_narrow_sparse_warnings(
434 463 &narrow_warnings,
435 464 &sparse_warnings,
436 465 ui,
437 466 repo,
438 467 )?;
439 468 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
440 469 dmap.with_status(
441 470 matcher.as_ref(),
442 471 repo.working_directory_path().to_owned(),
443 472 ignore_files(repo, config),
444 473 options,
445 474 after_status,
446 475 )?;
447 476
448 477 // Development config option to test write races
449 478 if let Err(e) =
450 479 debug_wait_for_file(config, "status.pre-dirstate-write-file")
451 480 {
452 481 ui.write_stderr(e.as_bytes()).ok();
453 482 }
454 483
455 484 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
456 485 && !dirstate_write_needed
457 486 {
458 487 // Nothing to update
459 488 return Ok(());
460 489 }
461 490
462 491 // Update the dirstate on disk if we can
463 492 let with_lock_result =
464 493 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
465 494 if let Some(mtime_boundary) = filesystem_time_at_status_start {
466 495 for hg_path in fixup {
467 496 use std::os::unix::fs::MetadataExt;
468 497 let fs_path = hg_path_to_path_buf(&hg_path)
469 498 .expect("HgPath conversion");
470 499 // Specifically do not reuse `fs_metadata` from
471 500 // `unsure_is_clean` which was needed before reading
472 501 // contents. Here we access metadata again after reading
473 502 // content, in case it changed in the meantime.
474 503 let metadata_res = repo
475 504 .working_directory_vfs()
476 505 .symlink_metadata(&fs_path);
477 506 let fs_metadata = match metadata_res {
478 507 Ok(meta) => meta,
479 508 Err(err) => match err {
480 509 HgError::IoError { .. } => {
481 510 // The file has probably been deleted. In any
482 511 // case, it was in the dirstate before, so
483 512 // let's ignore the error.
484 513 continue;
485 514 }
486 515 _ => return Err(err.into()),
487 516 },
488 517 };
489 518 if let Some(mtime) =
490 519 TruncatedTimestamp::for_reliable_mtime_of(
491 520 &fs_metadata,
492 521 &mtime_boundary,
493 522 )
494 523 .when_reading_file(&fs_path)?
495 524 {
496 525 let mode = fs_metadata.mode();
497 526 let size = fs_metadata.len();
498 527 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
499 528 dirstate_write_needed = true
500 529 }
501 530 }
502 531 }
503 532 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
504 533 if dirstate_write_needed {
505 534 repo.write_dirstate()?
506 535 }
507 536 Ok(())
508 537 });
509 538 match with_lock_result {
510 539 Ok(closure_result) => closure_result?,
511 540 Err(LockError::AlreadyHeld) => {
512 541 // Not updating the dirstate is not ideal but not critical:
513 542 // don’t keep our caller waiting until some other Mercurial
514 543 // process releases the lock.
515 544 log::info!("not writing dirstate from `status`: lock is held")
516 545 }
517 546 Err(LockError::Other(HgError::IoError { error, .. }))
518 547 if error.kind() == io::ErrorKind::PermissionDenied =>
519 548 {
520 549 // `hg status` on a read-only repository is fine
521 550 }
522 551 Err(LockError::Other(error)) => {
523 552 // Report other I/O errors
524 553 Err(error)?
525 554 }
526 555 }
527 556 Ok(())
528 557 }
529 558
530 559 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
531 560 let mut ignore_files = Vec::new();
532 561 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
533 562 if repo_ignore.exists() {
534 563 ignore_files.push(repo_ignore)
535 564 }
536 565 for (key, value) in config.iter_section(b"ui") {
537 566 if key == b"ignore" || key.starts_with(b"ignore.") {
538 567 let path = get_path_from_bytes(value);
539 568 // TODO: expand "~/" and environment variable here, like Python
540 569 // does with `os.path.expanduser` and `os.path.expandvars`
541 570
542 571 let joined = repo.working_directory_path().join(path);
543 572 ignore_files.push(joined);
544 573 }
545 574 }
546 575 ignore_files
547 576 }
548 577
549 578 struct DisplayStatusPaths<'a> {
550 579 ui: &'a Ui,
551 580 no_status: bool,
552 581 relativize: Option<RelativizePaths>,
553 582 print0: bool,
554 583 }
555 584
556 585 impl DisplayStatusPaths<'_> {
557 586 // Probably more elegant to use a Deref or Borrow trait rather than
558 587 // harcode HgPathBuf, but probably not really useful at this point
559 588 fn display(
560 589 &self,
561 590 status_prefix: &[u8],
562 591 label: &'static str,
563 592 mut paths: Vec<StatusPath<'_>>,
564 593 ) -> Result<(), CommandError> {
565 594 paths.sort_unstable();
566 595 // TODO: get the stdout lock once for the whole loop
567 596 // instead of in each write
568 597 for StatusPath { path, copy_source } in paths {
569 598 let relative_path;
570 599 let relative_source;
571 600 let (path, copy_source) = if let Some(relativize) =
572 601 &self.relativize
573 602 {
574 603 relative_path = relativize.relativize(&path);
575 604 relative_source =
576 605 copy_source.as_ref().map(|s| relativize.relativize(s));
577 606 (&*relative_path, relative_source.as_deref())
578 607 } else {
579 608 (path.as_bytes(), copy_source.as_ref().map(|s| s.as_bytes()))
580 609 };
581 610 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
582 611 // in order to stream to stdout instead of allocating an
583 612 // itermediate `Vec<u8>`.
584 613 if !self.no_status {
585 614 self.ui.write_stdout_labelled(status_prefix, label)?
586 615 }
587 616 let linebreak = if self.print0 { b"\x00" } else { b"\n" };
588 617 self.ui.write_stdout_labelled(
589 618 &format_bytes!(b"{}{}", path, linebreak),
590 619 label,
591 620 )?;
592 621 if let Some(source) = copy_source.filter(|_| !self.no_status) {
593 622 let label = "status.copied";
594 623 self.ui.write_stdout_labelled(
595 624 &format_bytes!(b" {}{}", source, linebreak),
596 625 label,
597 626 )?
598 627 }
599 628 }
600 629 Ok(())
601 630 }
602 631 }
603 632
604 633 /// Outcome of the additional check for an ambiguous tracked file
605 634 enum UnsureOutcome {
606 635 /// The file is actually clean
607 636 Clean,
608 637 /// The file has been modified
609 638 Modified,
610 639 /// The file was deleted on disk (or became another type of fs entry)
611 640 Deleted,
612 641 }
613 642
614 643 /// Check if a file is modified by comparing actual repo store and file system.
615 644 ///
616 645 /// This meant to be used for those that the dirstate cannot resolve, due
617 646 /// to time resolution limits.
618 647 fn unsure_is_modified(
619 648 working_directory_vfs: hg::vfs::Vfs,
620 649 store_vfs: hg::vfs::Vfs,
621 650 check_exec: bool,
622 651 manifest: &Manifest,
623 652 hg_path: &HgPath,
624 653 ) -> Result<UnsureOutcome, HgError> {
625 654 let vfs = working_directory_vfs;
626 655 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
627 656 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
628 657 let is_symlink = fs_metadata.file_type().is_symlink();
629 658
630 659 let entry = manifest
631 660 .find_by_path(hg_path)?
632 661 .expect("ambgious file not in p1");
633 662
634 663 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
635 664 // dirstate
636 665 let fs_flags = if is_symlink {
637 666 Some(b'l')
638 667 } else if check_exec && has_exec_bit(&fs_metadata) {
639 668 Some(b'x')
640 669 } else {
641 670 None
642 671 };
643 672
644 673 let entry_flags = if check_exec {
645 674 entry.flags
646 675 } else if entry.flags == Some(b'x') {
647 676 None
648 677 } else {
649 678 entry.flags
650 679 };
651 680
652 681 if entry_flags != fs_flags {
653 682 return Ok(UnsureOutcome::Modified);
654 683 }
655 684 let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;
656 685 let fs_len = fs_metadata.len();
657 686 let file_node = entry.node_id()?;
658 687 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
659 688 HgError::corrupted(format!(
660 689 "filelog {:?} missing node {:?} from manifest",
661 690 hg_path, file_node
662 691 ))
663 692 })?;
664 693 if filelog_entry.file_data_len_not_equal_to(fs_len) {
665 694 // No need to read file contents:
666 695 // it cannot be equal if it has a different length.
667 696 return Ok(UnsureOutcome::Modified);
668 697 }
669 698
670 699 let p1_filelog_data = filelog_entry.data()?;
671 700 let p1_contents = p1_filelog_data.file_data()?;
672 701 if p1_contents.len() as u64 != fs_len {
673 702 // No need to read file contents:
674 703 // it cannot be equal if it has a different length.
675 704 return Ok(UnsureOutcome::Modified);
676 705 }
677 706
678 707 let fs_contents = if is_symlink {
679 708 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
680 709 } else {
681 710 vfs.read(fs_path)?
682 711 };
683 712
684 713 Ok(if p1_contents != &*fs_contents {
685 714 UnsureOutcome::Modified
686 715 } else {
687 716 UnsureOutcome::Clean
688 717 })
689 718 }
@@ -1,295 +1,308 b''
1 1 use crate::ui::utf8_to_local;
2 2 use crate::ui::UiError;
3 3 use crate::NoRepoInCwdError;
4 4 use format_bytes::format_bytes;
5 5 use hg::config::{ConfigError, ConfigParseError, ConfigValueParseError};
6 6 use hg::dirstate_tree::on_disk::DirstateV2ParseError;
7 7 use hg::errors::HgError;
8 8 use hg::exit_codes;
9 9 use hg::repo::RepoError;
10 10 use hg::revlog::RevlogError;
11 11 use hg::sparse::SparseConfigError;
12 12 use hg::utils::files::get_bytes_from_path;
13 use hg::{DirstateError, DirstateMapError, StatusError};
13 use hg::utils::hg_path::HgPathError;
14 use hg::{DirstateError, DirstateMapError, PatternError, StatusError};
14 15 use std::convert::From;
15 16
16 17 /// The kind of command error
17 18 #[derive(Debug)]
18 19 pub enum CommandError {
19 20 /// Exit with an error message and "standard" failure exit code.
20 21 Abort {
21 22 message: Vec<u8>,
22 23 detailed_exit_code: exit_codes::ExitCode,
23 24 hint: Option<Vec<u8>>,
24 25 },
25 26
26 27 /// Exit with a failure exit code but no message.
27 28 Unsuccessful,
28 29
29 30 /// Encountered something (such as a CLI argument, repository layout, …)
30 31 /// not supported by this version of `rhg`. Depending on configuration
31 32 /// `rhg` may attempt to silently fall back to Python-based `hg`, which
32 33 /// may or may not support this feature.
33 34 UnsupportedFeature { message: Vec<u8> },
34 35 /// The fallback executable does not exist (or has some other problem if
35 36 /// we end up being more precise about broken fallbacks).
36 37 InvalidFallback { path: Vec<u8>, err: String },
37 38 }
38 39
39 40 impl CommandError {
40 41 pub fn abort(message: impl AsRef<str>) -> Self {
41 42 CommandError::abort_with_exit_code(message, exit_codes::ABORT)
42 43 }
43 44
44 45 pub fn abort_with_exit_code(
45 46 message: impl AsRef<str>,
46 47 detailed_exit_code: exit_codes::ExitCode,
47 48 ) -> Self {
48 49 CommandError::Abort {
49 50 // TODO: bytes-based (instead of Unicode-based) formatting
50 51 // of error messages to handle non-UTF-8 filenames etc:
51 52 // https://www.mercurial-scm.org/wiki/EncodingStrategy#Mixing_output
52 53 message: utf8_to_local(message.as_ref()).into(),
53 54 detailed_exit_code,
54 55 hint: None,
55 56 }
56 57 }
57 58
58 59 pub fn abort_with_exit_code_and_hint(
59 60 message: impl AsRef<str>,
60 61 detailed_exit_code: exit_codes::ExitCode,
61 62 hint: Option<impl AsRef<str>>,
62 63 ) -> Self {
63 64 CommandError::Abort {
64 65 message: utf8_to_local(message.as_ref()).into(),
65 66 detailed_exit_code,
66 67 hint: hint.map(|h| utf8_to_local(h.as_ref()).into()),
67 68 }
68 69 }
69 70
70 71 pub fn abort_with_exit_code_bytes(
71 72 message: impl AsRef<[u8]>,
72 73 detailed_exit_code: exit_codes::ExitCode,
73 74 ) -> Self {
74 75 // TODO: use this everywhere it makes sense instead of the string
75 76 // version.
76 77 CommandError::Abort {
77 78 message: message.as_ref().into(),
78 79 detailed_exit_code,
79 80 hint: None,
80 81 }
81 82 }
82 83
83 84 pub fn unsupported(message: impl AsRef<str>) -> Self {
84 85 CommandError::UnsupportedFeature {
85 86 message: utf8_to_local(message.as_ref()).into(),
86 87 }
87 88 }
88 89 }
89 90
90 91 /// For now we don’t differenciate between invalid CLI args and valid for `hg`
91 92 /// but not supported yet by `rhg`.
92 93 impl From<clap::Error> for CommandError {
93 94 fn from(error: clap::Error) -> Self {
94 95 CommandError::unsupported(error.to_string())
95 96 }
96 97 }
97 98
98 99 impl From<HgError> for CommandError {
99 100 fn from(error: HgError) -> Self {
100 101 match error {
101 102 HgError::UnsupportedFeature(message) => {
102 103 CommandError::unsupported(message)
103 104 }
104 105 HgError::CensoredNodeError => {
105 106 CommandError::unsupported("Encountered a censored node")
106 107 }
107 108 HgError::Abort {
108 109 message,
109 110 detailed_exit_code,
110 111 hint,
111 112 } => CommandError::abort_with_exit_code_and_hint(
112 113 message,
113 114 detailed_exit_code,
114 115 hint,
115 116 ),
116 117 _ => CommandError::abort(error.to_string()),
117 118 }
118 119 }
119 120 }
120 121
121 122 impl From<ConfigValueParseError> for CommandError {
122 123 fn from(error: ConfigValueParseError) -> Self {
123 124 CommandError::abort_with_exit_code(
124 125 error.to_string(),
125 126 exit_codes::CONFIG_ERROR_ABORT,
126 127 )
127 128 }
128 129 }
129 130
130 131 impl From<UiError> for CommandError {
131 132 fn from(_error: UiError) -> Self {
132 133 // If we already failed writing to stdout or stderr,
133 134 // writing an error message to stderr about it would be likely to fail
134 135 // too.
135 136 CommandError::abort("")
136 137 }
137 138 }
138 139
139 140 impl From<RepoError> for CommandError {
140 141 fn from(error: RepoError) -> Self {
141 142 match error {
142 143 RepoError::NotFound { at } => {
143 144 CommandError::abort_with_exit_code_bytes(
144 145 format_bytes!(
145 146 b"abort: repository {} not found",
146 147 get_bytes_from_path(at)
147 148 ),
148 149 exit_codes::ABORT,
149 150 )
150 151 }
151 152 RepoError::ConfigParseError(error) => error.into(),
152 153 RepoError::Other(error) => error.into(),
153 154 }
154 155 }
155 156 }
156 157
157 158 impl<'a> From<&'a NoRepoInCwdError> for CommandError {
158 159 fn from(error: &'a NoRepoInCwdError) -> Self {
159 160 let NoRepoInCwdError { cwd } = error;
160 161 CommandError::abort_with_exit_code_bytes(
161 162 format_bytes!(
162 163 b"abort: no repository found in '{}' (.hg not found)!",
163 164 get_bytes_from_path(cwd)
164 165 ),
165 166 exit_codes::ABORT,
166 167 )
167 168 }
168 169 }
169 170
170 171 impl From<ConfigError> for CommandError {
171 172 fn from(error: ConfigError) -> Self {
172 173 match error {
173 174 ConfigError::Parse(error) => error.into(),
174 175 ConfigError::Other(error) => error.into(),
175 176 }
176 177 }
177 178 }
178 179
179 180 impl From<ConfigParseError> for CommandError {
180 181 fn from(error: ConfigParseError) -> Self {
181 182 let ConfigParseError {
182 183 origin,
183 184 line,
184 185 message,
185 186 } = error;
186 187 let line_message = if let Some(line_number) = line {
187 188 format_bytes!(b":{}", line_number.to_string().into_bytes())
188 189 } else {
189 190 Vec::new()
190 191 };
191 192 CommandError::abort_with_exit_code_bytes(
192 193 format_bytes!(
193 194 b"config error at {}{}: {}",
194 195 origin,
195 196 line_message,
196 197 message
197 198 ),
198 199 exit_codes::CONFIG_ERROR_ABORT,
199 200 )
200 201 }
201 202 }
202 203
203 204 impl From<(RevlogError, &str)> for CommandError {
204 205 fn from((err, rev): (RevlogError, &str)) -> CommandError {
205 206 match err {
206 207 RevlogError::WDirUnsupported => CommandError::abort(
207 208 "abort: working directory revision cannot be specified",
208 209 ),
209 210 RevlogError::InvalidRevision => CommandError::abort(format!(
210 211 "abort: invalid revision identifier: {}",
211 212 rev
212 213 )),
213 214 RevlogError::AmbiguousPrefix => CommandError::abort(format!(
214 215 "abort: ambiguous revision identifier: {}",
215 216 rev
216 217 )),
217 218 RevlogError::Other(error) => error.into(),
218 219 }
219 220 }
220 221 }
221 222
222 223 impl From<StatusError> for CommandError {
223 224 fn from(error: StatusError) -> Self {
224 225 match error {
225 226 StatusError::Pattern(_) => {
226 227 CommandError::unsupported(format!("{}", error))
227 228 }
228 229 _ => CommandError::abort(format!("{}", error)),
229 230 }
230 231 }
231 232 }
232 233
234 impl From<HgPathError> for CommandError {
235 fn from(error: HgPathError) -> Self {
236 CommandError::unsupported(format!("{}", error))
237 }
238 }
239
240 impl From<PatternError> for CommandError {
241 fn from(error: PatternError) -> Self {
242 CommandError::unsupported(format!("{}", error))
243 }
244 }
245
233 246 impl From<DirstateMapError> for CommandError {
234 247 fn from(error: DirstateMapError) -> Self {
235 248 CommandError::abort(format!("{}", error))
236 249 }
237 250 }
238 251
239 252 impl From<DirstateError> for CommandError {
240 253 fn from(error: DirstateError) -> Self {
241 254 match error {
242 255 DirstateError::Common(error) => error.into(),
243 256 DirstateError::Map(error) => error.into(),
244 257 }
245 258 }
246 259 }
247 260
248 261 impl From<DirstateV2ParseError> for CommandError {
249 262 fn from(error: DirstateV2ParseError) -> Self {
250 263 HgError::from(error).into()
251 264 }
252 265 }
253 266
254 267 impl From<SparseConfigError> for CommandError {
255 268 fn from(e: SparseConfigError) -> Self {
256 269 match e {
257 270 SparseConfigError::IncludesAfterExcludes { context } => {
258 271 Self::abort_with_exit_code_bytes(
259 272 format_bytes!(
260 273 b"{} config cannot have includes after excludes",
261 274 context
262 275 ),
263 276 exit_codes::CONFIG_PARSE_ERROR_ABORT,
264 277 )
265 278 }
266 279 SparseConfigError::EntryOutsideSection { context, line } => {
267 280 Self::abort_with_exit_code_bytes(
268 281 format_bytes!(
269 282 b"{} config entry outside of section: {}",
270 283 context,
271 284 &line,
272 285 ),
273 286 exit_codes::CONFIG_PARSE_ERROR_ABORT,
274 287 )
275 288 }
276 289 SparseConfigError::InvalidNarrowPrefix(prefix) => {
277 290 Self::abort_with_exit_code_bytes(
278 291 format_bytes!(
279 292 b"invalid prefix on narrow pattern: {}",
280 293 &prefix
281 294 ),
282 295 exit_codes::ABORT,
283 296 )
284 297 }
285 298 SparseConfigError::IncludesInNarrow => Self::abort(
286 299 "including other spec files using '%include' \
287 300 is not supported in narrowspec",
288 301 ),
289 302 SparseConfigError::HgError(e) => Self::from(e),
290 303 SparseConfigError::PatternError(e) => {
291 304 Self::unsupported(format!("{}", e))
292 305 }
293 306 }
294 307 }
295 308 }
General Comments 0
You need to be logged in to leave comments. Login now