Mercurial > hg
comparison rust/hg-core/src/dirstate_tree/status.rs @ 49568:da48f170d203
rust-status: query fs traversal metadata lazily
Currently, any time the status algorithm needs to read a directory from the
filesystem (because the stat-only optimization is not available), it also
stats each directory entry eagerly.
Stat'ing the entries is only needed in a few cases (like when checking
the mtime of a directory for caching): this patch creates a wrapper struct
`DirEntry` that only stats the directory entry it represents when needed.
Excerpt of an `strace` before this change on Mozilla Central:
```
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
newfstatat(3, "", {st_mode=S_IFDIR|0755, st_size=3540, ...}, AT_EMPTY_PATH) = 0
getdents64(3, 0x55dc970bd440 /* 139 entries */, 32768) = 5072
statx(3, ".hg", AT_STATX_SYNC_AS_STAT|AT_SYMLINK_NOFOLLOW, STATX_ALL, {stx_mask=STATX_ALL|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFDIR|0755, stx_size=772, ...}) = 0
[... 135 other successful `statx` calls]
getdents64(3, 0x55dc970bd440 /* 0 entries */, 32768) = 0
close(3) = 0
```
After this change:
```
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
newfstatat(3, "", {st_mode=S_IFDIR|0755, st_size=3540, ...}, AT_EMPTY_PATH) = 0
getdents64(3, 0x561567c10190 /* 139 entries */, 32768) = 5072
getdents64(3, 0x561567c10190 /* 0 entries */, 32768) = 0
close(3) = 0
```
author | Raphaël Gomès <rgomes@octobus.net> |
---|---|
date | Wed, 19 Oct 2022 12:38:06 +0200 |
parents | 6b32d39e9a67 |
children | 18282cf18aa2 |
comparison
equal
deleted
inserted
replaced
49567:6b32d39e9a67 | 49568:da48f170d203 |
---|---|
12 use crate::utils::files::get_bytes_from_os_string; | 12 use crate::utils::files::get_bytes_from_os_string; |
13 use crate::utils::files::get_path_from_bytes; | 13 use crate::utils::files::get_path_from_bytes; |
14 use crate::utils::hg_path::HgPath; | 14 use crate::utils::hg_path::HgPath; |
15 use crate::BadMatch; | 15 use crate::BadMatch; |
16 use crate::DirstateStatus; | 16 use crate::DirstateStatus; |
17 use crate::HgPathBuf; | |
18 use crate::HgPathCow; | 17 use crate::HgPathCow; |
19 use crate::PatternFileWarning; | 18 use crate::PatternFileWarning; |
20 use crate::StatusError; | 19 use crate::StatusError; |
21 use crate::StatusOptions; | 20 use crate::StatusOptions; |
22 use micro_timer::timed; | 21 use micro_timer::timed; |
23 use once_cell::sync::OnceCell; | 22 use once_cell::sync::OnceCell; |
24 use rayon::prelude::*; | 23 use rayon::prelude::*; |
25 use sha1::{Digest, Sha1}; | 24 use sha1::{Digest, Sha1}; |
26 use std::borrow::Cow; | 25 use std::borrow::Cow; |
26 use std::convert::TryFrom; | |
27 use std::convert::TryInto; | |
27 use std::io; | 28 use std::io; |
28 use std::path::Path; | 29 use std::path::Path; |
29 use std::path::PathBuf; | 30 use std::path::PathBuf; |
30 use std::sync::Mutex; | 31 use std::sync::Mutex; |
31 use std::time::SystemTime; | 32 use std::time::SystemTime; |
127 }; | 128 }; |
128 let is_at_repo_root = true; | 129 let is_at_repo_root = true; |
129 let hg_path = &BorrowedPath::OnDisk(HgPath::new("")); | 130 let hg_path = &BorrowedPath::OnDisk(HgPath::new("")); |
130 let has_ignored_ancestor = HasIgnoredAncestor::create(None, hg_path); | 131 let has_ignored_ancestor = HasIgnoredAncestor::create(None, hg_path); |
131 let root_cached_mtime = None; | 132 let root_cached_mtime = None; |
132 let root_dir_metadata = None; | |
133 // If the path we have for the repository root is a symlink, do follow it. | 133 // If the path we have for the repository root is a symlink, do follow it. |
134 // (As opposed to symlinks within the working directory which are not | 134 // (As opposed to symlinks within the working directory which are not |
135 // followed, using `std::fs::symlink_metadata`.) | 135 // followed, using `std::fs::symlink_metadata`.) |
136 common.traverse_fs_directory_and_dirstate( | 136 common.traverse_fs_directory_and_dirstate( |
137 &has_ignored_ancestor, | 137 &has_ignored_ancestor, |
138 dmap.root.as_ref(), | 138 dmap.root.as_ref(), |
139 hg_path, | 139 hg_path, |
140 &root_dir, | 140 &DirEntry { |
141 root_dir_metadata, | 141 hg_path: Cow::Borrowed(HgPath::new(b"")), |
142 fs_path: Cow::Borrowed(&root_dir), | |
143 symlink_metadata: None, | |
144 file_type: FakeFileType::Directory, | |
145 }, | |
142 root_cached_mtime, | 146 root_cached_mtime, |
143 is_at_repo_root, | 147 is_at_repo_root, |
144 )?; | 148 )?; |
145 let mut outcome = common.outcome.into_inner().unwrap(); | 149 let mut outcome = common.outcome.into_inner().unwrap(); |
146 let new_cachable = common.new_cachable_directories.into_inner().unwrap(); | 150 let new_cachable = common.new_cachable_directories.into_inner().unwrap(); |
317 /// If this returns true, we can get accurate results by only using | 321 /// If this returns true, we can get accurate results by only using |
318 /// `symlink_metadata` for child nodes that exist in the dirstate and don’t | 322 /// `symlink_metadata` for child nodes that exist in the dirstate and don’t |
319 /// need to call `read_dir`. | 323 /// need to call `read_dir`. |
320 fn can_skip_fs_readdir( | 324 fn can_skip_fs_readdir( |
321 &self, | 325 &self, |
322 directory_metadata: Option<&std::fs::Metadata>, | 326 directory_entry: &DirEntry, |
323 cached_directory_mtime: Option<TruncatedTimestamp>, | 327 cached_directory_mtime: Option<TruncatedTimestamp>, |
324 ) -> bool { | 328 ) -> bool { |
325 if !self.options.list_unknown && !self.options.list_ignored { | 329 if !self.options.list_unknown && !self.options.list_ignored { |
326 // All states that we care about listing have corresponding | 330 // All states that we care about listing have corresponding |
327 // dirstate entries. | 331 // dirstate entries. |
333 { | 337 { |
334 if let Some(cached_mtime) = cached_directory_mtime { | 338 if let Some(cached_mtime) = cached_directory_mtime { |
335 // The dirstate contains a cached mtime for this directory, set | 339 // The dirstate contains a cached mtime for this directory, set |
336 // by a previous run of the `status` algorithm which found this | 340 // by a previous run of the `status` algorithm which found this |
337 // directory eligible for `read_dir` caching. | 341 // directory eligible for `read_dir` caching. |
338 if let Some(meta) = directory_metadata { | 342 if let Ok(meta) = directory_entry.symlink_metadata() { |
339 if cached_mtime | 343 if cached_mtime |
340 .likely_equal_to_mtime_of(meta) | 344 .likely_equal_to_mtime_of(&meta) |
341 .unwrap_or(false) | 345 .unwrap_or(false) |
342 { | 346 { |
343 // The mtime of that directory has not changed | 347 // The mtime of that directory has not changed |
344 // since then, which means that the results of | 348 // since then, which means that the results of |
345 // `read_dir` should also be unchanged. | 349 // `read_dir` should also be unchanged. |
356 fn traverse_fs_directory_and_dirstate<'ancestor>( | 360 fn traverse_fs_directory_and_dirstate<'ancestor>( |
357 &self, | 361 &self, |
358 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>, | 362 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>, |
359 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>, | 363 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>, |
360 directory_hg_path: &BorrowedPath<'tree, 'on_disk>, | 364 directory_hg_path: &BorrowedPath<'tree, 'on_disk>, |
361 directory_fs_path: &Path, | 365 directory_entry: &DirEntry, |
362 directory_metadata: Option<&std::fs::Metadata>, | |
363 cached_directory_mtime: Option<TruncatedTimestamp>, | 366 cached_directory_mtime: Option<TruncatedTimestamp>, |
364 is_at_repo_root: bool, | 367 is_at_repo_root: bool, |
365 ) -> Result<bool, DirstateV2ParseError> { | 368 ) -> Result<bool, DirstateV2ParseError> { |
366 if self.can_skip_fs_readdir(directory_metadata, cached_directory_mtime) | 369 if self.can_skip_fs_readdir(directory_entry, cached_directory_mtime) { |
367 { | |
368 dirstate_nodes | 370 dirstate_nodes |
369 .par_iter() | 371 .par_iter() |
370 .map(|dirstate_node| { | 372 .map(|dirstate_node| { |
371 let fs_path = directory_fs_path.join(get_path_from_bytes( | 373 let fs_path = &directory_entry.fs_path; |
374 let fs_path = fs_path.join(get_path_from_bytes( | |
372 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(), | 375 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(), |
373 )); | 376 )); |
374 match std::fs::symlink_metadata(&fs_path) { | 377 match std::fs::symlink_metadata(&fs_path) { |
375 Ok(fs_metadata) => self.traverse_fs_and_dirstate( | 378 Ok(fs_metadata) => { |
376 &fs_path, | 379 let file_type = |
377 &fs_metadata, | 380 match fs_metadata.file_type().try_into() { |
378 dirstate_node, | 381 Ok(file_type) => file_type, |
379 has_ignored_ancestor, | 382 Err(_) => return Ok(()), |
380 ), | 383 }; |
384 let entry = DirEntry { | |
385 hg_path: Cow::Borrowed( | |
386 dirstate_node | |
387 .full_path(&self.dmap.on_disk)?, | |
388 ), | |
389 fs_path: Cow::Borrowed(&fs_path), | |
390 symlink_metadata: Some(fs_metadata), | |
391 file_type, | |
392 }; | |
393 self.traverse_fs_and_dirstate( | |
394 &entry, | |
395 dirstate_node, | |
396 has_ignored_ancestor, | |
397 ) | |
398 } | |
381 Err(e) if e.kind() == std::io::ErrorKind::NotFound => { | 399 Err(e) if e.kind() == std::io::ErrorKind::NotFound => { |
382 self.traverse_dirstate_only(dirstate_node) | 400 self.traverse_dirstate_only(dirstate_node) |
383 } | 401 } |
384 Err(error) => { | 402 Err(error) => { |
385 let hg_path = | 403 let hg_path = |
396 return Ok(children_all_have_dirstate_node_or_are_ignored); | 414 return Ok(children_all_have_dirstate_node_or_are_ignored); |
397 } | 415 } |
398 | 416 |
399 let mut fs_entries = if let Ok(entries) = self.read_dir( | 417 let mut fs_entries = if let Ok(entries) = self.read_dir( |
400 directory_hg_path, | 418 directory_hg_path, |
401 directory_fs_path, | 419 &directory_entry.fs_path, |
402 is_at_repo_root, | 420 is_at_repo_root, |
403 ) { | 421 ) { |
404 entries | 422 entries |
405 } else { | 423 } else { |
406 // Treat an unreadable directory (typically because of insufficient | 424 // Treat an unreadable directory (typically because of insufficient |
438 use itertools::EitherOrBoth::*; | 456 use itertools::EitherOrBoth::*; |
439 let has_dirstate_node_or_is_ignored; | 457 let has_dirstate_node_or_is_ignored; |
440 match pair { | 458 match pair { |
441 Both(dirstate_node, fs_entry) => { | 459 Both(dirstate_node, fs_entry) => { |
442 self.traverse_fs_and_dirstate( | 460 self.traverse_fs_and_dirstate( |
443 &fs_entry.fs_path, | 461 &fs_entry, |
444 &fs_entry.metadata, | |
445 dirstate_node, | 462 dirstate_node, |
446 has_ignored_ancestor, | 463 has_ignored_ancestor, |
447 )?; | 464 )?; |
448 has_dirstate_node_or_is_ignored = true | 465 has_dirstate_node_or_is_ignored = true |
449 } | 466 } |
464 .try_reduce(|| true, |a, b| Ok(a && b)) | 481 .try_reduce(|| true, |a, b| Ok(a && b)) |
465 } | 482 } |
466 | 483 |
467 fn traverse_fs_and_dirstate<'ancestor>( | 484 fn traverse_fs_and_dirstate<'ancestor>( |
468 &self, | 485 &self, |
469 fs_path: &Path, | 486 fs_entry: &DirEntry, |
470 fs_metadata: &std::fs::Metadata, | |
471 dirstate_node: NodeRef<'tree, 'on_disk>, | 487 dirstate_node: NodeRef<'tree, 'on_disk>, |
472 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>, | 488 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>, |
473 ) -> Result<(), DirstateV2ParseError> { | 489 ) -> Result<(), DirstateV2ParseError> { |
474 self.check_for_outdated_directory_cache(&dirstate_node)?; | 490 self.check_for_outdated_directory_cache(&dirstate_node)?; |
475 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?; | 491 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?; |
476 let file_type = fs_metadata.file_type(); | 492 let file_or_symlink = fs_entry.is_file() || fs_entry.is_symlink(); |
477 let file_or_symlink = file_type.is_file() || file_type.is_symlink(); | |
478 if !file_or_symlink { | 493 if !file_or_symlink { |
479 // If we previously had a file here, it was removed (with | 494 // If we previously had a file here, it was removed (with |
480 // `hg rm` or similar) or deleted before it could be | 495 // `hg rm` or similar) or deleted before it could be |
481 // replaced by a directory or something else. | 496 // replaced by a directory or something else. |
482 self.mark_removed_or_deleted_if_file(&dirstate_node)?; | 497 self.mark_removed_or_deleted_if_file(&dirstate_node)?; |
483 } | 498 } |
484 if file_type.is_dir() { | 499 if fs_entry.is_dir() { |
485 if self.options.collect_traversed_dirs { | 500 if self.options.collect_traversed_dirs { |
486 self.outcome | 501 self.outcome |
487 .lock() | 502 .lock() |
488 .unwrap() | 503 .unwrap() |
489 .traversed | 504 .traversed |
497 let children_all_have_dirstate_node_or_are_ignored = self | 512 let children_all_have_dirstate_node_or_are_ignored = self |
498 .traverse_fs_directory_and_dirstate( | 513 .traverse_fs_directory_and_dirstate( |
499 &is_ignored, | 514 &is_ignored, |
500 dirstate_node.children(self.dmap.on_disk)?, | 515 dirstate_node.children(self.dmap.on_disk)?, |
501 hg_path, | 516 hg_path, |
502 fs_path, | 517 fs_entry, |
503 Some(fs_metadata), | |
504 dirstate_node.cached_directory_mtime()?, | 518 dirstate_node.cached_directory_mtime()?, |
505 is_at_repo_root, | 519 is_at_repo_root, |
506 )?; | 520 )?; |
507 self.maybe_save_directory_mtime( | 521 self.maybe_save_directory_mtime( |
508 children_all_have_dirstate_node_or_are_ignored, | 522 children_all_have_dirstate_node_or_are_ignored, |
509 fs_metadata, | 523 fs_entry, |
510 dirstate_node, | 524 dirstate_node, |
511 )? | 525 )? |
512 } else { | 526 } else { |
513 if file_or_symlink && self.matcher.matches(&hg_path) { | 527 if file_or_symlink && self.matcher.matches(&hg_path) { |
514 if let Some(entry) = dirstate_node.entry()? { | 528 if let Some(entry) = dirstate_node.entry()? { |
525 } else if entry.removed() { | 539 } else if entry.removed() { |
526 self.push_outcome(Outcome::Removed, &dirstate_node)?; | 540 self.push_outcome(Outcome::Removed, &dirstate_node)?; |
527 } else if entry.modified() { | 541 } else if entry.modified() { |
528 self.push_outcome(Outcome::Modified, &dirstate_node)?; | 542 self.push_outcome(Outcome::Modified, &dirstate_node)?; |
529 } else { | 543 } else { |
530 self.handle_normal_file(&dirstate_node, fs_metadata)?; | 544 self.handle_normal_file(&dirstate_node, fs_entry)?; |
531 } | 545 } |
532 } else { | 546 } else { |
533 // `node.entry.is_none()` indicates a "directory" | 547 // `node.entry.is_none()` indicates a "directory" |
534 // node, but the filesystem has a file | 548 // node, but the filesystem has a file |
535 self.mark_unknown_or_ignored( | 549 self.mark_unknown_or_ignored( |
548 } | 562 } |
549 | 563 |
550 fn maybe_save_directory_mtime( | 564 fn maybe_save_directory_mtime( |
551 &self, | 565 &self, |
552 children_all_have_dirstate_node_or_are_ignored: bool, | 566 children_all_have_dirstate_node_or_are_ignored: bool, |
553 directory_metadata: &std::fs::Metadata, | 567 directory_entry: &DirEntry, |
554 dirstate_node: NodeRef<'tree, 'on_disk>, | 568 dirstate_node: NodeRef<'tree, 'on_disk>, |
555 ) -> Result<(), DirstateV2ParseError> { | 569 ) -> Result<(), DirstateV2ParseError> { |
556 if !children_all_have_dirstate_node_or_are_ignored { | 570 if !children_all_have_dirstate_node_or_are_ignored { |
557 return Ok(()); | 571 return Ok(()); |
558 } | 572 } |
574 // has nanosecond precision, the times reported for a | 588 // has nanosecond precision, the times reported for a |
575 // directory’s (or file’s) modified time may have lower | 589 // directory’s (or file’s) modified time may have lower |
576 // resolution based on the filesystem (for example ext3 | 590 // resolution based on the filesystem (for example ext3 |
577 // only stores integer seconds), kernel (see | 591 // only stores integer seconds), kernel (see |
578 // https://stackoverflow.com/a/14393315/1162888), etc. | 592 // https://stackoverflow.com/a/14393315/1162888), etc. |
593 let metadata = match directory_entry.symlink_metadata() { | |
594 Ok(meta) => meta, | |
595 Err(_) => return Ok(()), | |
596 }; | |
579 let directory_mtime = if let Ok(option) = | 597 let directory_mtime = if let Ok(option) = |
580 TruncatedTimestamp::for_reliable_mtime_of( | 598 TruncatedTimestamp::for_reliable_mtime_of(&metadata, status_start) |
581 directory_metadata, | 599 { |
582 status_start, | |
583 ) { | |
584 if let Some(directory_mtime) = option { | 600 if let Some(directory_mtime) = option { |
585 directory_mtime | 601 directory_mtime |
586 } else { | 602 } else { |
587 // The directory was modified too recently, | 603 // The directory was modified too recently, |
588 // don’t cache its `read_dir` results. | 604 // don’t cache its `read_dir` results. |
639 | 655 |
640 /// A file that is clean in the dirstate was found in the filesystem | 656 /// A file that is clean in the dirstate was found in the filesystem |
641 fn handle_normal_file( | 657 fn handle_normal_file( |
642 &self, | 658 &self, |
643 dirstate_node: &NodeRef<'tree, 'on_disk>, | 659 dirstate_node: &NodeRef<'tree, 'on_disk>, |
644 fs_metadata: &std::fs::Metadata, | 660 fs_entry: &DirEntry, |
645 ) -> Result<(), DirstateV2ParseError> { | 661 ) -> Result<(), DirstateV2ParseError> { |
646 // Keep the low 31 bits | 662 // Keep the low 31 bits |
647 fn truncate_u64(value: u64) -> i32 { | 663 fn truncate_u64(value: u64) -> i32 { |
648 (value & 0x7FFF_FFFF) as i32 | 664 (value & 0x7FFF_FFFF) as i32 |
649 } | 665 } |
650 | 666 |
667 let fs_metadata = match fs_entry.symlink_metadata() { | |
668 Ok(meta) => meta, | |
669 Err(_) => return Ok(()), | |
670 }; | |
671 | |
651 let entry = dirstate_node | 672 let entry = dirstate_node |
652 .entry()? | 673 .entry()? |
653 .expect("handle_normal_file called with entry-less node"); | 674 .expect("handle_normal_file called with entry-less node"); |
654 let mode_changed = | 675 let mode_changed = |
655 || self.options.check_exec && entry.mode_changed(fs_metadata); | 676 || self.options.check_exec && entry.mode_changed(&fs_metadata); |
656 let size = entry.size(); | 677 let size = entry.size(); |
657 let size_changed = size != truncate_u64(fs_metadata.len()); | 678 let size_changed = size != truncate_u64(fs_metadata.len()); |
658 if size >= 0 && size_changed && fs_metadata.file_type().is_symlink() { | 679 if size >= 0 && size_changed && fs_metadata.file_type().is_symlink() { |
659 // issue6456: Size returned may be longer due to encryption | 680 // issue6456: Size returned may be longer due to encryption |
660 // on EXT-4 fscrypt. TODO maybe only do it on EXT4? | 681 // on EXT-4 fscrypt. TODO maybe only do it on EXT4? |
665 { | 686 { |
666 self.push_outcome(Outcome::Modified, dirstate_node)? | 687 self.push_outcome(Outcome::Modified, dirstate_node)? |
667 } else { | 688 } else { |
668 let mtime_looks_clean; | 689 let mtime_looks_clean; |
669 if let Some(dirstate_mtime) = entry.truncated_mtime() { | 690 if let Some(dirstate_mtime) = entry.truncated_mtime() { |
670 let fs_mtime = TruncatedTimestamp::for_mtime_of(fs_metadata) | 691 let fs_mtime = TruncatedTimestamp::for_mtime_of(&fs_metadata) |
671 .expect("OS/libc does not support mtime?"); | 692 .expect("OS/libc does not support mtime?"); |
672 // There might be a change in the future if for example the | 693 // There might be a change in the future if for example the |
673 // internal clock become off while process run, but this is a | 694 // internal clock become off while process run, but this is a |
674 // case where the issues the user would face | 695 // case where the issues the user would face |
675 // would be a lot worse and there is nothing we | 696 // would be a lot worse and there is nothing we |
736 has_ignored_ancestor: bool, | 757 has_ignored_ancestor: bool, |
737 directory_hg_path: &HgPath, | 758 directory_hg_path: &HgPath, |
738 fs_entry: &DirEntry, | 759 fs_entry: &DirEntry, |
739 ) -> bool { | 760 ) -> bool { |
740 let hg_path = directory_hg_path.join(&fs_entry.hg_path); | 761 let hg_path = directory_hg_path.join(&fs_entry.hg_path); |
741 let file_type = fs_entry.metadata.file_type(); | 762 let file_or_symlink = fs_entry.is_file() || fs_entry.is_symlink(); |
742 let file_or_symlink = file_type.is_file() || file_type.is_symlink(); | 763 if fs_entry.is_dir() { |
743 if file_type.is_dir() { | |
744 let is_ignored = | 764 let is_ignored = |
745 has_ignored_ancestor || (self.ignore_fn)(&hg_path); | 765 has_ignored_ancestor || (self.ignore_fn)(&hg_path); |
746 let traverse_children = if is_ignored { | 766 let traverse_children = if is_ignored { |
747 // Descendants of an ignored directory are all ignored | 767 // Descendants of an ignored directory are all ignored |
748 self.options.list_ignored | 768 self.options.list_ignored |
751 // ignored | 771 // ignored |
752 self.options.list_unknown || self.options.list_ignored | 772 self.options.list_unknown || self.options.list_ignored |
753 }; | 773 }; |
754 if traverse_children { | 774 if traverse_children { |
755 let is_at_repo_root = false; | 775 let is_at_repo_root = false; |
756 if let Ok(children_fs_entries) = self.read_dir( | 776 if let Ok(children_fs_entries) = |
757 &hg_path, | 777 self.read_dir(&hg_path, &fs_entry.fs_path, is_at_repo_root) |
758 &fs_entry.fs_path, | 778 { |
759 is_at_repo_root, | |
760 ) { | |
761 children_fs_entries.par_iter().for_each(|child_fs_entry| { | 779 children_fs_entries.par_iter().for_each(|child_fs_entry| { |
762 self.traverse_fs_only( | 780 self.traverse_fs_only( |
763 is_ignored, | 781 is_ignored, |
764 &hg_path, | 782 &hg_path, |
765 child_fs_entry, | 783 child_fs_entry, |
818 } | 836 } |
819 is_ignored | 837 is_ignored |
820 } | 838 } |
821 } | 839 } |
822 | 840 |
823 struct DirEntry { | 841 /// Since [`std::fs::FileType`] cannot be built directly, we emulate what we |
824 /// Path as stored in the dirstate | 842 /// care about. |
825 hg_path: HgPathBuf, | 843 #[derive(Copy, Clone, Debug, PartialEq, Eq)] |
844 enum FakeFileType { | |
845 File, | |
846 Directory, | |
847 Symlink, | |
848 } | |
849 | |
850 impl TryFrom<std::fs::FileType> for FakeFileType { | |
851 type Error = (); | |
852 | |
853 fn try_from(f: std::fs::FileType) -> Result<Self, Self::Error> { | |
854 if f.is_dir() { | |
855 Ok(Self::Directory) | |
856 } else if f.is_file() { | |
857 Ok(Self::File) | |
858 } else if f.is_symlink() { | |
859 Ok(Self::Symlink) | |
860 } else { | |
861 // Things like FIFO etc. | |
862 Err(()) | |
863 } | |
864 } | |
865 } | |
866 | |
867 struct DirEntry<'a> { | |
868 /// Path as stored in the dirstate, or just the filename for optimization. | |
869 hg_path: HgPathCow<'a>, | |
826 /// Filesystem path | 870 /// Filesystem path |
827 fs_path: PathBuf, | 871 fs_path: Cow<'a, Path>, |
828 metadata: std::fs::Metadata, | 872 /// Lazily computed |
829 } | 873 symlink_metadata: Option<std::fs::Metadata>, |
830 | 874 /// Already computed for ergonomics. |
831 impl DirEntry { | 875 file_type: FakeFileType, |
832 /// Returns **unsorted** entries in the given directory, with name and | 876 } |
833 /// metadata. | 877 |
878 impl<'a> DirEntry<'a> { | |
879 /// Returns **unsorted** entries in the given directory, with name, | |
880 /// metadata and file type. | |
834 /// | 881 /// |
835 /// If a `.hg` sub-directory is encountered: | 882 /// If a `.hg` sub-directory is encountered: |
836 /// | 883 /// |
837 /// * At the repository root, ignore that sub-directory | 884 /// * At the repository root, ignore that sub-directory |
838 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty | 885 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty |
842 let at_cwd = path == Path::new(""); | 889 let at_cwd = path == Path::new(""); |
843 let read_dir_path = if at_cwd { Path::new(".") } else { path }; | 890 let read_dir_path = if at_cwd { Path::new(".") } else { path }; |
844 let mut results = Vec::new(); | 891 let mut results = Vec::new(); |
845 for entry in read_dir_path.read_dir()? { | 892 for entry in read_dir_path.read_dir()? { |
846 let entry = entry?; | 893 let entry = entry?; |
847 let metadata = match entry.metadata() { | 894 let file_type = match entry.file_type() { |
848 Ok(v) => v, | 895 Ok(v) => v, |
849 Err(e) => { | 896 Err(e) => { |
850 // race with file deletion? | 897 // race with file deletion? |
851 if e.kind() == std::io::ErrorKind::NotFound { | 898 if e.kind() == std::io::ErrorKind::NotFound { |
852 continue; | 899 continue; |
859 // FIXME don't do this when cached | 906 // FIXME don't do this when cached |
860 if file_name == ".hg" { | 907 if file_name == ".hg" { |
861 if is_at_repo_root { | 908 if is_at_repo_root { |
862 // Skip the repo’s own .hg (might be a symlink) | 909 // Skip the repo’s own .hg (might be a symlink) |
863 continue; | 910 continue; |
864 } else if metadata.is_dir() { | 911 } else if file_type.is_dir() { |
865 // A .hg sub-directory at another location means a subrepo, | 912 // A .hg sub-directory at another location means a subrepo, |
866 // skip it entirely. | 913 // skip it entirely. |
867 return Ok(Vec::new()); | 914 return Ok(Vec::new()); |
868 } | 915 } |
869 } | 916 } |
870 let full_path = if at_cwd { | 917 let full_path = if at_cwd { |
871 file_name.clone().into() | 918 file_name.clone().into() |
872 } else { | 919 } else { |
873 entry.path() | 920 entry.path() |
874 }; | 921 }; |
875 let base_name = get_bytes_from_os_string(file_name).into(); | 922 let filename = |
923 Cow::Owned(get_bytes_from_os_string(file_name).into()); | |
924 let file_type = match FakeFileType::try_from(file_type) { | |
925 Ok(file_type) => file_type, | |
926 Err(_) => continue, | |
927 }; | |
876 results.push(DirEntry { | 928 results.push(DirEntry { |
877 hg_path: base_name, | 929 hg_path: filename, |
878 fs_path: full_path, | 930 fs_path: Cow::Owned(full_path.to_path_buf()), |
879 metadata, | 931 symlink_metadata: None, |
932 file_type, | |
880 }) | 933 }) |
881 } | 934 } |
882 Ok(results) | 935 Ok(results) |
936 } | |
937 | |
938 fn symlink_metadata(&self) -> Result<std::fs::Metadata, std::io::Error> { | |
939 match &self.symlink_metadata { | |
940 Some(meta) => Ok(meta.clone()), | |
941 None => std::fs::symlink_metadata(&self.fs_path), | |
942 } | |
943 } | |
944 | |
945 fn is_dir(&self) -> bool { | |
946 self.file_type == FakeFileType::Directory | |
947 } | |
948 | |
949 fn is_file(&self) -> bool { | |
950 self.file_type == FakeFileType::File | |
951 } | |
952 | |
953 fn is_symlink(&self) -> bool { | |
954 self.file_type == FakeFileType::Symlink | |
883 } | 955 } |
884 } | 956 } |
885 | 957 |
886 /// Return the `mtime` of a temporary file newly-created in the `.hg` directory | 958 /// Return the `mtime` of a temporary file newly-created in the `.hg` directory |
887 /// of the give repository. | 959 /// of the give repository. |