68 pub enum BadMatch { |
48 pub enum BadMatch { |
69 OsError(i32), |
49 OsError(i32), |
70 BadType(BadType), |
50 BadType(BadType), |
71 } |
51 } |
72 |
52 |
73 /// Enum used to dispatch new status entries into the right collections. |
|
74 /// Is similar to `crate::EntryState`, but represents the transient state of |
|
75 /// entries during the lifetime of a command. |
|
76 #[derive(Debug, Copy, Clone)] |
|
77 pub enum Dispatch { |
|
78 Unsure, |
|
79 Modified, |
|
80 Added, |
|
81 Removed, |
|
82 Deleted, |
|
83 Clean, |
|
84 Unknown, |
|
85 Ignored, |
|
86 /// Empty dispatch, the file is not worth listing |
|
87 None, |
|
88 /// Was explicitly matched but cannot be found/accessed |
|
89 Bad(BadMatch), |
|
90 Directory { |
|
91 /// True if the directory used to be a file in the dmap so we can say |
|
92 /// that it's been removed. |
|
93 was_file: bool, |
|
94 }, |
|
95 } |
|
96 |
|
97 type IoResult<T> = std::io::Result<T>; |
|
98 |
|
99 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait + 'static>`, so add |
53 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait + 'static>`, so add |
100 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere". |
54 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere". |
101 pub type IgnoreFnType<'a> = |
55 pub type IgnoreFnType<'a> = |
102 Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>; |
56 Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>; |
103 |
57 |
104 /// We have a good mix of owned (from directory traversal) and borrowed (from |
58 /// We have a good mix of owned (from directory traversal) and borrowed (from |
105 /// the dirstate/explicit) paths, this comes up a lot. |
59 /// the dirstate/explicit) paths, this comes up a lot. |
106 pub type HgPathCow<'a> = Cow<'a, HgPath>; |
60 pub type HgPathCow<'a> = Cow<'a, HgPath>; |
107 |
|
108 /// A path with its computed ``Dispatch`` information |
|
109 type DispatchedPath<'a> = (HgPathCow<'a>, Dispatch); |
|
110 |
|
111 /// The conversion from `HgPath` to a real fs path failed. |
|
112 /// `22` is the error code for "Invalid argument" |
|
113 const INVALID_PATH_DISPATCH: Dispatch = Dispatch::Bad(BadMatch::OsError(22)); |
|
114 |
|
115 /// Dates and times that are outside the 31-bit signed range are compared |
|
116 /// modulo 2^31. This should prevent hg from behaving badly with very large |
|
117 /// files or corrupt dates while still having a high probability of detecting |
|
118 /// changes. (issue2608) |
|
119 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>` |
|
120 /// is not defined for `i32`, and there is no `As` trait. This forces the |
|
121 /// caller to cast `b` as `i32`. |
|
122 fn mod_compare(a: i32, b: i32) -> bool { |
|
123 a & i32::max_value() != b & i32::max_value() |
|
124 } |
|
125 |
|
126 /// Return a sorted list containing information about the entries |
|
127 /// in the directory. |
|
128 /// |
|
129 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory |
|
130 fn list_directory( |
|
131 path: impl AsRef<Path>, |
|
132 skip_dot_hg: bool, |
|
133 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> { |
|
134 let mut results = vec![]; |
|
135 let entries = read_dir(path.as_ref())?; |
|
136 |
|
137 for entry in entries { |
|
138 let entry = entry?; |
|
139 let filename = os_string_to_hg_path_buf(entry.file_name())?; |
|
140 let file_type = entry.file_type()?; |
|
141 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() { |
|
142 return Ok(vec![]); |
|
143 } else { |
|
144 results.push((filename, entry)) |
|
145 } |
|
146 } |
|
147 |
|
148 results.sort_unstable_by_key(|e| e.0.clone()); |
|
149 Ok(results) |
|
150 } |
|
151 |
|
152 /// The file corresponding to the dirstate entry was found on the filesystem. |
|
153 fn dispatch_found( |
|
154 filename: impl AsRef<HgPath>, |
|
155 entry: DirstateEntry, |
|
156 metadata: HgMetadata, |
|
157 copy_map: &CopyMap, |
|
158 options: StatusOptions, |
|
159 ) -> Dispatch { |
|
160 match entry.state() { |
|
161 EntryState::Normal => { |
|
162 let mode = entry.mode(); |
|
163 let size = entry.size(); |
|
164 let mtime = entry.mtime(); |
|
165 |
|
166 let HgMetadata { |
|
167 st_mode, |
|
168 st_size, |
|
169 st_mtime, |
|
170 .. |
|
171 } = metadata; |
|
172 |
|
173 let size_changed = mod_compare(size, st_size as i32); |
|
174 let mode_changed = |
|
175 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec; |
|
176 let metadata_changed = size >= 0 && (size_changed || mode_changed); |
|
177 let other_parent = size == SIZE_FROM_OTHER_PARENT; |
|
178 |
|
179 if metadata_changed |
|
180 || other_parent |
|
181 || copy_map.contains_key(filename.as_ref()) |
|
182 { |
|
183 if metadata.is_symlink() && size_changed { |
|
184 // issue6456: Size returned may be longer due to encryption |
|
185 // on EXT-4 fscrypt. TODO maybe only do it on EXT4? |
|
186 Dispatch::Unsure |
|
187 } else { |
|
188 Dispatch::Modified |
|
189 } |
|
190 } else if mod_compare(mtime, st_mtime as i32) |
|
191 || st_mtime == options.last_normal_time |
|
192 { |
|
193 // the file may have just been marked as normal and |
|
194 // it may have changed in the same second without |
|
195 // changing its size. This can happen if we quickly |
|
196 // do multiple commits. Force lookup, so we don't |
|
197 // miss such a racy file change. |
|
198 Dispatch::Unsure |
|
199 } else if options.list_clean { |
|
200 Dispatch::Clean |
|
201 } else { |
|
202 Dispatch::None |
|
203 } |
|
204 } |
|
205 EntryState::Merged => Dispatch::Modified, |
|
206 EntryState::Added => Dispatch::Added, |
|
207 EntryState::Removed => Dispatch::Removed, |
|
208 } |
|
209 } |
|
210 |
|
211 /// The file corresponding to this Dirstate entry is missing. |
|
212 fn dispatch_missing(state: EntryState) -> Dispatch { |
|
213 match state { |
|
214 // File was removed from the filesystem during commands |
|
215 EntryState::Normal | EntryState::Merged | EntryState::Added => { |
|
216 Dispatch::Deleted |
|
217 } |
|
218 // File was removed, everything is normal |
|
219 EntryState::Removed => Dispatch::Removed, |
|
220 } |
|
221 } |
|
222 |
|
223 fn dispatch_os_error(e: &std::io::Error) -> Dispatch { |
|
224 Dispatch::Bad(BadMatch::OsError( |
|
225 e.raw_os_error().expect("expected real OS error"), |
|
226 )) |
|
227 } |
|
228 |
|
229 lazy_static! { |
|
230 static ref DEFAULT_WORK: HashSet<&'static HgPath> = { |
|
231 let mut h = HashSet::new(); |
|
232 h.insert(HgPath::new(b"")); |
|
233 h |
|
234 }; |
|
235 } |
|
236 |
61 |
237 #[derive(Debug, Copy, Clone)] |
62 #[derive(Debug, Copy, Clone)] |
238 pub struct StatusOptions { |
63 pub struct StatusOptions { |
239 /// Remember the most recent modification timeslot for status, to make |
64 /// Remember the most recent modification timeslot for status, to make |
240 /// sure we won't miss future size-preserving file content modifications |
65 /// sure we won't miss future size-preserving file content modifications |
317 f.write_str("dirstate-v2 parse error") |
142 f.write_str("dirstate-v2 parse error") |
318 } |
143 } |
319 } |
144 } |
320 } |
145 } |
321 } |
146 } |
322 |
|
323 /// Gives information about which files are changed in the working directory |
|
324 /// and how, compared to the revision we're based on |
|
325 pub struct Status<'a, M: ?Sized + Matcher + Sync> { |
|
326 dmap: &'a DirstateMap, |
|
327 pub(crate) matcher: &'a M, |
|
328 root_dir: PathBuf, |
|
329 pub(crate) options: StatusOptions, |
|
330 ignore_fn: IgnoreFnType<'a>, |
|
331 } |
|
332 |
|
333 impl<'a, M> Status<'a, M> |
|
334 where |
|
335 M: ?Sized + Matcher + Sync, |
|
336 { |
|
337 pub fn new( |
|
338 dmap: &'a DirstateMap, |
|
339 matcher: &'a M, |
|
340 root_dir: PathBuf, |
|
341 ignore_files: Vec<PathBuf>, |
|
342 options: StatusOptions, |
|
343 ) -> StatusResult<(Self, Vec<PatternFileWarning>)> { |
|
344 // Needs to outlive `dir_ignore_fn` since it's captured. |
|
345 |
|
346 let (ignore_fn, warnings): (IgnoreFnType, _) = |
|
347 if options.list_ignored || options.list_unknown { |
|
348 get_ignore_function(ignore_files, &root_dir, &mut |_| {})? |
|
349 } else { |
|
350 (Box::new(|&_| true), vec![]) |
|
351 }; |
|
352 |
|
353 Ok(( |
|
354 Self { |
|
355 dmap, |
|
356 matcher, |
|
357 root_dir, |
|
358 options, |
|
359 ignore_fn, |
|
360 }, |
|
361 warnings, |
|
362 )) |
|
363 } |
|
364 |
|
365 /// Is the path ignored? |
|
366 pub fn is_ignored(&self, path: impl AsRef<HgPath>) -> bool { |
|
367 (self.ignore_fn)(path.as_ref()) |
|
368 } |
|
369 |
|
370 /// Is the path or one of its ancestors ignored? |
|
371 pub fn dir_ignore(&self, dir: impl AsRef<HgPath>) -> bool { |
|
372 // Only involve ignore mechanism if we're listing unknowns or ignored. |
|
373 if self.options.list_ignored || self.options.list_unknown { |
|
374 if self.is_ignored(&dir) { |
|
375 true |
|
376 } else { |
|
377 for p in find_dirs(dir.as_ref()) { |
|
378 if self.is_ignored(p) { |
|
379 return true; |
|
380 } |
|
381 } |
|
382 false |
|
383 } |
|
384 } else { |
|
385 true |
|
386 } |
|
387 } |
|
388 |
|
389 /// Get stat data about the files explicitly specified by the matcher. |
|
390 /// Returns a tuple of the directories that need to be traversed and the |
|
391 /// files with their corresponding `Dispatch`. |
|
392 /// TODO subrepos |
|
393 #[timed] |
|
394 pub fn walk_explicit( |
|
395 &self, |
|
396 traversed_sender: crossbeam_channel::Sender<HgPathBuf>, |
|
397 ) -> (Vec<DispatchedPath<'a>>, Vec<DispatchedPath<'a>>) { |
|
398 self.matcher |
|
399 .file_set() |
|
400 .unwrap_or(&DEFAULT_WORK) |
|
401 .par_iter() |
|
402 .flat_map(|&filename| -> Option<_> { |
|
403 // TODO normalization |
|
404 let normalized = filename; |
|
405 |
|
406 let buf = match hg_path_to_path_buf(normalized) { |
|
407 Ok(x) => x, |
|
408 Err(_) => { |
|
409 return Some(( |
|
410 Cow::Borrowed(normalized), |
|
411 INVALID_PATH_DISPATCH, |
|
412 )) |
|
413 } |
|
414 }; |
|
415 let target = self.root_dir.join(buf); |
|
416 let st = target.symlink_metadata(); |
|
417 let in_dmap = self.dmap.get(normalized); |
|
418 match st { |
|
419 Ok(meta) => { |
|
420 let file_type = meta.file_type(); |
|
421 return if file_type.is_file() || file_type.is_symlink() |
|
422 { |
|
423 if let Some(entry) = in_dmap { |
|
424 return Some(( |
|
425 Cow::Borrowed(normalized), |
|
426 dispatch_found( |
|
427 &normalized, |
|
428 *entry, |
|
429 HgMetadata::from_metadata(meta), |
|
430 &self.dmap.copy_map, |
|
431 self.options, |
|
432 ), |
|
433 )); |
|
434 } |
|
435 Some(( |
|
436 Cow::Borrowed(normalized), |
|
437 Dispatch::Unknown, |
|
438 )) |
|
439 } else if file_type.is_dir() { |
|
440 if self.options.collect_traversed_dirs { |
|
441 traversed_sender |
|
442 .send(normalized.to_owned()) |
|
443 .expect("receiver should outlive sender"); |
|
444 } |
|
445 Some(( |
|
446 Cow::Borrowed(normalized), |
|
447 Dispatch::Directory { |
|
448 was_file: in_dmap.is_some(), |
|
449 }, |
|
450 )) |
|
451 } else { |
|
452 Some(( |
|
453 Cow::Borrowed(normalized), |
|
454 Dispatch::Bad(BadMatch::BadType( |
|
455 // TODO do more than unknown |
|
456 // Support for all `BadType` variant |
|
457 // varies greatly between platforms. |
|
458 // So far, no tests check the type and |
|
459 // this should be good enough for most |
|
460 // users. |
|
461 BadType::Unknown, |
|
462 )), |
|
463 )) |
|
464 }; |
|
465 } |
|
466 Err(_) => { |
|
467 if let Some(entry) = in_dmap { |
|
468 return Some(( |
|
469 Cow::Borrowed(normalized), |
|
470 dispatch_missing(entry.state()), |
|
471 )); |
|
472 } |
|
473 } |
|
474 }; |
|
475 None |
|
476 }) |
|
477 .partition(|(_, dispatch)| match dispatch { |
|
478 Dispatch::Directory { .. } => true, |
|
479 _ => false, |
|
480 }) |
|
481 } |
|
482 |
|
483 /// Walk the working directory recursively to look for changes compared to |
|
484 /// the current `DirstateMap`. |
|
485 /// |
|
486 /// This takes a mutable reference to the results to account for the |
|
487 /// `extend` in timings |
|
488 #[timed] |
|
489 pub fn traverse( |
|
490 &self, |
|
491 path: impl AsRef<HgPath>, |
|
492 old_results: &FastHashMap<HgPathCow<'a>, Dispatch>, |
|
493 results: &mut Vec<DispatchedPath<'a>>, |
|
494 traversed_sender: crossbeam_channel::Sender<HgPathBuf>, |
|
495 ) { |
|
496 // The traversal is done in parallel, so use a channel to gather |
|
497 // entries. `crossbeam_channel::Sender` is `Sync`, while `mpsc::Sender` |
|
498 // is not. |
|
499 let (files_transmitter, files_receiver) = |
|
500 crossbeam_channel::unbounded(); |
|
501 |
|
502 self.traverse_dir( |
|
503 &files_transmitter, |
|
504 path, |
|
505 &old_results, |
|
506 traversed_sender, |
|
507 ); |
|
508 |
|
509 // Disconnect the channel so the receiver stops waiting |
|
510 drop(files_transmitter); |
|
511 |
|
512 let new_results = files_receiver |
|
513 .into_iter() |
|
514 .par_bridge() |
|
515 .map(|(f, d)| (Cow::Owned(f), d)); |
|
516 |
|
517 results.par_extend(new_results); |
|
518 } |
|
519 |
|
520 /// Dispatch a single entry (file, folder, symlink...) found during |
|
521 /// `traverse`. If the entry is a folder that needs to be traversed, it |
|
522 /// will be handled in a separate thread. |
|
523 fn handle_traversed_entry<'b>( |
|
524 &'a self, |
|
525 scope: &rayon::Scope<'b>, |
|
526 files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>, |
|
527 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>, |
|
528 filename: HgPathBuf, |
|
529 dir_entry: DirEntry, |
|
530 traversed_sender: crossbeam_channel::Sender<HgPathBuf>, |
|
531 ) -> IoResult<()> |
|
532 where |
|
533 'a: 'b, |
|
534 { |
|
535 let file_type = dir_entry.file_type()?; |
|
536 let entry_option = self.dmap.get(&filename); |
|
537 |
|
538 if filename.as_bytes() == b".hg" { |
|
539 // Could be a directory or a symlink |
|
540 return Ok(()); |
|
541 } |
|
542 |
|
543 if file_type.is_dir() { |
|
544 self.handle_traversed_dir( |
|
545 scope, |
|
546 files_sender, |
|
547 old_results, |
|
548 entry_option, |
|
549 filename, |
|
550 traversed_sender, |
|
551 ); |
|
552 } else if file_type.is_file() || file_type.is_symlink() { |
|
553 if let Some(entry) = entry_option { |
|
554 if self.matcher.matches_everything() |
|
555 || self.matcher.matches(&filename) |
|
556 { |
|
557 let metadata = dir_entry.metadata()?; |
|
558 files_sender |
|
559 .send(( |
|
560 filename.to_owned(), |
|
561 dispatch_found( |
|
562 &filename, |
|
563 *entry, |
|
564 HgMetadata::from_metadata(metadata), |
|
565 &self.dmap.copy_map, |
|
566 self.options, |
|
567 ), |
|
568 )) |
|
569 .unwrap(); |
|
570 } |
|
571 } else if (self.matcher.matches_everything() |
|
572 || self.matcher.matches(&filename)) |
|
573 && !self.is_ignored(&filename) |
|
574 { |
|
575 if (self.options.list_ignored |
|
576 || self.matcher.exact_match(&filename)) |
|
577 && self.dir_ignore(&filename) |
|
578 { |
|
579 if self.options.list_ignored { |
|
580 files_sender |
|
581 .send((filename.to_owned(), Dispatch::Ignored)) |
|
582 .unwrap(); |
|
583 } |
|
584 } else if self.options.list_unknown { |
|
585 files_sender |
|
586 .send((filename.to_owned(), Dispatch::Unknown)) |
|
587 .unwrap(); |
|
588 } |
|
589 } else if self.is_ignored(&filename) && self.options.list_ignored { |
|
590 if self.matcher.matches(&filename) { |
|
591 files_sender |
|
592 .send((filename.to_owned(), Dispatch::Ignored)) |
|
593 .unwrap(); |
|
594 } |
|
595 } |
|
596 } else if let Some(entry) = entry_option { |
|
597 // Used to be a file or a folder, now something else. |
|
598 if self.matcher.matches_everything() |
|
599 || self.matcher.matches(&filename) |
|
600 { |
|
601 files_sender |
|
602 .send(( |
|
603 filename.to_owned(), |
|
604 dispatch_missing(entry.state()), |
|
605 )) |
|
606 .unwrap(); |
|
607 } |
|
608 } |
|
609 |
|
610 Ok(()) |
|
611 } |
|
612 |
|
613 /// A directory was found in the filesystem and needs to be traversed |
|
614 fn handle_traversed_dir<'b>( |
|
615 &'a self, |
|
616 scope: &rayon::Scope<'b>, |
|
617 files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>, |
|
618 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>, |
|
619 entry_option: Option<&'a DirstateEntry>, |
|
620 directory: HgPathBuf, |
|
621 traversed_sender: crossbeam_channel::Sender<HgPathBuf>, |
|
622 ) where |
|
623 'a: 'b, |
|
624 { |
|
625 scope.spawn(move |_| { |
|
626 // Nested `if` until `rust-lang/rust#53668` is stable |
|
627 if let Some(entry) = entry_option { |
|
628 // Used to be a file, is now a folder |
|
629 if self.matcher.matches_everything() |
|
630 || self.matcher.matches(&directory) |
|
631 { |
|
632 files_sender |
|
633 .send(( |
|
634 directory.to_owned(), |
|
635 dispatch_missing(entry.state()), |
|
636 )) |
|
637 .unwrap(); |
|
638 } |
|
639 } |
|
640 // Do we need to traverse it? |
|
641 if !self.is_ignored(&directory) || self.options.list_ignored { |
|
642 self.traverse_dir( |
|
643 files_sender, |
|
644 directory, |
|
645 &old_results, |
|
646 traversed_sender, |
|
647 ) |
|
648 } |
|
649 }); |
|
650 } |
|
651 |
|
652 /// Decides whether the directory needs to be listed, and if so handles the |
|
653 /// entries in a separate thread. |
|
654 fn traverse_dir( |
|
655 &self, |
|
656 files_sender: &crossbeam_channel::Sender<(HgPathBuf, Dispatch)>, |
|
657 directory: impl AsRef<HgPath>, |
|
658 old_results: &FastHashMap<Cow<HgPath>, Dispatch>, |
|
659 traversed_sender: crossbeam_channel::Sender<HgPathBuf>, |
|
660 ) { |
|
661 let directory = directory.as_ref(); |
|
662 |
|
663 if self.options.collect_traversed_dirs { |
|
664 traversed_sender |
|
665 .send(directory.to_owned()) |
|
666 .expect("receiver should outlive sender"); |
|
667 } |
|
668 |
|
669 let visit_entries = match self.matcher.visit_children_set(directory) { |
|
670 VisitChildrenSet::Empty => return, |
|
671 VisitChildrenSet::This | VisitChildrenSet::Recursive => None, |
|
672 VisitChildrenSet::Set(set) => Some(set), |
|
673 }; |
|
674 let buf = match hg_path_to_path_buf(directory) { |
|
675 Ok(b) => b, |
|
676 Err(_) => { |
|
677 files_sender |
|
678 .send((directory.to_owned(), INVALID_PATH_DISPATCH)) |
|
679 .expect("receiver should outlive sender"); |
|
680 return; |
|
681 } |
|
682 }; |
|
683 let dir_path = self.root_dir.join(buf); |
|
684 |
|
685 let skip_dot_hg = !directory.as_bytes().is_empty(); |
|
686 let entries = match list_directory(dir_path, skip_dot_hg) { |
|
687 Err(e) => { |
|
688 files_sender |
|
689 .send((directory.to_owned(), dispatch_os_error(&e))) |
|
690 .expect("receiver should outlive sender"); |
|
691 return; |
|
692 } |
|
693 Ok(entries) => entries, |
|
694 }; |
|
695 |
|
696 rayon::scope(|scope| { |
|
697 for (filename, dir_entry) in entries { |
|
698 if let Some(ref set) = visit_entries { |
|
699 if !set.contains(filename.deref()) { |
|
700 continue; |
|
701 } |
|
702 } |
|
703 // TODO normalize |
|
704 let filename = if directory.is_empty() { |
|
705 filename.to_owned() |
|
706 } else { |
|
707 directory.join(&filename) |
|
708 }; |
|
709 |
|
710 if !old_results.contains_key(filename.deref()) { |
|
711 match self.handle_traversed_entry( |
|
712 scope, |
|
713 files_sender, |
|
714 old_results, |
|
715 filename, |
|
716 dir_entry, |
|
717 traversed_sender.clone(), |
|
718 ) { |
|
719 Err(e) => { |
|
720 files_sender |
|
721 .send(( |
|
722 directory.to_owned(), |
|
723 dispatch_os_error(&e), |
|
724 )) |
|
725 .expect("receiver should outlive sender"); |
|
726 } |
|
727 Ok(_) => {} |
|
728 } |
|
729 } |
|
730 } |
|
731 }) |
|
732 } |
|
733 |
|
734 /// Add the files in the dirstate to the results. |
|
735 /// |
|
736 /// This takes a mutable reference to the results to account for the |
|
737 /// `extend` in timings |
|
738 #[timed] |
|
739 pub fn extend_from_dmap(&self, results: &mut Vec<DispatchedPath<'a>>) { |
|
740 results.par_extend( |
|
741 self.dmap |
|
742 .par_iter() |
|
743 .filter(|(path, _)| self.matcher.matches(path)) |
|
744 .map(move |(filename, entry)| { |
|
745 let filename: &HgPath = filename; |
|
746 let filename_as_path = match hg_path_to_path_buf(filename) |
|
747 { |
|
748 Ok(f) => f, |
|
749 Err(_) => { |
|
750 return ( |
|
751 Cow::Borrowed(filename), |
|
752 INVALID_PATH_DISPATCH, |
|
753 ) |
|
754 } |
|
755 }; |
|
756 let meta = self |
|
757 .root_dir |
|
758 .join(filename_as_path) |
|
759 .symlink_metadata(); |
|
760 match meta { |
|
761 Ok(m) |
|
762 if !(m.file_type().is_file() |
|
763 || m.file_type().is_symlink()) => |
|
764 { |
|
765 ( |
|
766 Cow::Borrowed(filename), |
|
767 dispatch_missing(entry.state()), |
|
768 ) |
|
769 } |
|
770 Ok(m) => ( |
|
771 Cow::Borrowed(filename), |
|
772 dispatch_found( |
|
773 filename, |
|
774 *entry, |
|
775 HgMetadata::from_metadata(m), |
|
776 &self.dmap.copy_map, |
|
777 self.options, |
|
778 ), |
|
779 ), |
|
780 Err(e) |
|
781 if e.kind() == ErrorKind::NotFound |
|
782 || e.raw_os_error() == Some(20) => |
|
783 { |
|
784 // Rust does not yet have an `ErrorKind` for |
|
785 // `NotADirectory` (errno 20) |
|
786 // It happens if the dirstate contains `foo/bar` |
|
787 // and foo is not a |
|
788 // directory |
|
789 ( |
|
790 Cow::Borrowed(filename), |
|
791 dispatch_missing(entry.state()), |
|
792 ) |
|
793 } |
|
794 Err(e) => { |
|
795 (Cow::Borrowed(filename), dispatch_os_error(&e)) |
|
796 } |
|
797 } |
|
798 }), |
|
799 ); |
|
800 } |
|
801 |
|
802 /// Checks all files that are in the dirstate but were not found during the |
|
803 /// working directory traversal. This means that the rest must |
|
804 /// be either ignored, under a symlink or under a new nested repo. |
|
805 /// |
|
806 /// This takes a mutable reference to the results to account for the |
|
807 /// `extend` in timings |
|
808 #[timed] |
|
809 pub fn handle_unknowns(&self, results: &mut Vec<DispatchedPath<'a>>) { |
|
810 let to_visit: Vec<(&HgPath, &DirstateEntry)> = |
|
811 if results.is_empty() && self.matcher.matches_everything() { |
|
812 self.dmap.iter().map(|(f, e)| (f.deref(), e)).collect() |
|
813 } else { |
|
814 // Only convert to a hashmap if needed. |
|
815 let old_results: FastHashMap<_, _> = |
|
816 results.iter().cloned().collect(); |
|
817 self.dmap |
|
818 .iter() |
|
819 .filter_map(move |(f, e)| { |
|
820 if !old_results.contains_key(f.deref()) |
|
821 && self.matcher.matches(f) |
|
822 { |
|
823 Some((f.deref(), e)) |
|
824 } else { |
|
825 None |
|
826 } |
|
827 }) |
|
828 .collect() |
|
829 }; |
|
830 |
|
831 let path_auditor = PathAuditor::new(&self.root_dir); |
|
832 |
|
833 let new_results = to_visit.into_par_iter().filter_map( |
|
834 |(filename, entry)| -> Option<_> { |
|
835 // Report ignored items in the dmap as long as they are not |
|
836 // under a symlink directory. |
|
837 if path_auditor.check(filename) { |
|
838 // TODO normalize for case-insensitive filesystems |
|
839 let buf = match hg_path_to_path_buf(filename) { |
|
840 Ok(x) => x, |
|
841 Err(_) => { |
|
842 return Some(( |
|
843 Cow::Owned(filename.to_owned()), |
|
844 INVALID_PATH_DISPATCH, |
|
845 )); |
|
846 } |
|
847 }; |
|
848 Some(( |
|
849 Cow::Owned(filename.to_owned()), |
|
850 match self.root_dir.join(&buf).symlink_metadata() { |
|
851 // File was just ignored, no links, and exists |
|
852 Ok(meta) => { |
|
853 let metadata = HgMetadata::from_metadata(meta); |
|
854 dispatch_found( |
|
855 filename, |
|
856 *entry, |
|
857 metadata, |
|
858 &self.dmap.copy_map, |
|
859 self.options, |
|
860 ) |
|
861 } |
|
862 // File doesn't exist |
|
863 Err(_) => dispatch_missing(entry.state()), |
|
864 }, |
|
865 )) |
|
866 } else { |
|
867 // It's either missing or under a symlink directory which |
|
868 // we, in this case, report as missing. |
|
869 Some(( |
|
870 Cow::Owned(filename.to_owned()), |
|
871 dispatch_missing(entry.state()), |
|
872 )) |
|
873 } |
|
874 }, |
|
875 ); |
|
876 |
|
877 results.par_extend(new_results); |
|
878 } |
|
879 } |
|
880 |
|
881 #[timed] |
|
882 pub fn build_response<'a>( |
|
883 results: impl IntoIterator<Item = DispatchedPath<'a>>, |
|
884 traversed: Vec<HgPathCow<'a>>, |
|
885 ) -> DirstateStatus<'a> { |
|
886 let mut unsure = vec![]; |
|
887 let mut modified = vec![]; |
|
888 let mut added = vec![]; |
|
889 let mut removed = vec![]; |
|
890 let mut deleted = vec![]; |
|
891 let mut clean = vec![]; |
|
892 let mut ignored = vec![]; |
|
893 let mut unknown = vec![]; |
|
894 let mut bad = vec![]; |
|
895 |
|
896 for (filename, dispatch) in results.into_iter() { |
|
897 match dispatch { |
|
898 Dispatch::Unknown => unknown.push(filename), |
|
899 Dispatch::Unsure => unsure.push(filename), |
|
900 Dispatch::Modified => modified.push(filename), |
|
901 Dispatch::Added => added.push(filename), |
|
902 Dispatch::Removed => removed.push(filename), |
|
903 Dispatch::Deleted => deleted.push(filename), |
|
904 Dispatch::Clean => clean.push(filename), |
|
905 Dispatch::Ignored => ignored.push(filename), |
|
906 Dispatch::None => {} |
|
907 Dispatch::Bad(reason) => bad.push((filename, reason)), |
|
908 Dispatch::Directory { .. } => {} |
|
909 } |
|
910 } |
|
911 |
|
912 DirstateStatus { |
|
913 modified, |
|
914 added, |
|
915 removed, |
|
916 deleted, |
|
917 clean, |
|
918 ignored, |
|
919 unknown, |
|
920 bad, |
|
921 unsure, |
|
922 traversed, |
|
923 dirty: false, |
|
924 } |
|
925 } |
|
926 |
|
927 /// Get the status of files in the working directory. |
|
928 /// |
|
929 /// This is the current entry-point for `hg-core` and is realistically unusable |
|
930 /// outside of a Python context because its arguments need to provide a lot of |
|
931 /// information that will not be necessary in the future. |
|
932 #[timed] |
|
933 pub fn status<'a>( |
|
934 dmap: &'a DirstateMap, |
|
935 matcher: &'a (dyn Matcher + Sync), |
|
936 root_dir: PathBuf, |
|
937 ignore_files: Vec<PathBuf>, |
|
938 options: StatusOptions, |
|
939 ) -> StatusResult<(DirstateStatus<'a>, Vec<PatternFileWarning>)> { |
|
940 let (status, warnings) = |
|
941 Status::new(dmap, matcher, root_dir, ignore_files, options)?; |
|
942 |
|
943 Ok((status.run()?, warnings)) |
|
944 } |
|