##// END OF EJS Templates
rust-status: refactor status into a struct...
Raphaël Gomès -
r45671:7528699c default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (834 lines changed) Show them Hide them
@@ -73,7 +73,7 b' pub enum BadMatch {'
73 /// Is similar to `crate::EntryState`, but represents the transient state of
73 /// Is similar to `crate::EntryState`, but represents the transient state of
74 /// entries during the lifetime of a command.
74 /// entries during the lifetime of a command.
75 #[derive(Debug, Copy, Clone)]
75 #[derive(Debug, Copy, Clone)]
76 enum Dispatch {
76 pub enum Dispatch {
77 Unsure,
77 Unsure,
78 Modified,
78 Modified,
79 Added,
79 Added,
@@ -214,88 +214,6 b' lazy_static! {'
214 };
214 };
215 }
215 }
216
216
217 /// Get stat data about the files explicitly specified by match.
218 /// TODO subrepos
219 #[timed]
220 fn walk_explicit<'a>(
221 files: Option<&'a HashSet<&HgPath>>,
222 dmap: &'a DirstateMap,
223 root_dir: impl AsRef<Path> + Sync + Send + 'a,
224 options: StatusOptions,
225 traversed_sender: crossbeam::Sender<HgPathBuf>,
226 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
227 files
228 .unwrap_or(&DEFAULT_WORK)
229 .par_iter()
230 .map(move |&filename| {
231 // TODO normalization
232 let normalized = filename;
233
234 let buf = match hg_path_to_path_buf(normalized) {
235 Ok(x) => x,
236 Err(e) => return Some(Err(e.into())),
237 };
238 let target = root_dir.as_ref().join(buf);
239 let st = target.symlink_metadata();
240 let in_dmap = dmap.get(normalized);
241 match st {
242 Ok(meta) => {
243 let file_type = meta.file_type();
244 return if file_type.is_file() || file_type.is_symlink() {
245 if let Some(entry) = in_dmap {
246 return Some(Ok((
247 normalized,
248 dispatch_found(
249 &normalized,
250 *entry,
251 HgMetadata::from_metadata(meta),
252 &dmap.copy_map,
253 options,
254 ),
255 )));
256 }
257 Some(Ok((normalized, Dispatch::Unknown)))
258 } else if file_type.is_dir() {
259 if options.collect_traversed_dirs {
260 traversed_sender
261 .send(normalized.to_owned())
262 .expect("receiver should outlive sender");
263 }
264 Some(Ok((
265 normalized,
266 Dispatch::Directory {
267 was_file: in_dmap.is_some(),
268 },
269 )))
270 } else {
271 Some(Ok((
272 normalized,
273 Dispatch::Bad(BadMatch::BadType(
274 // TODO do more than unknown
275 // Support for all `BadType` variant
276 // varies greatly between platforms.
277 // So far, no tests check the type and
278 // this should be good enough for most
279 // users.
280 BadType::Unknown,
281 )),
282 )))
283 };
284 }
285 Err(_) => {
286 if let Some(entry) = in_dmap {
287 return Some(Ok((
288 normalized,
289 dispatch_missing(entry.state),
290 )));
291 }
292 }
293 };
294 None
295 })
296 .flatten()
297 }
298
299 #[derive(Debug, Copy, Clone)]
217 #[derive(Debug, Copy, Clone)]
300 pub struct StatusOptions {
218 pub struct StatusOptions {
301 /// Remember the most recent modification timeslot for status, to make
219 /// Remember the most recent modification timeslot for status, to make
@@ -312,25 +230,272 b' pub struct StatusOptions {'
312 pub collect_traversed_dirs: bool,
230 pub collect_traversed_dirs: bool,
313 }
231 }
314
232
315 /// Dispatch a single entry (file, folder, symlink...) found during `traverse`.
233 #[derive(Debug)]
316 /// If the entry is a folder that needs to be traversed, it will be handled
234 pub struct DirstateStatus<'a> {
317 /// in a separate thread.
235 pub modified: Vec<Cow<'a, HgPath>>,
318 fn handle_traversed_entry<'a>(
236 pub added: Vec<Cow<'a, HgPath>>,
319 scope: &rayon::Scope<'a>,
237 pub removed: Vec<Cow<'a, HgPath>>,
320 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
238 pub deleted: Vec<Cow<'a, HgPath>>,
321 matcher: &'a (impl Matcher + Sync),
239 pub clean: Vec<Cow<'a, HgPath>>,
322 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
240 pub ignored: Vec<Cow<'a, HgPath>>,
241 pub unknown: Vec<Cow<'a, HgPath>>,
242 pub bad: Vec<(Cow<'a, HgPath>, BadMatch)>,
243 /// Only filled if `collect_traversed_dirs` is `true`
244 pub traversed: Vec<HgPathBuf>,
245 }
246
247 #[derive(Debug)]
248 pub enum StatusError {
249 IO(std::io::Error),
250 Path(HgPathError),
251 Pattern(PatternError),
252 }
253
254 pub type StatusResult<T> = Result<T, StatusError>;
255
256 impl From<PatternError> for StatusError {
257 fn from(e: PatternError) -> Self {
258 StatusError::Pattern(e)
259 }
260 }
261 impl From<HgPathError> for StatusError {
262 fn from(e: HgPathError) -> Self {
263 StatusError::Path(e)
264 }
265 }
266 impl From<std::io::Error> for StatusError {
267 fn from(e: std::io::Error) -> Self {
268 StatusError::IO(e)
269 }
270 }
271
272 impl ToString for StatusError {
273 fn to_string(&self) -> String {
274 match self {
275 StatusError::IO(e) => e.to_string(),
276 StatusError::Path(e) => e.to_string(),
277 StatusError::Pattern(e) => e.to_string(),
278 }
279 }
280 }
281
282 pub struct Status<'a, M: Matcher + Sync> {
283 dmap: &'a DirstateMap,
284 matcher: &'a M,
285 root_dir: PathBuf,
286 options: StatusOptions,
287 ignore_fn: IgnoreFnType<'a>,
288 }
289
290 impl<'a, M> Status<'a, M>
291 where
292 M: Matcher + Sync,
293 {
294 pub fn new(
323 dmap: &'a DirstateMap,
295 dmap: &'a DirstateMap,
296 matcher: &'a M,
297 root_dir: PathBuf,
298 ignore_files: Vec<PathBuf>,
299 options: StatusOptions,
300 ) -> StatusResult<(Self, Vec<PatternFileWarning>)> {
301 // Needs to outlive `dir_ignore_fn` since it's captured.
302
303 let (ignore_fn, warnings): (IgnoreFnType, _) =
304 if options.list_ignored || options.list_unknown {
305 get_ignore_function(ignore_files, &root_dir)?
306 } else {
307 (Box::new(|&_| true), vec![])
308 };
309
310 Ok((
311 Self {
312 dmap,
313 matcher,
314 root_dir,
315 options,
316 ignore_fn,
317 },
318 warnings,
319 ))
320 }
321
322 pub fn is_ignored(&self, path: impl AsRef<HgPath>) -> bool {
323 (self.ignore_fn)(path.as_ref())
324 }
325
326 /// Is the path or one of its ancestors ignored?
327 pub fn dir_ignore(&self, dir: impl AsRef<HgPath>) -> bool {
328 // Only involve ignore mechanism if we're listing unknowns or ignored.
329 if self.options.list_ignored || self.options.list_unknown {
330 if self.is_ignored(&dir) {
331 true
332 } else {
333 for p in find_dirs(dir.as_ref()) {
334 if self.is_ignored(p) {
335 return true;
336 }
337 }
338 false
339 }
340 } else {
341 true
342 }
343 }
344
345 /// Get stat data about the files explicitly specified by match.
346 /// TODO subrepos
347 #[timed]
348 pub fn walk_explicit(
349 &self,
350 traversed_sender: crossbeam::Sender<HgPathBuf>,
351 ) -> (
352 Vec<(Cow<'a, HgPath>, Dispatch)>,
353 Vec<(Cow<'a, HgPath>, Dispatch)>,
354 ) {
355 self.matcher
356 .file_set()
357 .unwrap_or(&DEFAULT_WORK)
358 .par_iter()
359 .map(|&filename| -> Option<IoResult<_>> {
360 // TODO normalization
361 let normalized = filename;
362
363 let buf = match hg_path_to_path_buf(normalized) {
364 Ok(x) => x,
365 Err(e) => return Some(Err(e.into())),
366 };
367 let target = self.root_dir.join(buf);
368 let st = target.symlink_metadata();
369 let in_dmap = self.dmap.get(normalized);
370 match st {
371 Ok(meta) => {
372 let file_type = meta.file_type();
373 return if file_type.is_file() || file_type.is_symlink()
374 {
375 if let Some(entry) = in_dmap {
376 return Some(Ok((
377 Cow::Borrowed(normalized),
378 dispatch_found(
379 &normalized,
380 *entry,
381 HgMetadata::from_metadata(meta),
382 &self.dmap.copy_map,
383 self.options,
384 ),
385 )));
386 }
387 Some(Ok((
388 Cow::Borrowed(normalized),
389 Dispatch::Unknown,
390 )))
391 } else if file_type.is_dir() {
392 if self.options.collect_traversed_dirs {
393 traversed_sender
394 .send(normalized.to_owned())
395 .expect("receiver should outlive sender");
396 }
397 Some(Ok((
398 Cow::Borrowed(normalized),
399 Dispatch::Directory {
400 was_file: in_dmap.is_some(),
401 },
402 )))
403 } else {
404 Some(Ok((
405 Cow::Borrowed(normalized),
406 Dispatch::Bad(BadMatch::BadType(
407 // TODO do more than unknown
408 // Support for all `BadType` variant
409 // varies greatly between platforms.
410 // So far, no tests check the type and
411 // this should be good enough for most
412 // users.
413 BadType::Unknown,
414 )),
415 )))
416 };
417 }
418 Err(_) => {
419 if let Some(entry) = in_dmap {
420 return Some(Ok((
421 Cow::Borrowed(normalized),
422 dispatch_missing(entry.state),
423 )));
424 }
425 }
426 };
427 None
428 })
429 .flatten()
430 .filter_map(Result::ok)
431 .partition(|(_, dispatch)| match dispatch {
432 Dispatch::Directory { .. } => true,
433 _ => false,
434 })
435 }
436
437 /// Walk the working directory recursively to look for changes compared to
438 /// the current `DirstateMap`.
439 ///
440 /// This takes a mutable reference to the results to account for the
441 /// `extend` in timings
442 #[timed]
443 pub fn traverse(
444 &self,
445 path: impl AsRef<HgPath>,
446 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
447 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
448 traversed_sender: crossbeam::Sender<HgPathBuf>,
449 ) -> IoResult<()> {
450 // The traversal is done in parallel, so use a channel to gather
451 // entries. `crossbeam::Sender` is `Sync`, while `mpsc::Sender`
452 // is not.
453 let (files_transmitter, files_receiver) =
454 crossbeam::channel::unbounded();
455
456 self.traverse_dir(
457 &files_transmitter,
458 path,
459 &old_results,
460 traversed_sender,
461 )?;
462
463 // Disconnect the channel so the receiver stops waiting
464 drop(files_transmitter);
465
466 // TODO don't collect. Find a way of replicating the behavior of
467 // `itertools::process_results`, but for `rayon::ParallelIterator`
468 let new_results: IoResult<Vec<(Cow<HgPath>, Dispatch)>> =
469 files_receiver
470 .into_iter()
471 .map(|item| {
472 let (f, d) = item?;
473 Ok((Cow::Owned(f), d))
474 })
475 .collect();
476
477 results.par_extend(new_results?);
478
479 Ok(())
480 }
481
482 /// Dispatch a single entry (file, folder, symlink...) found during
483 /// `traverse`. If the entry is a folder that needs to be traversed, it
484 /// will be handled in a separate thread.
485 fn handle_traversed_entry<'b>(
486 &'a self,
487 scope: &rayon::Scope<'b>,
488 files_sender: &'b crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
324 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
489 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
325 ignore_fn: &'a IgnoreFnType,
326 dir_ignore_fn: &'a IgnoreFnType,
327 options: StatusOptions,
328 filename: HgPathBuf,
490 filename: HgPathBuf,
329 dir_entry: DirEntry,
491 dir_entry: DirEntry,
330 traversed_sender: crossbeam::Sender<HgPathBuf>,
492 traversed_sender: crossbeam::Sender<HgPathBuf>,
331 ) -> IoResult<()> {
493 ) -> IoResult<()>
494 where
495 'a: 'b,
496 {
332 let file_type = dir_entry.file_type()?;
497 let file_type = dir_entry.file_type()?;
333 let entry_option = dmap.get(&filename);
498 let entry_option = self.dmap.get(&filename);
334
499
335 if filename.as_bytes() == b".hg" {
500 if filename.as_bytes() == b".hg" {
336 // Could be a directory or a symlink
501 // Could be a directory or a symlink
@@ -338,23 +503,19 b" fn handle_traversed_entry<'a>("
338 }
503 }
339
504
340 if file_type.is_dir() {
505 if file_type.is_dir() {
341 handle_traversed_dir(
506 self.handle_traversed_dir(
342 scope,
507 scope,
343 files_sender,
508 files_sender,
344 matcher,
345 root_dir,
346 dmap,
347 old_results,
509 old_results,
348 ignore_fn,
349 dir_ignore_fn,
350 options,
351 entry_option,
510 entry_option,
352 filename,
511 filename,
353 traversed_sender,
512 traversed_sender,
354 );
513 );
355 } else if file_type.is_file() || file_type.is_symlink() {
514 } else if file_type.is_file() || file_type.is_symlink() {
356 if let Some(entry) = entry_option {
515 if let Some(entry) = entry_option {
357 if matcher.matches_everything() || matcher.matches(&filename) {
516 if self.matcher.matches_everything()
517 || self.matcher.matches(&filename)
518 {
358 let metadata = dir_entry.metadata()?;
519 let metadata = dir_entry.metadata()?;
359 files_sender
520 files_sender
360 .send(Ok((
521 .send(Ok((
@@ -363,38 +524,45 b" fn handle_traversed_entry<'a>("
363 &filename,
524 &filename,
364 *entry,
525 *entry,
365 HgMetadata::from_metadata(metadata),
526 HgMetadata::from_metadata(metadata),
366 &dmap.copy_map,
527 &self.dmap.copy_map,
367 options,
528 self.options,
368 ),
529 ),
369 )))
530 )))
370 .unwrap();
531 .unwrap();
371 }
532 }
372 } else if (matcher.matches_everything() || matcher.matches(&filename))
533 } else if (self.matcher.matches_everything()
373 && !ignore_fn(&filename)
534 || self.matcher.matches(&filename))
535 && !self.is_ignored(&filename)
374 {
536 {
375 if (options.list_ignored || matcher.exact_match(&filename))
537 if (self.options.list_ignored
376 && dir_ignore_fn(&filename)
538 || self.matcher.exact_match(&filename))
539 && self.dir_ignore(&filename)
377 {
540 {
378 if options.list_ignored {
541 if self.options.list_ignored {
379 files_sender
542 files_sender
380 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
543 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
381 .unwrap();
544 .unwrap();
382 }
545 }
383 } else if options.list_unknown {
546 } else if self.options.list_unknown {
384 files_sender
547 files_sender
385 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
548 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
386 .unwrap();
549 .unwrap();
387 }
550 }
388 } else if ignore_fn(&filename) && options.list_ignored {
551 } else if self.is_ignored(&filename) && self.options.list_ignored {
389 files_sender
552 files_sender
390 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
553 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
391 .unwrap();
554 .unwrap();
392 }
555 }
393 } else if let Some(entry) = entry_option {
556 } else if let Some(entry) = entry_option {
394 // Used to be a file or a folder, now something else.
557 // Used to be a file or a folder, now something else.
395 if matcher.matches_everything() || matcher.matches(&filename) {
558 if self.matcher.matches_everything()
559 || self.matcher.matches(&filename)
560 {
396 files_sender
561 files_sender
397 .send(Ok((filename.to_owned(), dispatch_missing(entry.state))))
562 .send(Ok((
563 filename.to_owned(),
564 dispatch_missing(entry.state),
565 )))
398 .unwrap();
566 .unwrap();
399 }
567 }
400 }
568 }
@@ -403,25 +571,24 b" fn handle_traversed_entry<'a>("
403 }
571 }
404
572
405 /// A directory was found in the filesystem and needs to be traversed
573 /// A directory was found in the filesystem and needs to be traversed
406 fn handle_traversed_dir<'a>(
574 fn handle_traversed_dir<'b>(
407 scope: &rayon::Scope<'a>,
575 &'a self,
408 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
576 scope: &rayon::Scope<'b>,
409 matcher: &'a (impl Matcher + Sync),
577 files_sender: &'b crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
410 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
411 dmap: &'a DirstateMap,
412 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
578 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
413 ignore_fn: &'a IgnoreFnType,
414 dir_ignore_fn: &'a IgnoreFnType,
415 options: StatusOptions,
416 entry_option: Option<&'a DirstateEntry>,
579 entry_option: Option<&'a DirstateEntry>,
417 directory: HgPathBuf,
580 directory: HgPathBuf,
418 traversed_sender: crossbeam::Sender<HgPathBuf>,
581 traversed_sender: crossbeam::Sender<HgPathBuf>,
419 ) {
582 ) where
583 'a: 'b,
584 {
420 scope.spawn(move |_| {
585 scope.spawn(move |_| {
421 // Nested `if` until `rust-lang/rust#53668` is stable
586 // Nested `if` until `rust-lang/rust#53668` is stable
422 if let Some(entry) = entry_option {
587 if let Some(entry) = entry_option {
423 // Used to be a file, is now a folder
588 // Used to be a file, is now a folder
424 if matcher.matches_everything() || matcher.matches(&directory) {
589 if self.matcher.matches_everything()
590 || self.matcher.matches(&directory)
591 {
425 files_sender
592 files_sender
426 .send(Ok((
593 .send(Ok((
427 directory.to_owned(),
594 directory.to_owned(),
@@ -431,17 +598,11 b" fn handle_traversed_dir<'a>("
431 }
598 }
432 }
599 }
433 // Do we need to traverse it?
600 // Do we need to traverse it?
434 if !ignore_fn(&directory) || options.list_ignored {
601 if !self.is_ignored(&directory) || self.options.list_ignored {
435 traverse_dir(
602 self.traverse_dir(
436 files_sender,
603 files_sender,
437 matcher,
438 root_dir,
439 dmap,
440 directory,
604 directory,
441 &old_results,
605 &old_results,
442 ignore_fn,
443 dir_ignore_fn,
444 options,
445 traversed_sender,
606 traversed_sender,
446 )
607 )
447 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
608 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
@@ -451,33 +612,28 b" fn handle_traversed_dir<'a>("
451
612
452 /// Decides whether the directory needs to be listed, and if so handles the
613 /// Decides whether the directory needs to be listed, and if so handles the
453 /// entries in a separate thread.
614 /// entries in a separate thread.
454 fn traverse_dir<'a>(
615 fn traverse_dir(
616 &self,
455 files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
617 files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
456 matcher: &'a (impl Matcher + Sync),
457 root_dir: impl AsRef<Path> + Sync + Send + Copy,
458 dmap: &'a DirstateMap,
459 directory: impl AsRef<HgPath>,
618 directory: impl AsRef<HgPath>,
460 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
619 old_results: &FastHashMap<Cow<HgPath>, Dispatch>,
461 ignore_fn: &IgnoreFnType,
462 dir_ignore_fn: &IgnoreFnType,
463 options: StatusOptions,
464 traversed_sender: crossbeam::Sender<HgPathBuf>,
620 traversed_sender: crossbeam::Sender<HgPathBuf>,
465 ) -> IoResult<()> {
621 ) -> IoResult<()> {
466 let directory = directory.as_ref();
622 let directory = directory.as_ref();
467
623
468 if options.collect_traversed_dirs {
624 if self.options.collect_traversed_dirs {
469 traversed_sender
625 traversed_sender
470 .send(directory.to_owned())
626 .send(directory.to_owned())
471 .expect("receiver should outlive sender");
627 .expect("receiver should outlive sender");
472 }
628 }
473
629
474 let visit_entries = match matcher.visit_children_set(directory) {
630 let visit_entries = match self.matcher.visit_children_set(directory) {
475 VisitChildrenSet::Empty => return Ok(()),
631 VisitChildrenSet::Empty => return Ok(()),
476 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
632 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
477 VisitChildrenSet::Set(set) => Some(set),
633 VisitChildrenSet::Set(set) => Some(set),
478 };
634 };
479 let buf = hg_path_to_path_buf(directory)?;
635 let buf = hg_path_to_path_buf(directory)?;
480 let dir_path = root_dir.as_ref().join(buf);
636 let dir_path = self.root_dir.join(buf);
481
637
482 let skip_dot_hg = !directory.as_bytes().is_empty();
638 let skip_dot_hg = !directory.as_bytes().is_empty();
483 let entries = match list_directory(dir_path, skip_dot_hg) {
639 let entries = match list_directory(dir_path, skip_dot_hg) {
@@ -487,8 +643,9 b" fn traverse_dir<'a>("
487 .send(Ok((
643 .send(Ok((
488 directory.to_owned(),
644 directory.to_owned(),
489 Dispatch::Bad(BadMatch::OsError(
645 Dispatch::Bad(BadMatch::OsError(
490 // Unwrapping here is OK because the error always
646 // Unwrapping here is OK because the error
491 // is a real os error
647 // always is a
648 // real os error
492 e.raw_os_error().unwrap(),
649 e.raw_os_error().unwrap(),
493 )),
650 )),
494 )))
651 )))
@@ -515,16 +672,10 b" fn traverse_dir<'a>("
515 };
672 };
516
673
517 if !old_results.contains_key(filename.deref()) {
674 if !old_results.contains_key(filename.deref()) {
518 handle_traversed_entry(
675 self.handle_traversed_entry(
519 scope,
676 scope,
520 files_sender,
677 files_sender,
521 matcher,
522 root_dir,
523 dmap,
524 old_results,
678 old_results,
525 ignore_fn,
526 dir_ignore_fn,
527 options,
528 filename,
679 filename,
529 dir_entry,
680 dir_entry,
530 traversed_sender.clone(),
681 traversed_sender.clone(),
@@ -535,54 +686,78 b" fn traverse_dir<'a>("
535 })
686 })
536 }
687 }
537
688
538 /// Walk the working directory recursively to look for changes compared to the
689 /// This takes a mutable reference to the results to account for the
539 /// current `DirstateMap`.
690 /// `extend` in timings
540 ///
541 /// This takes a mutable reference to the results to account for the `extend`
542 /// in timings
543 #[timed]
691 #[timed]
544 fn traverse<'a>(
692 fn handle_unknowns(
545 matcher: &'a (impl Matcher + Sync),
693 &self,
546 root_dir: impl AsRef<Path> + Sync + Send + Copy,
547 dmap: &'a DirstateMap,
548 path: impl AsRef<HgPath>,
549 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
550 ignore_fn: &IgnoreFnType,
551 dir_ignore_fn: &IgnoreFnType,
552 options: StatusOptions,
553 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
694 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
554 traversed_sender: crossbeam::Sender<HgPathBuf>,
555 ) -> IoResult<()> {
695 ) -> IoResult<()> {
556 let root_dir = root_dir.as_ref();
696 let to_visit: Vec<(&HgPath, &DirstateEntry)> =
697 if results.is_empty() && self.matcher.matches_everything() {
698 self.dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
699 } else {
700 // Only convert to a hashmap if needed.
701 let old_results: FastHashMap<_, _> =
702 results.iter().cloned().collect();
703 self.dmap
704 .iter()
705 .filter_map(move |(f, e)| {
706 if !old_results.contains_key(f.deref())
707 && self.matcher.matches(f)
708 {
709 Some((f.deref(), e))
710 } else {
711 None
712 }
713 })
714 .collect()
715 };
557
716
558 // The traversal is done in parallel, so use a channel to gather entries.
717 // We walked all dirs under the roots that weren't ignored, and
559 // `crossbeam::Sender` is `Sync`, while `mpsc::Sender` is not.
718 // everything that matched was stat'ed and is already in results.
560 let (files_transmitter, files_receiver) = crossbeam::channel::unbounded();
719 // The rest must thus be ignored or under a symlink.
561
720 let path_auditor = PathAuditor::new(&self.root_dir);
562 traverse_dir(
563 &files_transmitter,
564 matcher,
565 root_dir,
566 &dmap,
567 path,
568 &old_results,
569 &ignore_fn,
570 &dir_ignore_fn,
571 options,
572 traversed_sender,
573 )?;
574
575 // Disconnect the channel so the receiver stops waiting
576 drop(files_transmitter);
577
721
578 // TODO don't collect. Find a way of replicating the behavior of
722 // TODO don't collect. Find a way of replicating the behavior of
579 // `itertools::process_results`, but for `rayon::ParallelIterator`
723 // `itertools::process_results`, but for `rayon::ParallelIterator`
580 let new_results: IoResult<Vec<(Cow<'a, HgPath>, Dispatch)>> =
724 let new_results: IoResult<Vec<_>> = to_visit
581 files_receiver
725 .into_par_iter()
582 .into_iter()
726 .filter_map(|(filename, entry)| -> Option<IoResult<_>> {
583 .map(|item| {
727 // Report ignored items in the dmap as long as they are not
584 let (f, d) = item?;
728 // under a symlink directory.
585 Ok((Cow::Owned(f), d))
729 if path_auditor.check(filename) {
730 // TODO normalize for case-insensitive filesystems
731 let buf = match hg_path_to_path_buf(filename) {
732 Ok(x) => x,
733 Err(e) => return Some(Err(e.into())),
734 };
735 Some(Ok((
736 Cow::Borrowed(filename),
737 match self.root_dir.join(&buf).symlink_metadata() {
738 // File was just ignored, no links, and exists
739 Ok(meta) => {
740 let metadata = HgMetadata::from_metadata(meta);
741 dispatch_found(
742 filename,
743 *entry,
744 metadata,
745 &self.dmap.copy_map,
746 self.options,
747 )
748 }
749 // File doesn't exist
750 Err(_) => dispatch_missing(entry.state),
751 },
752 )))
753 } else {
754 // It's either missing or under a symlink directory which
755 // we, in this case, report as missing.
756 Some(Ok((
757 Cow::Borrowed(filename),
758 dispatch_missing(entry.state),
759 )))
760 }
586 })
761 })
587 .collect();
762 .collect();
588
763
@@ -591,32 +766,38 b" fn traverse<'a>("
591 Ok(())
766 Ok(())
592 }
767 }
593
768
594 /// Stat all entries in the `DirstateMap` and mark them for dispatch.
769 /// This takes a mutable reference to the results to account for the
595 fn stat_dmap_entries(
770 /// `extend` in timings
596 dmap: &DirstateMap,
771 #[timed]
597 root_dir: impl AsRef<Path> + Sync + Send,
772 fn extend_from_dmap(
598 options: StatusOptions,
773 &self,
599 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
774 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
600 dmap.par_iter().map(move |(filename, entry)| {
775 ) {
776 results.par_extend(self.dmap.par_iter().flat_map(
777 move |(filename, entry)| {
601 let filename: &HgPath = filename;
778 let filename: &HgPath = filename;
602 let filename_as_path = hg_path_to_path_buf(filename)?;
779 let filename_as_path = hg_path_to_path_buf(filename)?;
603 let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
780 let meta =
781 self.root_dir.join(filename_as_path).symlink_metadata();
604
782
605 match meta {
783 match meta {
606 Ok(ref m)
784 Ok(ref m)
607 if !(m.file_type().is_file()
785 if !(m.file_type().is_file()
608 || m.file_type().is_symlink()) =>
786 || m.file_type().is_symlink()) =>
609 {
787 {
610 Ok((filename, dispatch_missing(entry.state)))
788 Ok((
789 Cow::Borrowed(filename),
790 dispatch_missing(entry.state),
791 ))
611 }
792 }
612 Ok(m) => Ok((
793 Ok(m) => Ok((
613 filename,
794 Cow::Borrowed(filename),
614 dispatch_found(
795 dispatch_found(
615 filename,
796 filename,
616 *entry,
797 *entry,
617 HgMetadata::from_metadata(m),
798 HgMetadata::from_metadata(m),
618 &dmap.copy_map,
799 &self.dmap.copy_map,
619 options,
800 self.options,
620 ),
801 ),
621 )),
802 )),
622 Err(ref e)
803 Err(ref e)
@@ -625,43 +806,19 b' fn stat_dmap_entries('
625 {
806 {
626 // Rust does not yet have an `ErrorKind` for
807 // Rust does not yet have an `ErrorKind` for
627 // `NotADirectory` (errno 20)
808 // `NotADirectory` (errno 20)
628 // It happens if the dirstate contains `foo/bar` and
809 // It happens if the dirstate contains `foo/bar`
629 // foo is not a directory
810 // and foo is not a
630 Ok((filename, dispatch_missing(entry.state)))
811 // directory
812 Ok((
813 Cow::Borrowed(filename),
814 dispatch_missing(entry.state),
815 ))
631 }
816 }
632 Err(e) => Err(e),
817 Err(e) => Err(e),
633 }
818 }
634 })
819 },
820 ));
635 }
821 }
636
637 /// This takes a mutable reference to the results to account for the `extend`
638 /// in timings
639 #[timed]
640 fn extend_from_dmap<'a>(
641 dmap: &'a DirstateMap,
642 root_dir: impl AsRef<Path> + Sync + Send,
643 options: StatusOptions,
644 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
645 ) {
646 results.par_extend(
647 stat_dmap_entries(dmap, root_dir, options)
648 .flatten()
649 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch)),
650 );
651 }
652
653 #[derive(Debug)]
654 pub struct DirstateStatus<'a> {
655 pub modified: Vec<Cow<'a, HgPath>>,
656 pub added: Vec<Cow<'a, HgPath>>,
657 pub removed: Vec<Cow<'a, HgPath>>,
658 pub deleted: Vec<Cow<'a, HgPath>>,
659 pub clean: Vec<Cow<'a, HgPath>>,
660 pub ignored: Vec<Cow<'a, HgPath>>,
661 pub unknown: Vec<Cow<'a, HgPath>>,
662 pub bad: Vec<(Cow<'a, HgPath>, BadMatch)>,
663 /// Only filled if `collect_traversed_dirs` is `true`
664 pub traversed: Vec<HgPathBuf>,
665 }
822 }
666
823
667 #[timed]
824 #[timed]
@@ -711,189 +868,29 b" fn build_response<'a>("
711 )
868 )
712 }
869 }
713
870
714 #[derive(Debug)]
715 pub enum StatusError {
716 IO(std::io::Error),
717 Path(HgPathError),
718 Pattern(PatternError),
719 }
720
721 pub type StatusResult<T> = Result<T, StatusError>;
722
723 impl From<PatternError> for StatusError {
724 fn from(e: PatternError) -> Self {
725 StatusError::Pattern(e)
726 }
727 }
728 impl From<HgPathError> for StatusError {
729 fn from(e: HgPathError) -> Self {
730 StatusError::Path(e)
731 }
732 }
733 impl From<std::io::Error> for StatusError {
734 fn from(e: std::io::Error) -> Self {
735 StatusError::IO(e)
736 }
737 }
738
739 impl ToString for StatusError {
740 fn to_string(&self) -> String {
741 match self {
742 StatusError::IO(e) => e.to_string(),
743 StatusError::Path(e) => e.to_string(),
744 StatusError::Pattern(e) => e.to_string(),
745 }
746 }
747 }
748
749 /// This takes a mutable reference to the results to account for the `extend`
750 /// in timings
751 #[timed]
752 fn handle_unknowns<'a>(
753 dmap: &'a DirstateMap,
754 matcher: &(impl Matcher + Sync),
755 root_dir: impl AsRef<Path> + Sync + Send + Copy,
756 options: StatusOptions,
757 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
758 ) -> IoResult<()> {
759 let to_visit: Vec<(&HgPath, &DirstateEntry)> = if results.is_empty()
760 && matcher.matches_everything()
761 {
762 dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
763 } else {
764 // Only convert to a hashmap if needed.
765 let old_results: FastHashMap<_, _> = results.iter().cloned().collect();
766 dmap.iter()
767 .filter_map(move |(f, e)| {
768 if !old_results.contains_key(f.deref()) && matcher.matches(f) {
769 Some((f.deref(), e))
770 } else {
771 None
772 }
773 })
774 .collect()
775 };
776
777 // We walked all dirs under the roots that weren't ignored, and
778 // everything that matched was stat'ed and is already in results.
779 // The rest must thus be ignored or under a symlink.
780 let path_auditor = PathAuditor::new(root_dir);
781
782 // TODO don't collect. Find a way of replicating the behavior of
783 // `itertools::process_results`, but for `rayon::ParallelIterator`
784 let new_results: IoResult<Vec<_>> = to_visit
785 .into_par_iter()
786 .filter_map(|(filename, entry)| -> Option<IoResult<_>> {
787 // Report ignored items in the dmap as long as they are not
788 // under a symlink directory.
789 if path_auditor.check(filename) {
790 // TODO normalize for case-insensitive filesystems
791 let buf = match hg_path_to_path_buf(filename) {
792 Ok(x) => x,
793 Err(e) => return Some(Err(e.into())),
794 };
795 Some(Ok((
796 Cow::Borrowed(filename),
797 match root_dir.as_ref().join(&buf).symlink_metadata() {
798 // File was just ignored, no links, and exists
799 Ok(meta) => {
800 let metadata = HgMetadata::from_metadata(meta);
801 dispatch_found(
802 filename,
803 *entry,
804 metadata,
805 &dmap.copy_map,
806 options,
807 )
808 }
809 // File doesn't exist
810 Err(_) => dispatch_missing(entry.state),
811 },
812 )))
813 } else {
814 // It's either missing or under a symlink directory which
815 // we, in this case, report as missing.
816 Some(Ok((
817 Cow::Borrowed(filename),
818 dispatch_missing(entry.state),
819 )))
820 }
821 })
822 .collect();
823
824 results.par_extend(new_results?);
825
826 Ok(())
827 }
828
829 /// Get the status of files in the working directory.
871 /// Get the status of files in the working directory.
830 ///
872 ///
831 /// This is the current entry-point for `hg-core` and is realistically unusable
873 /// This is the current entry-point for `hg-core` and is realistically unusable
832 /// outside of a Python context because its arguments need to provide a lot of
874 /// outside of a Python context because its arguments need to provide a lot of
833 /// information that will not be necessary in the future.
875 /// information that will not be necessary in the future.
834 #[timed]
876 #[timed]
835 pub fn status<'a: 'c, 'b: 'c, 'c>(
877 pub fn status<'a>(
836 dmap: &'a DirstateMap,
878 dmap: &'a DirstateMap,
837 matcher: &'b (impl Matcher + Sync),
879 matcher: &'a (impl Matcher + Sync),
838 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'c,
880 root_dir: PathBuf,
839 ignore_files: Vec<PathBuf>,
881 ignore_files: Vec<PathBuf>,
840 options: StatusOptions,
882 options: StatusOptions,
841 ) -> StatusResult<(
883 ) -> StatusResult<(
842 (Vec<Cow<'c, HgPath>>, DirstateStatus<'c>),
884 (Vec<Cow<'a, HgPath>>, DirstateStatus<'a>),
843 Vec<PatternFileWarning>,
885 Vec<PatternFileWarning>,
844 )> {
886 )> {
845 // Needs to outlive `dir_ignore_fn` since it's captured.
887 let (traversed_sender, traversed_receiver) =
846 let ignore_fn: IgnoreFnType;
888 crossbeam::channel::unbounded();
847
889 let (st, warnings) =
848 // Only involve real ignore mechanism if we're listing unknowns or ignored.
890 Status::new(dmap, matcher, root_dir, ignore_files, options)?;
849 let (dir_ignore_fn, warnings): (IgnoreFnType, _) = if options.list_ignored
850 || options.list_unknown
851 {
852 let (ignore, warnings) = get_ignore_function(ignore_files, root_dir)?;
853
854 ignore_fn = ignore;
855 let dir_ignore_fn = Box::new(|dir: &_| {
856 // Is the path or one of its ancestors ignored?
857 if ignore_fn(dir) {
858 true
859 } else {
860 for p in find_dirs(dir) {
861 if ignore_fn(p) {
862 return true;
863 }
864 }
865 false
866 }
867 });
868 (dir_ignore_fn, warnings)
869 } else {
870 ignore_fn = Box::new(|&_| true);
871 (Box::new(|&_| true), vec![])
872 };
873
874 let files = matcher.file_set();
875
876 // `crossbeam::Sender` is `Sync`, while `mpsc::Sender` is not.
877 let (traversed_sender, traversed_recv) = crossbeam::channel::unbounded();
878
891
879 // Step 1: check the files explicitly mentioned by the user
892 // Step 1: check the files explicitly mentioned by the user
880 let explicit = walk_explicit(
893 let (work, mut results) = st.walk_explicit(traversed_sender.clone());
881 files,
882 &dmap,
883 root_dir,
884 options,
885 traversed_sender.clone(),
886 );
887
888 // Collect results into a `Vec` because we do very few lookups in most
889 // cases.
890 let (work, mut results): (Vec<_>, Vec<_>) = explicit
891 .filter_map(Result::ok)
892 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch))
893 .partition(|(_, dispatch)| match dispatch {
894 Dispatch::Directory { .. } => true,
895 _ => false,
896 });
897
894
898 if !work.is_empty() {
895 if !work.is_empty() {
899 // Hashmaps are quite a bit slower to build than vecs, so only build it
896 // Hashmaps are quite a bit slower to build than vecs, so only build it
@@ -909,17 +906,11 b" pub fn status<'a: 'c, 'b: 'c, 'c>("
909 results.push((dir.to_owned(), Dispatch::Removed));
906 results.push((dir.to_owned(), Dispatch::Removed));
910 }
907 }
911 if options.list_ignored
908 if options.list_ignored
912 || options.list_unknown && !dir_ignore_fn(&dir)
909 || options.list_unknown && !st.dir_ignore(&dir)
913 {
910 {
914 traverse(
911 st.traverse(
915 matcher,
916 root_dir,
917 &dmap,
918 &dir,
912 &dir,
919 &old_results,
913 &old_results,
920 &ignore_fn,
921 &dir_ignore_fn,
922 options,
923 &mut results,
914 &mut results,
924 traversed_sender.clone(),
915 traversed_sender.clone(),
925 )?;
916 )?;
@@ -937,17 +928,16 b" pub fn status<'a: 'c, 'b: 'c, 'c>("
937 // symlink directory.
928 // symlink directory.
938
929
939 if options.list_unknown {
930 if options.list_unknown {
940 handle_unknowns(dmap, matcher, root_dir, options, &mut results)?;
931 st.handle_unknowns(&mut results)?;
941 } else {
932 } else {
942 // We may not have walked the full directory tree above, so stat
933 // We may not have walked the full directory tree above, so stat
943 // and check everything we missed.
934 // and check everything we missed.
944 extend_from_dmap(&dmap, root_dir, options, &mut results);
935 st.extend_from_dmap(&mut results);
945 }
936 }
946 }
937 }
947
938
948 // Close the channel
949 drop(traversed_sender);
939 drop(traversed_sender);
950 let traversed_dirs = traversed_recv.into_iter().collect();
940 let traversed = traversed_receiver.into_iter().collect();
951
941
952 Ok((build_response(results, traversed_dirs), warnings))
942 Ok((build_response(results, traversed), warnings))
953 }
943 }
@@ -127,7 +127,7 b' pub fn status_wrapper('
127 let ((lookup, status_res), warnings) = status(
127 let ((lookup, status_res), warnings) = status(
128 &dmap,
128 &dmap,
129 &matcher,
129 &matcher,
130 &root_dir,
130 root_dir.to_path_buf(),
131 ignore_files,
131 ignore_files,
132 StatusOptions {
132 StatusOptions {
133 check_exec,
133 check_exec,
@@ -164,7 +164,7 b' pub fn status_wrapper('
164 let ((lookup, status_res), warnings) = status(
164 let ((lookup, status_res), warnings) = status(
165 &dmap,
165 &dmap,
166 &matcher,
166 &matcher,
167 &root_dir,
167 root_dir.to_path_buf(),
168 ignore_files,
168 ignore_files,
169 StatusOptions {
169 StatusOptions {
170 check_exec,
170 check_exec,
@@ -219,7 +219,7 b' pub fn status_wrapper('
219 let ((lookup, status_res), warnings) = status(
219 let ((lookup, status_res), warnings) = status(
220 &dmap,
220 &dmap,
221 &matcher,
221 &matcher,
222 &root_dir,
222 root_dir.to_path_buf(),
223 ignore_files,
223 ignore_files,
224 StatusOptions {
224 StatusOptions {
225 check_exec,
225 check_exec,
General Comments 0
You need to be logged in to leave comments. Login now