##// 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, (1196 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,344 +230,6 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`.
316 /// If the entry is a folder that needs to be traversed, it will be handled
317 /// in a separate thread.
318 fn handle_traversed_entry<'a>(
319 scope: &rayon::Scope<'a>,
320 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
321 matcher: &'a (impl Matcher + Sync),
322 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
323 dmap: &'a DirstateMap,
324 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
325 ignore_fn: &'a IgnoreFnType,
326 dir_ignore_fn: &'a IgnoreFnType,
327 options: StatusOptions,
328 filename: HgPathBuf,
329 dir_entry: DirEntry,
330 traversed_sender: crossbeam::Sender<HgPathBuf>,
331 ) -> IoResult<()> {
332 let file_type = dir_entry.file_type()?;
333 let entry_option = dmap.get(&filename);
334
335 if filename.as_bytes() == b".hg" {
336 // Could be a directory or a symlink
337 return Ok(());
338 }
339
340 if file_type.is_dir() {
341 handle_traversed_dir(
342 scope,
343 files_sender,
344 matcher,
345 root_dir,
346 dmap,
347 old_results,
348 ignore_fn,
349 dir_ignore_fn,
350 options,
351 entry_option,
352 filename,
353 traversed_sender,
354 );
355 } else if file_type.is_file() || file_type.is_symlink() {
356 if let Some(entry) = entry_option {
357 if matcher.matches_everything() || matcher.matches(&filename) {
358 let metadata = dir_entry.metadata()?;
359 files_sender
360 .send(Ok((
361 filename.to_owned(),
362 dispatch_found(
363 &filename,
364 *entry,
365 HgMetadata::from_metadata(metadata),
366 &dmap.copy_map,
367 options,
368 ),
369 )))
370 .unwrap();
371 }
372 } else if (matcher.matches_everything() || matcher.matches(&filename))
373 && !ignore_fn(&filename)
374 {
375 if (options.list_ignored || matcher.exact_match(&filename))
376 && dir_ignore_fn(&filename)
377 {
378 if options.list_ignored {
379 files_sender
380 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
381 .unwrap();
382 }
383 } else if options.list_unknown {
384 files_sender
385 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
386 .unwrap();
387 }
388 } else if ignore_fn(&filename) && options.list_ignored {
389 files_sender
390 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
391 .unwrap();
392 }
393 } else if let Some(entry) = entry_option {
394 // Used to be a file or a folder, now something else.
395 if matcher.matches_everything() || matcher.matches(&filename) {
396 files_sender
397 .send(Ok((filename.to_owned(), dispatch_missing(entry.state))))
398 .unwrap();
399 }
400 }
401
402 Ok(())
403 }
404
405 /// A directory was found in the filesystem and needs to be traversed
406 fn handle_traversed_dir<'a>(
407 scope: &rayon::Scope<'a>,
408 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
409 matcher: &'a (impl Matcher + Sync),
410 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
411 dmap: &'a DirstateMap,
412 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>,
417 directory: HgPathBuf,
418 traversed_sender: crossbeam::Sender<HgPathBuf>,
419 ) {
420 scope.spawn(move |_| {
421 // Nested `if` until `rust-lang/rust#53668` is stable
422 if let Some(entry) = entry_option {
423 // Used to be a file, is now a folder
424 if matcher.matches_everything() || matcher.matches(&directory) {
425 files_sender
426 .send(Ok((
427 directory.to_owned(),
428 dispatch_missing(entry.state),
429 )))
430 .unwrap();
431 }
432 }
433 // Do we need to traverse it?
434 if !ignore_fn(&directory) || options.list_ignored {
435 traverse_dir(
436 files_sender,
437 matcher,
438 root_dir,
439 dmap,
440 directory,
441 &old_results,
442 ignore_fn,
443 dir_ignore_fn,
444 options,
445 traversed_sender,
446 )
447 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
448 }
449 });
450 }
451
452 /// Decides whether the directory needs to be listed, and if so handles the
453 /// entries in a separate thread.
454 fn traverse_dir<'a>(
455 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>,
460 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
461 ignore_fn: &IgnoreFnType,
462 dir_ignore_fn: &IgnoreFnType,
463 options: StatusOptions,
464 traversed_sender: crossbeam::Sender<HgPathBuf>,
465 ) -> IoResult<()> {
466 let directory = directory.as_ref();
467
468 if options.collect_traversed_dirs {
469 traversed_sender
470 .send(directory.to_owned())
471 .expect("receiver should outlive sender");
472 }
473
474 let visit_entries = match matcher.visit_children_set(directory) {
475 VisitChildrenSet::Empty => return Ok(()),
476 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
477 VisitChildrenSet::Set(set) => Some(set),
478 };
479 let buf = hg_path_to_path_buf(directory)?;
480 let dir_path = root_dir.as_ref().join(buf);
481
482 let skip_dot_hg = !directory.as_bytes().is_empty();
483 let entries = match list_directory(dir_path, skip_dot_hg) {
484 Err(e) => match e.kind() {
485 ErrorKind::NotFound | ErrorKind::PermissionDenied => {
486 files_sender
487 .send(Ok((
488 directory.to_owned(),
489 Dispatch::Bad(BadMatch::OsError(
490 // Unwrapping here is OK because the error always
491 // is a real os error
492 e.raw_os_error().unwrap(),
493 )),
494 )))
495 .unwrap();
496 return Ok(());
497 }
498 _ => return Err(e),
499 },
500 Ok(entries) => entries,
501 };
502
503 rayon::scope(|scope| -> IoResult<()> {
504 for (filename, dir_entry) in entries {
505 if let Some(ref set) = visit_entries {
506 if !set.contains(filename.deref()) {
507 continue;
508 }
509 }
510 // TODO normalize
511 let filename = if directory.is_empty() {
512 filename.to_owned()
513 } else {
514 directory.join(&filename)
515 };
516
517 if !old_results.contains_key(filename.deref()) {
518 handle_traversed_entry(
519 scope,
520 files_sender,
521 matcher,
522 root_dir,
523 dmap,
524 old_results,
525 ignore_fn,
526 dir_ignore_fn,
527 options,
528 filename,
529 dir_entry,
530 traversed_sender.clone(),
531 )?;
532 }
533 }
534 Ok(())
535 })
536 }
537
538 /// Walk the working directory recursively to look for changes compared to the
539 /// current `DirstateMap`.
540 ///
541 /// This takes a mutable reference to the results to account for the `extend`
542 /// in timings
543 #[timed]
544 fn traverse<'a>(
545 matcher: &'a (impl Matcher + Sync),
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)>,
554 traversed_sender: crossbeam::Sender<HgPathBuf>,
555 ) -> IoResult<()> {
556 let root_dir = root_dir.as_ref();
557
558 // The traversal is done in parallel, so use a channel to gather entries.
559 // `crossbeam::Sender` is `Sync`, while `mpsc::Sender` is not.
560 let (files_transmitter, files_receiver) = crossbeam::channel::unbounded();
561
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
578 // TODO don't collect. Find a way of replicating the behavior of
579 // `itertools::process_results`, but for `rayon::ParallelIterator`
580 let new_results: IoResult<Vec<(Cow<'a, HgPath>, Dispatch)>> =
581 files_receiver
582 .into_iter()
583 .map(|item| {
584 let (f, d) = item?;
585 Ok((Cow::Owned(f), d))
586 })
587 .collect();
588
589 results.par_extend(new_results?);
590
591 Ok(())
592 }
593
594 /// Stat all entries in the `DirstateMap` and mark them for dispatch.
595 fn stat_dmap_entries(
596 dmap: &DirstateMap,
597 root_dir: impl AsRef<Path> + Sync + Send,
598 options: StatusOptions,
599 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
600 dmap.par_iter().map(move |(filename, entry)| {
601 let filename: &HgPath = filename;
602 let filename_as_path = hg_path_to_path_buf(filename)?;
603 let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
604
605 match meta {
606 Ok(ref m)
607 if !(m.file_type().is_file()
608 || m.file_type().is_symlink()) =>
609 {
610 Ok((filename, dispatch_missing(entry.state)))
611 }
612 Ok(m) => Ok((
613 filename,
614 dispatch_found(
615 filename,
616 *entry,
617 HgMetadata::from_metadata(m),
618 &dmap.copy_map,
619 options,
620 ),
621 )),
622 Err(ref e)
623 if e.kind() == ErrorKind::NotFound
624 || e.raw_os_error() == Some(20) =>
625 {
626 // Rust does not yet have an `ErrorKind` for
627 // `NotADirectory` (errno 20)
628 // It happens if the dirstate contains `foo/bar` and
629 // foo is not a directory
630 Ok((filename, dispatch_missing(entry.state)))
631 }
632 Err(e) => Err(e),
633 }
634 })
635 }
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)]
233 #[derive(Debug)]
654 pub struct DirstateStatus<'a> {
234 pub struct DirstateStatus<'a> {
655 pub modified: Vec<Cow<'a, HgPath>>,
235 pub modified: Vec<Cow<'a, HgPath>>,
@@ -664,6 +244,583 b" pub struct DirstateStatus<'a> {"
664 pub traversed: Vec<HgPathBuf>,
244 pub traversed: Vec<HgPathBuf>,
665 }
245 }
666
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(
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)>>,
489 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
490 filename: HgPathBuf,
491 dir_entry: DirEntry,
492 traversed_sender: crossbeam::Sender<HgPathBuf>,
493 ) -> IoResult<()>
494 where
495 'a: 'b,
496 {
497 let file_type = dir_entry.file_type()?;
498 let entry_option = self.dmap.get(&filename);
499
500 if filename.as_bytes() == b".hg" {
501 // Could be a directory or a symlink
502 return Ok(());
503 }
504
505 if file_type.is_dir() {
506 self.handle_traversed_dir(
507 scope,
508 files_sender,
509 old_results,
510 entry_option,
511 filename,
512 traversed_sender,
513 );
514 } else if file_type.is_file() || file_type.is_symlink() {
515 if let Some(entry) = entry_option {
516 if self.matcher.matches_everything()
517 || self.matcher.matches(&filename)
518 {
519 let metadata = dir_entry.metadata()?;
520 files_sender
521 .send(Ok((
522 filename.to_owned(),
523 dispatch_found(
524 &filename,
525 *entry,
526 HgMetadata::from_metadata(metadata),
527 &self.dmap.copy_map,
528 self.options,
529 ),
530 )))
531 .unwrap();
532 }
533 } else if (self.matcher.matches_everything()
534 || self.matcher.matches(&filename))
535 && !self.is_ignored(&filename)
536 {
537 if (self.options.list_ignored
538 || self.matcher.exact_match(&filename))
539 && self.dir_ignore(&filename)
540 {
541 if self.options.list_ignored {
542 files_sender
543 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
544 .unwrap();
545 }
546 } else if self.options.list_unknown {
547 files_sender
548 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
549 .unwrap();
550 }
551 } else if self.is_ignored(&filename) && self.options.list_ignored {
552 files_sender
553 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
554 .unwrap();
555 }
556 } else if let Some(entry) = entry_option {
557 // Used to be a file or a folder, now something else.
558 if self.matcher.matches_everything()
559 || self.matcher.matches(&filename)
560 {
561 files_sender
562 .send(Ok((
563 filename.to_owned(),
564 dispatch_missing(entry.state),
565 )))
566 .unwrap();
567 }
568 }
569
570 Ok(())
571 }
572
573 /// A directory was found in the filesystem and needs to be traversed
574 fn handle_traversed_dir<'b>(
575 &'a self,
576 scope: &rayon::Scope<'b>,
577 files_sender: &'b crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
578 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
579 entry_option: Option<&'a DirstateEntry>,
580 directory: HgPathBuf,
581 traversed_sender: crossbeam::Sender<HgPathBuf>,
582 ) where
583 'a: 'b,
584 {
585 scope.spawn(move |_| {
586 // Nested `if` until `rust-lang/rust#53668` is stable
587 if let Some(entry) = entry_option {
588 // Used to be a file, is now a folder
589 if self.matcher.matches_everything()
590 || self.matcher.matches(&directory)
591 {
592 files_sender
593 .send(Ok((
594 directory.to_owned(),
595 dispatch_missing(entry.state),
596 )))
597 .unwrap();
598 }
599 }
600 // Do we need to traverse it?
601 if !self.is_ignored(&directory) || self.options.list_ignored {
602 self.traverse_dir(
603 files_sender,
604 directory,
605 &old_results,
606 traversed_sender,
607 )
608 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
609 }
610 });
611 }
612
613 /// Decides whether the directory needs to be listed, and if so handles the
614 /// entries in a separate thread.
615 fn traverse_dir(
616 &self,
617 files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
618 directory: impl AsRef<HgPath>,
619 old_results: &FastHashMap<Cow<HgPath>, Dispatch>,
620 traversed_sender: crossbeam::Sender<HgPathBuf>,
621 ) -> IoResult<()> {
622 let directory = directory.as_ref();
623
624 if self.options.collect_traversed_dirs {
625 traversed_sender
626 .send(directory.to_owned())
627 .expect("receiver should outlive sender");
628 }
629
630 let visit_entries = match self.matcher.visit_children_set(directory) {
631 VisitChildrenSet::Empty => return Ok(()),
632 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
633 VisitChildrenSet::Set(set) => Some(set),
634 };
635 let buf = hg_path_to_path_buf(directory)?;
636 let dir_path = self.root_dir.join(buf);
637
638 let skip_dot_hg = !directory.as_bytes().is_empty();
639 let entries = match list_directory(dir_path, skip_dot_hg) {
640 Err(e) => match e.kind() {
641 ErrorKind::NotFound | ErrorKind::PermissionDenied => {
642 files_sender
643 .send(Ok((
644 directory.to_owned(),
645 Dispatch::Bad(BadMatch::OsError(
646 // Unwrapping here is OK because the error
647 // always is a
648 // real os error
649 e.raw_os_error().unwrap(),
650 )),
651 )))
652 .unwrap();
653 return Ok(());
654 }
655 _ => return Err(e),
656 },
657 Ok(entries) => entries,
658 };
659
660 rayon::scope(|scope| -> IoResult<()> {
661 for (filename, dir_entry) in entries {
662 if let Some(ref set) = visit_entries {
663 if !set.contains(filename.deref()) {
664 continue;
665 }
666 }
667 // TODO normalize
668 let filename = if directory.is_empty() {
669 filename.to_owned()
670 } else {
671 directory.join(&filename)
672 };
673
674 if !old_results.contains_key(filename.deref()) {
675 self.handle_traversed_entry(
676 scope,
677 files_sender,
678 old_results,
679 filename,
680 dir_entry,
681 traversed_sender.clone(),
682 )?;
683 }
684 }
685 Ok(())
686 })
687 }
688
689 /// This takes a mutable reference to the results to account for the
690 /// `extend` in timings
691 #[timed]
692 fn handle_unknowns(
693 &self,
694 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
695 ) -> IoResult<()> {
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 };
716
717 // We walked all dirs under the roots that weren't ignored, and
718 // everything that matched was stat'ed and is already in results.
719 // The rest must thus be ignored or under a symlink.
720 let path_auditor = PathAuditor::new(&self.root_dir);
721
722 // TODO don't collect. Find a way of replicating the behavior of
723 // `itertools::process_results`, but for `rayon::ParallelIterator`
724 let new_results: IoResult<Vec<_>> = to_visit
725 .into_par_iter()
726 .filter_map(|(filename, entry)| -> Option<IoResult<_>> {
727 // Report ignored items in the dmap as long as they are not
728 // under a symlink directory.
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 }
761 })
762 .collect();
763
764 results.par_extend(new_results?);
765
766 Ok(())
767 }
768
769 /// This takes a mutable reference to the results to account for the
770 /// `extend` in timings
771 #[timed]
772 fn extend_from_dmap(
773 &self,
774 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
775 ) {
776 results.par_extend(self.dmap.par_iter().flat_map(
777 move |(filename, entry)| {
778 let filename: &HgPath = filename;
779 let filename_as_path = hg_path_to_path_buf(filename)?;
780 let meta =
781 self.root_dir.join(filename_as_path).symlink_metadata();
782
783 match meta {
784 Ok(ref m)
785 if !(m.file_type().is_file()
786 || m.file_type().is_symlink()) =>
787 {
788 Ok((
789 Cow::Borrowed(filename),
790 dispatch_missing(entry.state),
791 ))
792 }
793 Ok(m) => Ok((
794 Cow::Borrowed(filename),
795 dispatch_found(
796 filename,
797 *entry,
798 HgMetadata::from_metadata(m),
799 &self.dmap.copy_map,
800 self.options,
801 ),
802 )),
803 Err(ref e)
804 if e.kind() == ErrorKind::NotFound
805 || e.raw_os_error() == Some(20) =>
806 {
807 // Rust does not yet have an `ErrorKind` for
808 // `NotADirectory` (errno 20)
809 // It happens if the dirstate contains `foo/bar`
810 // and foo is not a
811 // directory
812 Ok((
813 Cow::Borrowed(filename),
814 dispatch_missing(entry.state),
815 ))
816 }
817 Err(e) => Err(e),
818 }
819 },
820 ));
821 }
822 }
823
667 #[timed]
824 #[timed]
668 fn build_response<'a>(
825 fn build_response<'a>(
669 results: impl IntoIterator<Item = (Cow<'a, HgPath>, Dispatch)>,
826 results: impl IntoIterator<Item = (Cow<'a, HgPath>, Dispatch)>,
@@ -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