##// END OF EJS Templates
rust-status: explicitly track bad file types...
Spencer Baugh -
r51755:5efccea9 default
parent child Browse files
Show More
@@ -1,149 +1,149 b''
1 // status.rs
1 // status.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! Rust implementation of dirstate.status (dirstate.py).
8 //! Rust implementation of dirstate.status (dirstate.py).
9 //! It is currently missing a lot of functionality compared to the Python one
9 //! It is currently missing a lot of functionality compared to the Python one
10 //! and will only be triggered in narrow cases.
10 //! and will only be triggered in narrow cases.
11
11
12 use crate::dirstate::entry::TruncatedTimestamp;
12 use crate::dirstate::entry::TruncatedTimestamp;
13 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
13 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
14 use crate::{
14 use crate::{
15 utils::hg_path::{HgPath, HgPathError},
15 utils::hg_path::{HgPath, HgPathError},
16 PatternError,
16 PatternError,
17 };
17 };
18
18
19 use std::{borrow::Cow, fmt};
19 use std::{borrow::Cow, fmt};
20
20
21 /// Wrong type of file from a `BadMatch`
21 /// Wrong type of file from a `BadMatch`
22 /// Note: a lot of those don't exist on all platforms.
22 /// Note: a lot of those don't exist on all platforms.
23 #[derive(Debug, Copy, Clone)]
23 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
24 pub enum BadType {
24 pub enum BadType {
25 CharacterDevice,
25 CharacterDevice,
26 BlockDevice,
26 BlockDevice,
27 FIFO,
27 FIFO,
28 Socket,
28 Socket,
29 Directory,
29 Directory,
30 Unknown,
30 Unknown,
31 }
31 }
32
32
33 impl fmt::Display for BadType {
33 impl fmt::Display for BadType {
34 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
34 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
35 f.write_str(match self {
35 f.write_str(match self {
36 BadType::CharacterDevice => "character device",
36 BadType::CharacterDevice => "character device",
37 BadType::BlockDevice => "block device",
37 BadType::BlockDevice => "block device",
38 BadType::FIFO => "fifo",
38 BadType::FIFO => "fifo",
39 BadType::Socket => "socket",
39 BadType::Socket => "socket",
40 BadType::Directory => "directory",
40 BadType::Directory => "directory",
41 BadType::Unknown => "unknown",
41 BadType::Unknown => "unknown",
42 })
42 })
43 }
43 }
44 }
44 }
45
45
46 /// Was explicitly matched but cannot be found/accessed
46 /// Was explicitly matched but cannot be found/accessed
47 #[derive(Debug, Copy, Clone)]
47 #[derive(Debug, Copy, Clone)]
48 pub enum BadMatch {
48 pub enum BadMatch {
49 OsError(i32),
49 OsError(i32),
50 BadType(BadType),
50 BadType(BadType),
51 }
51 }
52
52
53 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait + 'static>`, so add
53 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait + 'static>`, so add
54 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
54 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
55 pub type IgnoreFnType<'a> =
55 pub type IgnoreFnType<'a> =
56 Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
56 Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
57
57
58 /// We have a good mix of owned (from directory traversal) and borrowed (from
58 /// We have a good mix of owned (from directory traversal) and borrowed (from
59 /// the dirstate/explicit) paths, this comes up a lot.
59 /// the dirstate/explicit) paths, this comes up a lot.
60 pub type HgPathCow<'a> = Cow<'a, HgPath>;
60 pub type HgPathCow<'a> = Cow<'a, HgPath>;
61
61
62 #[derive(Debug, Copy, Clone)]
62 #[derive(Debug, Copy, Clone)]
63 pub struct StatusOptions {
63 pub struct StatusOptions {
64 /// Whether we are on a filesystem with UNIX-like exec flags
64 /// Whether we are on a filesystem with UNIX-like exec flags
65 pub check_exec: bool,
65 pub check_exec: bool,
66 pub list_clean: bool,
66 pub list_clean: bool,
67 pub list_unknown: bool,
67 pub list_unknown: bool,
68 pub list_ignored: bool,
68 pub list_ignored: bool,
69 /// Whether to populate `StatusPath::copy_source`
69 /// Whether to populate `StatusPath::copy_source`
70 pub list_copies: bool,
70 pub list_copies: bool,
71 /// Whether to collect traversed dirs for applying a callback later.
71 /// Whether to collect traversed dirs for applying a callback later.
72 /// Used by `hg purge` for example.
72 /// Used by `hg purge` for example.
73 pub collect_traversed_dirs: bool,
73 pub collect_traversed_dirs: bool,
74 }
74 }
75
75
76 #[derive(Default)]
76 #[derive(Default)]
77 pub struct DirstateStatus<'a> {
77 pub struct DirstateStatus<'a> {
78 /// The current time at the start of the `status()` algorithm, as measured
78 /// The current time at the start of the `status()` algorithm, as measured
79 /// and possibly truncated by the filesystem.
79 /// and possibly truncated by the filesystem.
80 pub filesystem_time_at_status_start: Option<TruncatedTimestamp>,
80 pub filesystem_time_at_status_start: Option<TruncatedTimestamp>,
81
81
82 /// Tracked files whose contents have changed since the parent revision
82 /// Tracked files whose contents have changed since the parent revision
83 pub modified: Vec<StatusPath<'a>>,
83 pub modified: Vec<StatusPath<'a>>,
84
84
85 /// Newly-tracked files that were not present in the parent
85 /// Newly-tracked files that were not present in the parent
86 pub added: Vec<StatusPath<'a>>,
86 pub added: Vec<StatusPath<'a>>,
87
87
88 /// Previously-tracked files that have been (re)moved with an hg command
88 /// Previously-tracked files that have been (re)moved with an hg command
89 pub removed: Vec<StatusPath<'a>>,
89 pub removed: Vec<StatusPath<'a>>,
90
90
91 /// (Still) tracked files that are missing, (re)moved with an non-hg
91 /// (Still) tracked files that are missing, (re)moved with an non-hg
92 /// command
92 /// command
93 pub deleted: Vec<StatusPath<'a>>,
93 pub deleted: Vec<StatusPath<'a>>,
94
94
95 /// Tracked files that are up to date with the parent.
95 /// Tracked files that are up to date with the parent.
96 /// Only pupulated if `StatusOptions::list_clean` is true.
96 /// Only pupulated if `StatusOptions::list_clean` is true.
97 pub clean: Vec<StatusPath<'a>>,
97 pub clean: Vec<StatusPath<'a>>,
98
98
99 /// Files in the working directory that are ignored with `.hgignore`.
99 /// Files in the working directory that are ignored with `.hgignore`.
100 /// Only pupulated if `StatusOptions::list_ignored` is true.
100 /// Only pupulated if `StatusOptions::list_ignored` is true.
101 pub ignored: Vec<StatusPath<'a>>,
101 pub ignored: Vec<StatusPath<'a>>,
102
102
103 /// Files in the working directory that are neither tracked nor ignored.
103 /// Files in the working directory that are neither tracked nor ignored.
104 /// Only pupulated if `StatusOptions::list_unknown` is true.
104 /// Only pupulated if `StatusOptions::list_unknown` is true.
105 pub unknown: Vec<StatusPath<'a>>,
105 pub unknown: Vec<StatusPath<'a>>,
106
106
107 /// Was explicitly matched but cannot be found/accessed
107 /// Was explicitly matched but cannot be found/accessed
108 pub bad: Vec<(HgPathCow<'a>, BadMatch)>,
108 pub bad: Vec<(HgPathCow<'a>, BadMatch)>,
109
109
110 /// Either clean or modified, but we can’t tell from filesystem metadata
110 /// Either clean or modified, but we can’t tell from filesystem metadata
111 /// alone. The file contents need to be read and compared with that in
111 /// alone. The file contents need to be read and compared with that in
112 /// the parent.
112 /// the parent.
113 pub unsure: Vec<StatusPath<'a>>,
113 pub unsure: Vec<StatusPath<'a>>,
114
114
115 /// Only filled if `collect_traversed_dirs` is `true`
115 /// Only filled if `collect_traversed_dirs` is `true`
116 pub traversed: Vec<HgPathCow<'a>>,
116 pub traversed: Vec<HgPathCow<'a>>,
117
117
118 /// Whether `status()` made changed to the `DirstateMap` that should be
118 /// Whether `status()` made changed to the `DirstateMap` that should be
119 /// written back to disk
119 /// written back to disk
120 pub dirty: bool,
120 pub dirty: bool,
121 }
121 }
122
122
123 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
123 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
124 pub struct StatusPath<'a> {
124 pub struct StatusPath<'a> {
125 pub path: HgPathCow<'a>,
125 pub path: HgPathCow<'a>,
126 pub copy_source: Option<HgPathCow<'a>>,
126 pub copy_source: Option<HgPathCow<'a>>,
127 }
127 }
128
128
129 #[derive(Debug, derive_more::From)]
129 #[derive(Debug, derive_more::From)]
130 pub enum StatusError {
130 pub enum StatusError {
131 /// An invalid path that cannot be represented in Mercurial was found
131 /// An invalid path that cannot be represented in Mercurial was found
132 Path(HgPathError),
132 Path(HgPathError),
133 /// An invalid "ignore" pattern was found
133 /// An invalid "ignore" pattern was found
134 Pattern(PatternError),
134 Pattern(PatternError),
135 /// Corrupted dirstate
135 /// Corrupted dirstate
136 DirstateV2ParseError(DirstateV2ParseError),
136 DirstateV2ParseError(DirstateV2ParseError),
137 }
137 }
138
138
139 impl fmt::Display for StatusError {
139 impl fmt::Display for StatusError {
140 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
140 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141 match self {
141 match self {
142 StatusError::Path(error) => error.fmt(f),
142 StatusError::Path(error) => error.fmt(f),
143 StatusError::Pattern(error) => error.fmt(f),
143 StatusError::Pattern(error) => error.fmt(f),
144 StatusError::DirstateV2ParseError(_) => {
144 StatusError::DirstateV2ParseError(_) => {
145 f.write_str("dirstate-v2 parse error")
145 f.write_str("dirstate-v2 parse error")
146 }
146 }
147 }
147 }
148 }
148 }
149 }
149 }
@@ -1,996 +1,1013 b''
1 use crate::dirstate::entry::TruncatedTimestamp;
1 use crate::dirstate::entry::TruncatedTimestamp;
2 use crate::dirstate::status::IgnoreFnType;
2 use crate::dirstate::status::IgnoreFnType;
3 use crate::dirstate::status::StatusPath;
3 use crate::dirstate::status::StatusPath;
4 use crate::dirstate_tree::dirstate_map::BorrowedPath;
4 use crate::dirstate_tree::dirstate_map::BorrowedPath;
5 use crate::dirstate_tree::dirstate_map::ChildNodesRef;
5 use crate::dirstate_tree::dirstate_map::ChildNodesRef;
6 use crate::dirstate_tree::dirstate_map::DirstateMap;
6 use crate::dirstate_tree::dirstate_map::DirstateMap;
7 use crate::dirstate_tree::dirstate_map::DirstateVersion;
7 use crate::dirstate_tree::dirstate_map::DirstateVersion;
8 use crate::dirstate_tree::dirstate_map::NodeRef;
8 use crate::dirstate_tree::dirstate_map::NodeRef;
9 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
9 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
10 use crate::matchers::get_ignore_function;
10 use crate::matchers::get_ignore_function;
11 use crate::matchers::Matcher;
11 use crate::matchers::Matcher;
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_bytes_from_path;
13 use crate::utils::files::get_bytes_from_path;
14 use crate::utils::files::get_path_from_bytes;
14 use crate::utils::files::get_path_from_bytes;
15 use crate::utils::hg_path::HgPath;
15 use crate::utils::hg_path::HgPath;
16 use crate::BadMatch;
16 use crate::BadMatch;
17 use crate::BadType;
17 use crate::DirstateStatus;
18 use crate::DirstateStatus;
18 use crate::HgPathCow;
19 use crate::HgPathCow;
19 use crate::PatternFileWarning;
20 use crate::PatternFileWarning;
20 use crate::StatusError;
21 use crate::StatusError;
21 use crate::StatusOptions;
22 use crate::StatusOptions;
22 use once_cell::sync::OnceCell;
23 use once_cell::sync::OnceCell;
23 use rayon::prelude::*;
24 use rayon::prelude::*;
24 use sha1::{Digest, Sha1};
25 use sha1::{Digest, Sha1};
25 use std::borrow::Cow;
26 use std::borrow::Cow;
26 use std::io;
27 use std::io;
28 use std::os::unix::prelude::FileTypeExt;
27 use std::path::Path;
29 use std::path::Path;
28 use std::path::PathBuf;
30 use std::path::PathBuf;
29 use std::sync::Mutex;
31 use std::sync::Mutex;
30 use std::time::SystemTime;
32 use std::time::SystemTime;
31
33
32 /// Returns the status of the working directory compared to its parent
34 /// Returns the status of the working directory compared to its parent
33 /// changeset.
35 /// changeset.
34 ///
36 ///
35 /// This algorithm is based on traversing the filesystem tree (`fs` in function
37 /// This algorithm is based on traversing the filesystem tree (`fs` in function
36 /// and variable names) and dirstate tree at the same time. The core of this
38 /// and variable names) and dirstate tree at the same time. The core of this
37 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
39 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
38 /// and its use of `itertools::merge_join_by`. When reaching a path that only
40 /// and its use of `itertools::merge_join_by`. When reaching a path that only
39 /// exists in one of the two trees, depending on information requested by
41 /// exists in one of the two trees, depending on information requested by
40 /// `options` we may need to traverse the remaining subtree.
42 /// `options` we may need to traverse the remaining subtree.
41 #[logging_timer::time("trace")]
43 #[logging_timer::time("trace")]
42 pub fn status<'dirstate>(
44 pub fn status<'dirstate>(
43 dmap: &'dirstate mut DirstateMap,
45 dmap: &'dirstate mut DirstateMap,
44 matcher: &(dyn Matcher + Sync),
46 matcher: &(dyn Matcher + Sync),
45 root_dir: PathBuf,
47 root_dir: PathBuf,
46 ignore_files: Vec<PathBuf>,
48 ignore_files: Vec<PathBuf>,
47 options: StatusOptions,
49 options: StatusOptions,
48 ) -> Result<(DirstateStatus<'dirstate>, Vec<PatternFileWarning>), StatusError>
50 ) -> Result<(DirstateStatus<'dirstate>, Vec<PatternFileWarning>), StatusError>
49 {
51 {
50 // Also cap for a Python caller of this function, but don't complain if
52 // Also cap for a Python caller of this function, but don't complain if
51 // the global threadpool has already been set since this code path is also
53 // the global threadpool has already been set since this code path is also
52 // being used by `rhg`, which calls this early.
54 // being used by `rhg`, which calls this early.
53 let _ = crate::utils::cap_default_rayon_threads();
55 let _ = crate::utils::cap_default_rayon_threads();
54
56
55 let (ignore_fn, warnings, patterns_changed): (IgnoreFnType, _, _) =
57 let (ignore_fn, warnings, patterns_changed): (IgnoreFnType, _, _) =
56 if options.list_ignored || options.list_unknown {
58 if options.list_ignored || options.list_unknown {
57 let (ignore_fn, warnings, changed) = match dmap.dirstate_version {
59 let (ignore_fn, warnings, changed) = match dmap.dirstate_version {
58 DirstateVersion::V1 => {
60 DirstateVersion::V1 => {
59 let (ignore_fn, warnings) = get_ignore_function(
61 let (ignore_fn, warnings) = get_ignore_function(
60 ignore_files,
62 ignore_files,
61 &root_dir,
63 &root_dir,
62 &mut |_source, _pattern_bytes| {},
64 &mut |_source, _pattern_bytes| {},
63 )?;
65 )?;
64 (ignore_fn, warnings, None)
66 (ignore_fn, warnings, None)
65 }
67 }
66 DirstateVersion::V2 => {
68 DirstateVersion::V2 => {
67 let mut hasher = Sha1::new();
69 let mut hasher = Sha1::new();
68 let (ignore_fn, warnings) = get_ignore_function(
70 let (ignore_fn, warnings) = get_ignore_function(
69 ignore_files,
71 ignore_files,
70 &root_dir,
72 &root_dir,
71 &mut |source, pattern_bytes| {
73 &mut |source, pattern_bytes| {
72 // If inside the repo, use the relative version to
74 // If inside the repo, use the relative version to
73 // make it deterministic inside tests.
75 // make it deterministic inside tests.
74 // The performance hit should be negligible.
76 // The performance hit should be negligible.
75 let source = source
77 let source = source
76 .strip_prefix(&root_dir)
78 .strip_prefix(&root_dir)
77 .unwrap_or(source);
79 .unwrap_or(source);
78 let source = get_bytes_from_path(source);
80 let source = get_bytes_from_path(source);
79
81
80 let mut subhasher = Sha1::new();
82 let mut subhasher = Sha1::new();
81 subhasher.update(pattern_bytes);
83 subhasher.update(pattern_bytes);
82 let patterns_hash = subhasher.finalize();
84 let patterns_hash = subhasher.finalize();
83
85
84 hasher.update(source);
86 hasher.update(source);
85 hasher.update(b" ");
87 hasher.update(b" ");
86 hasher.update(patterns_hash);
88 hasher.update(patterns_hash);
87 hasher.update(b"\n");
89 hasher.update(b"\n");
88 },
90 },
89 )?;
91 )?;
90 let new_hash = *hasher.finalize().as_ref();
92 let new_hash = *hasher.finalize().as_ref();
91 let changed = new_hash != dmap.ignore_patterns_hash;
93 let changed = new_hash != dmap.ignore_patterns_hash;
92 dmap.ignore_patterns_hash = new_hash;
94 dmap.ignore_patterns_hash = new_hash;
93 (ignore_fn, warnings, Some(changed))
95 (ignore_fn, warnings, Some(changed))
94 }
96 }
95 };
97 };
96 (ignore_fn, warnings, changed)
98 (ignore_fn, warnings, changed)
97 } else {
99 } else {
98 (Box::new(|&_| true), vec![], None)
100 (Box::new(|&_| true), vec![], None)
99 };
101 };
100
102
101 let filesystem_time_at_status_start =
103 let filesystem_time_at_status_start =
102 filesystem_now(&root_dir).ok().map(TruncatedTimestamp::from);
104 filesystem_now(&root_dir).ok().map(TruncatedTimestamp::from);
103
105
104 // If the repository is under the current directory, prefer using a
106 // If the repository is under the current directory, prefer using a
105 // relative path, so the kernel needs to traverse fewer directory in every
107 // relative path, so the kernel needs to traverse fewer directory in every
106 // call to `read_dir` or `symlink_metadata`.
108 // call to `read_dir` or `symlink_metadata`.
107 // This is effective in the common case where the current directory is the
109 // This is effective in the common case where the current directory is the
108 // repository root.
110 // repository root.
109
111
110 // TODO: Better yet would be to use libc functions like `openat` and
112 // TODO: Better yet would be to use libc functions like `openat` and
111 // `fstatat` to remove such repeated traversals entirely, but the standard
113 // `fstatat` to remove such repeated traversals entirely, but the standard
112 // library does not provide APIs based on those.
114 // library does not provide APIs based on those.
113 // Maybe with a crate like https://crates.io/crates/openat instead?
115 // Maybe with a crate like https://crates.io/crates/openat instead?
114 let root_dir = if let Some(relative) = std::env::current_dir()
116 let root_dir = if let Some(relative) = std::env::current_dir()
115 .ok()
117 .ok()
116 .and_then(|cwd| root_dir.strip_prefix(cwd).ok())
118 .and_then(|cwd| root_dir.strip_prefix(cwd).ok())
117 {
119 {
118 relative
120 relative
119 } else {
121 } else {
120 &root_dir
122 &root_dir
121 };
123 };
122
124
123 let outcome = DirstateStatus {
125 let outcome = DirstateStatus {
124 filesystem_time_at_status_start,
126 filesystem_time_at_status_start,
125 ..Default::default()
127 ..Default::default()
126 };
128 };
127 let common = StatusCommon {
129 let common = StatusCommon {
128 dmap,
130 dmap,
129 options,
131 options,
130 matcher,
132 matcher,
131 ignore_fn,
133 ignore_fn,
132 outcome: Mutex::new(outcome),
134 outcome: Mutex::new(outcome),
133 ignore_patterns_have_changed: patterns_changed,
135 ignore_patterns_have_changed: patterns_changed,
134 new_cacheable_directories: Default::default(),
136 new_cacheable_directories: Default::default(),
135 outdated_cached_directories: Default::default(),
137 outdated_cached_directories: Default::default(),
136 filesystem_time_at_status_start,
138 filesystem_time_at_status_start,
137 };
139 };
138 let is_at_repo_root = true;
140 let is_at_repo_root = true;
139 let hg_path = &BorrowedPath::OnDisk(HgPath::new(""));
141 let hg_path = &BorrowedPath::OnDisk(HgPath::new(""));
140 let has_ignored_ancestor = HasIgnoredAncestor::create(None, hg_path);
142 let has_ignored_ancestor = HasIgnoredAncestor::create(None, hg_path);
141 let root_cached_mtime = None;
143 let root_cached_mtime = None;
142 // If the path we have for the repository root is a symlink, do follow it.
144 // If the path we have for the repository root is a symlink, do follow it.
143 // (As opposed to symlinks within the working directory which are not
145 // (As opposed to symlinks within the working directory which are not
144 // followed, using `std::fs::symlink_metadata`.)
146 // followed, using `std::fs::symlink_metadata`.)
145 common.traverse_fs_directory_and_dirstate(
147 common.traverse_fs_directory_and_dirstate(
146 &has_ignored_ancestor,
148 &has_ignored_ancestor,
147 dmap.root.as_ref(),
149 dmap.root.as_ref(),
148 hg_path,
150 hg_path,
149 &DirEntry {
151 &DirEntry {
150 hg_path: Cow::Borrowed(HgPath::new(b"")),
152 hg_path: Cow::Borrowed(HgPath::new(b"")),
151 fs_path: Cow::Borrowed(root_dir),
153 fs_path: Cow::Borrowed(root_dir),
152 symlink_metadata: None,
154 symlink_metadata: None,
153 file_type: FakeFileType::Directory,
155 file_type: FakeFileType::Directory,
154 },
156 },
155 root_cached_mtime,
157 root_cached_mtime,
156 is_at_repo_root,
158 is_at_repo_root,
157 )?;
159 )?;
158 let mut outcome = common.outcome.into_inner().unwrap();
160 let mut outcome = common.outcome.into_inner().unwrap();
159 let new_cacheable = common.new_cacheable_directories.into_inner().unwrap();
161 let new_cacheable = common.new_cacheable_directories.into_inner().unwrap();
160 let outdated = common.outdated_cached_directories.into_inner().unwrap();
162 let outdated = common.outdated_cached_directories.into_inner().unwrap();
161
163
162 outcome.dirty = common.ignore_patterns_have_changed == Some(true)
164 outcome.dirty = common.ignore_patterns_have_changed == Some(true)
163 || !outdated.is_empty()
165 || !outdated.is_empty()
164 || (!new_cacheable.is_empty()
166 || (!new_cacheable.is_empty()
165 && dmap.dirstate_version == DirstateVersion::V2);
167 && dmap.dirstate_version == DirstateVersion::V2);
166
168
167 // Remove outdated mtimes before adding new mtimes, in case a given
169 // Remove outdated mtimes before adding new mtimes, in case a given
168 // directory is both
170 // directory is both
169 for path in &outdated {
171 for path in &outdated {
170 dmap.clear_cached_mtime(path)?;
172 dmap.clear_cached_mtime(path)?;
171 }
173 }
172 for (path, mtime) in &new_cacheable {
174 for (path, mtime) in &new_cacheable {
173 dmap.set_cached_mtime(path, *mtime)?;
175 dmap.set_cached_mtime(path, *mtime)?;
174 }
176 }
175
177
176 Ok((outcome, warnings))
178 Ok((outcome, warnings))
177 }
179 }
178
180
179 /// Bag of random things needed by various parts of the algorithm. Reduces the
181 /// Bag of random things needed by various parts of the algorithm. Reduces the
180 /// number of parameters passed to functions.
182 /// number of parameters passed to functions.
181 struct StatusCommon<'a, 'tree, 'on_disk: 'tree> {
183 struct StatusCommon<'a, 'tree, 'on_disk: 'tree> {
182 dmap: &'tree DirstateMap<'on_disk>,
184 dmap: &'tree DirstateMap<'on_disk>,
183 options: StatusOptions,
185 options: StatusOptions,
184 matcher: &'a (dyn Matcher + Sync),
186 matcher: &'a (dyn Matcher + Sync),
185 ignore_fn: IgnoreFnType<'a>,
187 ignore_fn: IgnoreFnType<'a>,
186 outcome: Mutex<DirstateStatus<'on_disk>>,
188 outcome: Mutex<DirstateStatus<'on_disk>>,
187 /// New timestamps of directories to be used for caching their readdirs
189 /// New timestamps of directories to be used for caching their readdirs
188 new_cacheable_directories:
190 new_cacheable_directories:
189 Mutex<Vec<(Cow<'on_disk, HgPath>, TruncatedTimestamp)>>,
191 Mutex<Vec<(Cow<'on_disk, HgPath>, TruncatedTimestamp)>>,
190 /// Used to invalidate the readdir cache of directories
192 /// Used to invalidate the readdir cache of directories
191 outdated_cached_directories: Mutex<Vec<Cow<'on_disk, HgPath>>>,
193 outdated_cached_directories: Mutex<Vec<Cow<'on_disk, HgPath>>>,
192
194
193 /// Whether ignore files like `.hgignore` have changed since the previous
195 /// Whether ignore files like `.hgignore` have changed since the previous
194 /// time a `status()` call wrote their hash to the dirstate. `None` means
196 /// time a `status()` call wrote their hash to the dirstate. `None` means
195 /// we don’t know as this run doesn’t list either ignored or uknown files
197 /// we don’t know as this run doesn’t list either ignored or uknown files
196 /// and therefore isn’t reading `.hgignore`.
198 /// and therefore isn’t reading `.hgignore`.
197 ignore_patterns_have_changed: Option<bool>,
199 ignore_patterns_have_changed: Option<bool>,
198
200
199 /// The current time at the start of the `status()` algorithm, as measured
201 /// The current time at the start of the `status()` algorithm, as measured
200 /// and possibly truncated by the filesystem.
202 /// and possibly truncated by the filesystem.
201 filesystem_time_at_status_start: Option<TruncatedTimestamp>,
203 filesystem_time_at_status_start: Option<TruncatedTimestamp>,
202 }
204 }
203
205
204 enum Outcome {
206 enum Outcome {
205 Modified,
207 Modified,
206 Added,
208 Added,
207 Removed,
209 Removed,
208 Deleted,
210 Deleted,
209 Clean,
211 Clean,
210 Ignored,
212 Ignored,
211 Unknown,
213 Unknown,
212 Unsure,
214 Unsure,
213 }
215 }
214
216
215 /// Lazy computation of whether a given path has a hgignored
217 /// Lazy computation of whether a given path has a hgignored
216 /// ancestor.
218 /// ancestor.
217 struct HasIgnoredAncestor<'a> {
219 struct HasIgnoredAncestor<'a> {
218 /// `path` and `parent` constitute the inputs to the computation,
220 /// `path` and `parent` constitute the inputs to the computation,
219 /// `cache` stores the outcome.
221 /// `cache` stores the outcome.
220 path: &'a HgPath,
222 path: &'a HgPath,
221 parent: Option<&'a HasIgnoredAncestor<'a>>,
223 parent: Option<&'a HasIgnoredAncestor<'a>>,
222 cache: OnceCell<bool>,
224 cache: OnceCell<bool>,
223 }
225 }
224
226
225 impl<'a> HasIgnoredAncestor<'a> {
227 impl<'a> HasIgnoredAncestor<'a> {
226 fn create(
228 fn create(
227 parent: Option<&'a HasIgnoredAncestor<'a>>,
229 parent: Option<&'a HasIgnoredAncestor<'a>>,
228 path: &'a HgPath,
230 path: &'a HgPath,
229 ) -> HasIgnoredAncestor<'a> {
231 ) -> HasIgnoredAncestor<'a> {
230 Self {
232 Self {
231 path,
233 path,
232 parent,
234 parent,
233 cache: OnceCell::new(),
235 cache: OnceCell::new(),
234 }
236 }
235 }
237 }
236
238
237 fn force<'b>(&self, ignore_fn: &IgnoreFnType<'b>) -> bool {
239 fn force<'b>(&self, ignore_fn: &IgnoreFnType<'b>) -> bool {
238 match self.parent {
240 match self.parent {
239 None => false,
241 None => false,
240 Some(parent) => {
242 Some(parent) => {
241 *(self.cache.get_or_init(|| {
243 *(self.cache.get_or_init(|| {
242 parent.force(ignore_fn) || ignore_fn(self.path)
244 parent.force(ignore_fn) || ignore_fn(self.path)
243 }))
245 }))
244 }
246 }
245 }
247 }
246 }
248 }
247 }
249 }
248
250
249 impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> {
251 impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> {
250 fn push_outcome(
252 fn push_outcome(
251 &self,
253 &self,
252 which: Outcome,
254 which: Outcome,
253 dirstate_node: &NodeRef<'tree, 'on_disk>,
255 dirstate_node: &NodeRef<'tree, 'on_disk>,
254 ) -> Result<(), DirstateV2ParseError> {
256 ) -> Result<(), DirstateV2ParseError> {
255 let path = dirstate_node
257 let path = dirstate_node
256 .full_path_borrowed(self.dmap.on_disk)?
258 .full_path_borrowed(self.dmap.on_disk)?
257 .detach_from_tree();
259 .detach_from_tree();
258 let copy_source = if self.options.list_copies {
260 let copy_source = if self.options.list_copies {
259 dirstate_node
261 dirstate_node
260 .copy_source_borrowed(self.dmap.on_disk)?
262 .copy_source_borrowed(self.dmap.on_disk)?
261 .map(|source| source.detach_from_tree())
263 .map(|source| source.detach_from_tree())
262 } else {
264 } else {
263 None
265 None
264 };
266 };
265 self.push_outcome_common(which, path, copy_source);
267 self.push_outcome_common(which, path, copy_source);
266 Ok(())
268 Ok(())
267 }
269 }
268
270
269 fn push_outcome_without_copy_source(
271 fn push_outcome_without_copy_source(
270 &self,
272 &self,
271 which: Outcome,
273 which: Outcome,
272 path: &BorrowedPath<'_, 'on_disk>,
274 path: &BorrowedPath<'_, 'on_disk>,
273 ) {
275 ) {
274 self.push_outcome_common(which, path.detach_from_tree(), None)
276 self.push_outcome_common(which, path.detach_from_tree(), None)
275 }
277 }
276
278
277 fn push_outcome_common(
279 fn push_outcome_common(
278 &self,
280 &self,
279 which: Outcome,
281 which: Outcome,
280 path: HgPathCow<'on_disk>,
282 path: HgPathCow<'on_disk>,
281 copy_source: Option<HgPathCow<'on_disk>>,
283 copy_source: Option<HgPathCow<'on_disk>>,
282 ) {
284 ) {
283 let mut outcome = self.outcome.lock().unwrap();
285 let mut outcome = self.outcome.lock().unwrap();
284 let vec = match which {
286 let vec = match which {
285 Outcome::Modified => &mut outcome.modified,
287 Outcome::Modified => &mut outcome.modified,
286 Outcome::Added => &mut outcome.added,
288 Outcome::Added => &mut outcome.added,
287 Outcome::Removed => &mut outcome.removed,
289 Outcome::Removed => &mut outcome.removed,
288 Outcome::Deleted => &mut outcome.deleted,
290 Outcome::Deleted => &mut outcome.deleted,
289 Outcome::Clean => &mut outcome.clean,
291 Outcome::Clean => &mut outcome.clean,
290 Outcome::Ignored => &mut outcome.ignored,
292 Outcome::Ignored => &mut outcome.ignored,
291 Outcome::Unknown => &mut outcome.unknown,
293 Outcome::Unknown => &mut outcome.unknown,
292 Outcome::Unsure => &mut outcome.unsure,
294 Outcome::Unsure => &mut outcome.unsure,
293 };
295 };
294 vec.push(StatusPath { path, copy_source });
296 vec.push(StatusPath { path, copy_source });
295 }
297 }
296
298
297 fn read_dir(
299 fn read_dir(
298 &self,
300 &self,
299 hg_path: &HgPath,
301 hg_path: &HgPath,
300 fs_path: &Path,
302 fs_path: &Path,
301 is_at_repo_root: bool,
303 is_at_repo_root: bool,
302 ) -> Result<Vec<DirEntry>, ()> {
304 ) -> Result<Vec<DirEntry>, ()> {
303 DirEntry::read_dir(fs_path, is_at_repo_root)
305 DirEntry::read_dir(fs_path, is_at_repo_root)
304 .map_err(|error| self.io_error(error, hg_path))
306 .map_err(|error| self.io_error(error, hg_path))
305 }
307 }
306
308
307 fn io_error(&self, error: std::io::Error, hg_path: &HgPath) {
309 fn io_error(&self, error: std::io::Error, hg_path: &HgPath) {
308 let errno = error.raw_os_error().expect("expected real OS error");
310 let errno = error.raw_os_error().expect("expected real OS error");
309 self.outcome
311 self.outcome
310 .lock()
312 .lock()
311 .unwrap()
313 .unwrap()
312 .bad
314 .bad
313 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
315 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
314 }
316 }
315
317
316 fn check_for_outdated_directory_cache(
318 fn check_for_outdated_directory_cache(
317 &self,
319 &self,
318 dirstate_node: &NodeRef<'tree, 'on_disk>,
320 dirstate_node: &NodeRef<'tree, 'on_disk>,
319 ) -> Result<bool, DirstateV2ParseError> {
321 ) -> Result<bool, DirstateV2ParseError> {
320 if self.ignore_patterns_have_changed == Some(true)
322 if self.ignore_patterns_have_changed == Some(true)
321 && dirstate_node.cached_directory_mtime()?.is_some()
323 && dirstate_node.cached_directory_mtime()?.is_some()
322 {
324 {
323 self.outdated_cached_directories.lock().unwrap().push(
325 self.outdated_cached_directories.lock().unwrap().push(
324 dirstate_node
326 dirstate_node
325 .full_path_borrowed(self.dmap.on_disk)?
327 .full_path_borrowed(self.dmap.on_disk)?
326 .detach_from_tree(),
328 .detach_from_tree(),
327 );
329 );
328 return Ok(true);
330 return Ok(true);
329 }
331 }
330 Ok(false)
332 Ok(false)
331 }
333 }
332
334
333 /// If this returns true, we can get accurate results by only using
335 /// If this returns true, we can get accurate results by only using
334 /// `symlink_metadata` for child nodes that exist in the dirstate and don’t
336 /// `symlink_metadata` for child nodes that exist in the dirstate and don’t
335 /// need to call `read_dir`.
337 /// need to call `read_dir`.
336 fn can_skip_fs_readdir(
338 fn can_skip_fs_readdir(
337 &self,
339 &self,
338 directory_entry: &DirEntry,
340 directory_entry: &DirEntry,
339 cached_directory_mtime: Option<TruncatedTimestamp>,
341 cached_directory_mtime: Option<TruncatedTimestamp>,
340 ) -> bool {
342 ) -> bool {
341 if !self.options.list_unknown && !self.options.list_ignored {
343 if !self.options.list_unknown && !self.options.list_ignored {
342 // All states that we care about listing have corresponding
344 // All states that we care about listing have corresponding
343 // dirstate entries.
345 // dirstate entries.
344 // This happens for example with `hg status -mard`.
346 // This happens for example with `hg status -mard`.
345 return true;
347 return true;
346 }
348 }
347 if !self.options.list_ignored
349 if !self.options.list_ignored
348 && self.ignore_patterns_have_changed == Some(false)
350 && self.ignore_patterns_have_changed == Some(false)
349 {
351 {
350 if let Some(cached_mtime) = cached_directory_mtime {
352 if let Some(cached_mtime) = cached_directory_mtime {
351 // The dirstate contains a cached mtime for this directory, set
353 // The dirstate contains a cached mtime for this directory, set
352 // by a previous run of the `status` algorithm which found this
354 // by a previous run of the `status` algorithm which found this
353 // directory eligible for `read_dir` caching.
355 // directory eligible for `read_dir` caching.
354 if let Ok(meta) = directory_entry.symlink_metadata() {
356 if let Ok(meta) = directory_entry.symlink_metadata() {
355 if cached_mtime
357 if cached_mtime
356 .likely_equal_to_mtime_of(&meta)
358 .likely_equal_to_mtime_of(&meta)
357 .unwrap_or(false)
359 .unwrap_or(false)
358 {
360 {
359 // The mtime of that directory has not changed
361 // The mtime of that directory has not changed
360 // since then, which means that the results of
362 // since then, which means that the results of
361 // `read_dir` should also be unchanged.
363 // `read_dir` should also be unchanged.
362 return true;
364 return true;
363 }
365 }
364 }
366 }
365 }
367 }
366 }
368 }
367 false
369 false
368 }
370 }
369
371
370 /// Returns whether all child entries of the filesystem directory have a
372 /// Returns whether all child entries of the filesystem directory have a
371 /// corresponding dirstate node or are ignored.
373 /// corresponding dirstate node or are ignored.
372 fn traverse_fs_directory_and_dirstate<'ancestor>(
374 fn traverse_fs_directory_and_dirstate<'ancestor>(
373 &self,
375 &self,
374 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>,
376 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>,
375 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>,
377 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>,
376 directory_hg_path: &BorrowedPath<'tree, 'on_disk>,
378 directory_hg_path: &BorrowedPath<'tree, 'on_disk>,
377 directory_entry: &DirEntry,
379 directory_entry: &DirEntry,
378 cached_directory_mtime: Option<TruncatedTimestamp>,
380 cached_directory_mtime: Option<TruncatedTimestamp>,
379 is_at_repo_root: bool,
381 is_at_repo_root: bool,
380 ) -> Result<bool, DirstateV2ParseError> {
382 ) -> Result<bool, DirstateV2ParseError> {
381 if self.can_skip_fs_readdir(directory_entry, cached_directory_mtime) {
383 if self.can_skip_fs_readdir(directory_entry, cached_directory_mtime) {
382 dirstate_nodes
384 dirstate_nodes
383 .par_iter()
385 .par_iter()
384 .map(|dirstate_node| {
386 .map(|dirstate_node| {
385 let fs_path = &directory_entry.fs_path;
387 let fs_path = &directory_entry.fs_path;
386 let fs_path = fs_path.join(get_path_from_bytes(
388 let fs_path = fs_path.join(get_path_from_bytes(
387 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(),
389 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(),
388 ));
390 ));
389 match std::fs::symlink_metadata(&fs_path) {
391 match std::fs::symlink_metadata(&fs_path) {
390 Ok(fs_metadata) => {
392 Ok(fs_metadata) => {
391 let file_type =
393 let file_type = fs_metadata.file_type().into();
392 match fs_metadata.file_type().try_into() {
393 Ok(file_type) => file_type,
394 Err(_) => return Ok(()),
395 };
396 let entry = DirEntry {
394 let entry = DirEntry {
397 hg_path: Cow::Borrowed(
395 hg_path: Cow::Borrowed(
398 dirstate_node
396 dirstate_node
399 .full_path(self.dmap.on_disk)?,
397 .full_path(self.dmap.on_disk)?,
400 ),
398 ),
401 fs_path: Cow::Borrowed(&fs_path),
399 fs_path: Cow::Borrowed(&fs_path),
402 symlink_metadata: Some(fs_metadata),
400 symlink_metadata: Some(fs_metadata),
403 file_type,
401 file_type,
404 };
402 };
405 self.traverse_fs_and_dirstate(
403 self.traverse_fs_and_dirstate(
406 &entry,
404 &entry,
407 dirstate_node,
405 dirstate_node,
408 has_ignored_ancestor,
406 has_ignored_ancestor,
409 )
407 )
410 }
408 }
411 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
409 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
412 self.traverse_dirstate_only(dirstate_node)
410 self.traverse_dirstate_only(dirstate_node)
413 }
411 }
414 Err(error) => {
412 Err(error) => {
415 let hg_path =
413 let hg_path =
416 dirstate_node.full_path(self.dmap.on_disk)?;
414 dirstate_node.full_path(self.dmap.on_disk)?;
417 self.io_error(error, hg_path);
415 self.io_error(error, hg_path);
418 Ok(())
416 Ok(())
419 }
417 }
420 }
418 }
421 })
419 })
422 .collect::<Result<_, _>>()?;
420 .collect::<Result<_, _>>()?;
423
421
424 // We don’t know, so conservatively say this isn’t the case
422 // We don’t know, so conservatively say this isn’t the case
425 let children_all_have_dirstate_node_or_are_ignored = false;
423 let children_all_have_dirstate_node_or_are_ignored = false;
426
424
427 return Ok(children_all_have_dirstate_node_or_are_ignored);
425 return Ok(children_all_have_dirstate_node_or_are_ignored);
428 }
426 }
429
427
430 let readdir_succeeded;
428 let readdir_succeeded;
431 let mut fs_entries = if let Ok(entries) = self.read_dir(
429 let mut fs_entries = if let Ok(entries) = self.read_dir(
432 directory_hg_path,
430 directory_hg_path,
433 &directory_entry.fs_path,
431 &directory_entry.fs_path,
434 is_at_repo_root,
432 is_at_repo_root,
435 ) {
433 ) {
436 readdir_succeeded = true;
434 readdir_succeeded = true;
437 entries
435 entries
438 } else {
436 } else {
439 // Treat an unreadable directory (typically because of insufficient
437 // Treat an unreadable directory (typically because of insufficient
440 // permissions) like an empty directory. `self.read_dir` has
438 // permissions) like an empty directory. `self.read_dir` has
441 // already called `self.io_error` so a warning will be emitted.
439 // already called `self.io_error` so a warning will be emitted.
442 // We still need to remember that there was an error so that we
440 // We still need to remember that there was an error so that we
443 // know not to cache this result.
441 // know not to cache this result.
444 readdir_succeeded = false;
442 readdir_succeeded = false;
445 Vec::new()
443 Vec::new()
446 };
444 };
447
445
448 // `merge_join_by` requires both its input iterators to be sorted:
446 // `merge_join_by` requires both its input iterators to be sorted:
449
447
450 let dirstate_nodes = dirstate_nodes.sorted();
448 let dirstate_nodes = dirstate_nodes.sorted();
451 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
449 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
452 // https://github.com/rust-lang/rust/issues/34162
450 // https://github.com/rust-lang/rust/issues/34162
453 fs_entries.sort_unstable_by(|e1, e2| e1.hg_path.cmp(&e2.hg_path));
451 fs_entries.sort_unstable_by(|e1, e2| e1.hg_path.cmp(&e2.hg_path));
454
452
455 // Propagate here any error that would happen inside the comparison
453 // Propagate here any error that would happen inside the comparison
456 // callback below
454 // callback below
457 for dirstate_node in &dirstate_nodes {
455 for dirstate_node in &dirstate_nodes {
458 dirstate_node.base_name(self.dmap.on_disk)?;
456 dirstate_node.base_name(self.dmap.on_disk)?;
459 }
457 }
460 itertools::merge_join_by(
458 itertools::merge_join_by(
461 dirstate_nodes,
459 dirstate_nodes,
462 &fs_entries,
460 &fs_entries,
463 |dirstate_node, fs_entry| {
461 |dirstate_node, fs_entry| {
464 // This `unwrap` never panics because we already propagated
462 // This `unwrap` never panics because we already propagated
465 // those errors above
463 // those errors above
466 dirstate_node
464 dirstate_node
467 .base_name(self.dmap.on_disk)
465 .base_name(self.dmap.on_disk)
468 .unwrap()
466 .unwrap()
469 .cmp(&fs_entry.hg_path)
467 .cmp(&fs_entry.hg_path)
470 },
468 },
471 )
469 )
472 .par_bridge()
470 .par_bridge()
473 .map(|pair| {
471 .map(|pair| {
474 use itertools::EitherOrBoth::*;
472 use itertools::EitherOrBoth::*;
475 let has_dirstate_node_or_is_ignored = match pair {
473 let has_dirstate_node_or_is_ignored = match pair {
476 Both(dirstate_node, fs_entry) => {
474 Both(dirstate_node, fs_entry) => {
477 self.traverse_fs_and_dirstate(
475 self.traverse_fs_and_dirstate(
478 fs_entry,
476 fs_entry,
479 dirstate_node,
477 dirstate_node,
480 has_ignored_ancestor,
478 has_ignored_ancestor,
481 )?;
479 )?;
482 true
480 true
483 }
481 }
484 Left(dirstate_node) => {
482 Left(dirstate_node) => {
485 self.traverse_dirstate_only(dirstate_node)?;
483 self.traverse_dirstate_only(dirstate_node)?;
486 true
484 true
487 }
485 }
488 Right(fs_entry) => self.traverse_fs_only(
486 Right(fs_entry) => self.traverse_fs_only(
489 has_ignored_ancestor.force(&self.ignore_fn),
487 has_ignored_ancestor.force(&self.ignore_fn),
490 directory_hg_path,
488 directory_hg_path,
491 fs_entry,
489 fs_entry,
492 ),
490 ),
493 };
491 };
494 Ok(has_dirstate_node_or_is_ignored)
492 Ok(has_dirstate_node_or_is_ignored)
495 })
493 })
496 .try_reduce(|| true, |a, b| Ok(a && b))
494 .try_reduce(|| true, |a, b| Ok(a && b))
497 .map(|res| res && readdir_succeeded)
495 .map(|res| res && readdir_succeeded)
498 }
496 }
499
497
500 fn traverse_fs_and_dirstate<'ancestor>(
498 fn traverse_fs_and_dirstate<'ancestor>(
501 &self,
499 &self,
502 fs_entry: &DirEntry,
500 fs_entry: &DirEntry,
503 dirstate_node: NodeRef<'tree, 'on_disk>,
501 dirstate_node: NodeRef<'tree, 'on_disk>,
504 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>,
502 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>,
505 ) -> Result<(), DirstateV2ParseError> {
503 ) -> Result<(), DirstateV2ParseError> {
506 let outdated_dircache =
504 let outdated_dircache =
507 self.check_for_outdated_directory_cache(&dirstate_node)?;
505 self.check_for_outdated_directory_cache(&dirstate_node)?;
508 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
506 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
509 let file_or_symlink = fs_entry.is_file() || fs_entry.is_symlink();
507 let file_or_symlink = fs_entry.is_file() || fs_entry.is_symlink();
510 if !file_or_symlink {
508 if !file_or_symlink {
511 // If we previously had a file here, it was removed (with
509 // If we previously had a file here, it was removed (with
512 // `hg rm` or similar) or deleted before it could be
510 // `hg rm` or similar) or deleted before it could be
513 // replaced by a directory or something else.
511 // replaced by a directory or something else.
514 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
512 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
515 }
513 }
514 if let Some(bad_type) = fs_entry.is_bad() {
515 if self.matcher.exact_match(hg_path) {
516 let path = dirstate_node.full_path(self.dmap.on_disk)?;
517 self.outcome.lock().unwrap().bad.push((
518 path.to_owned().into(),
519 BadMatch::BadType(bad_type),
520 ))
521 }
522 }
516 if fs_entry.is_dir() {
523 if fs_entry.is_dir() {
517 if self.options.collect_traversed_dirs {
524 if self.options.collect_traversed_dirs {
518 self.outcome
525 self.outcome
519 .lock()
526 .lock()
520 .unwrap()
527 .unwrap()
521 .traversed
528 .traversed
522 .push(hg_path.detach_from_tree())
529 .push(hg_path.detach_from_tree())
523 }
530 }
524 let is_ignored = HasIgnoredAncestor::create(
531 let is_ignored = HasIgnoredAncestor::create(
525 Some(has_ignored_ancestor),
532 Some(has_ignored_ancestor),
526 hg_path,
533 hg_path,
527 );
534 );
528 let is_at_repo_root = false;
535 let is_at_repo_root = false;
529 let children_all_have_dirstate_node_or_are_ignored = self
536 let children_all_have_dirstate_node_or_are_ignored = self
530 .traverse_fs_directory_and_dirstate(
537 .traverse_fs_directory_and_dirstate(
531 &is_ignored,
538 &is_ignored,
532 dirstate_node.children(self.dmap.on_disk)?,
539 dirstate_node.children(self.dmap.on_disk)?,
533 hg_path,
540 hg_path,
534 fs_entry,
541 fs_entry,
535 dirstate_node.cached_directory_mtime()?,
542 dirstate_node.cached_directory_mtime()?,
536 is_at_repo_root,
543 is_at_repo_root,
537 )?;
544 )?;
538 self.maybe_save_directory_mtime(
545 self.maybe_save_directory_mtime(
539 children_all_have_dirstate_node_or_are_ignored,
546 children_all_have_dirstate_node_or_are_ignored,
540 fs_entry,
547 fs_entry,
541 dirstate_node,
548 dirstate_node,
542 outdated_dircache,
549 outdated_dircache,
543 )?
550 )?
544 } else {
551 } else {
545 if file_or_symlink && self.matcher.matches(hg_path) {
552 if file_or_symlink && self.matcher.matches(hg_path) {
546 if let Some(entry) = dirstate_node.entry()? {
553 if let Some(entry) = dirstate_node.entry()? {
547 if !entry.any_tracked() {
554 if !entry.any_tracked() {
548 // Forward-compat if we start tracking unknown/ignored
555 // Forward-compat if we start tracking unknown/ignored
549 // files for caching reasons
556 // files for caching reasons
550 self.mark_unknown_or_ignored(
557 self.mark_unknown_or_ignored(
551 has_ignored_ancestor.force(&self.ignore_fn),
558 has_ignored_ancestor.force(&self.ignore_fn),
552 hg_path,
559 hg_path,
553 );
560 );
554 }
561 }
555 if entry.added() {
562 if entry.added() {
556 self.push_outcome(Outcome::Added, &dirstate_node)?;
563 self.push_outcome(Outcome::Added, &dirstate_node)?;
557 } else if entry.removed() {
564 } else if entry.removed() {
558 self.push_outcome(Outcome::Removed, &dirstate_node)?;
565 self.push_outcome(Outcome::Removed, &dirstate_node)?;
559 } else if entry.modified() {
566 } else if entry.modified() {
560 self.push_outcome(Outcome::Modified, &dirstate_node)?;
567 self.push_outcome(Outcome::Modified, &dirstate_node)?;
561 } else {
568 } else {
562 self.handle_normal_file(&dirstate_node, fs_entry)?;
569 self.handle_normal_file(&dirstate_node, fs_entry)?;
563 }
570 }
564 } else {
571 } else {
565 // `node.entry.is_none()` indicates a "directory"
572 // `node.entry.is_none()` indicates a "directory"
566 // node, but the filesystem has a file
573 // node, but the filesystem has a file
567 self.mark_unknown_or_ignored(
574 self.mark_unknown_or_ignored(
568 has_ignored_ancestor.force(&self.ignore_fn),
575 has_ignored_ancestor.force(&self.ignore_fn),
569 hg_path,
576 hg_path,
570 );
577 );
571 }
578 }
572 }
579 }
573
580
574 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter()
581 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter()
575 {
582 {
576 self.traverse_dirstate_only(child_node)?
583 self.traverse_dirstate_only(child_node)?
577 }
584 }
578 }
585 }
579 Ok(())
586 Ok(())
580 }
587 }
581
588
582 /// Save directory mtime if applicable.
589 /// Save directory mtime if applicable.
583 ///
590 ///
584 /// `outdated_directory_cache` is `true` if we've just invalidated the
591 /// `outdated_directory_cache` is `true` if we've just invalidated the
585 /// cache for this directory in `check_for_outdated_directory_cache`,
592 /// cache for this directory in `check_for_outdated_directory_cache`,
586 /// which forces the update.
593 /// which forces the update.
587 fn maybe_save_directory_mtime(
594 fn maybe_save_directory_mtime(
588 &self,
595 &self,
589 children_all_have_dirstate_node_or_are_ignored: bool,
596 children_all_have_dirstate_node_or_are_ignored: bool,
590 directory_entry: &DirEntry,
597 directory_entry: &DirEntry,
591 dirstate_node: NodeRef<'tree, 'on_disk>,
598 dirstate_node: NodeRef<'tree, 'on_disk>,
592 outdated_directory_cache: bool,
599 outdated_directory_cache: bool,
593 ) -> Result<(), DirstateV2ParseError> {
600 ) -> Result<(), DirstateV2ParseError> {
594 if !children_all_have_dirstate_node_or_are_ignored {
601 if !children_all_have_dirstate_node_or_are_ignored {
595 return Ok(());
602 return Ok(());
596 }
603 }
597 // All filesystem directory entries from `read_dir` have a
604 // All filesystem directory entries from `read_dir` have a
598 // corresponding node in the dirstate, so we can reconstitute the
605 // corresponding node in the dirstate, so we can reconstitute the
599 // names of those entries without calling `read_dir` again.
606 // names of those entries without calling `read_dir` again.
600
607
601 // TODO: use let-else here and below when available:
608 // TODO: use let-else here and below when available:
602 // https://github.com/rust-lang/rust/issues/87335
609 // https://github.com/rust-lang/rust/issues/87335
603 let status_start = if let Some(status_start) =
610 let status_start = if let Some(status_start) =
604 &self.filesystem_time_at_status_start
611 &self.filesystem_time_at_status_start
605 {
612 {
606 status_start
613 status_start
607 } else {
614 } else {
608 return Ok(());
615 return Ok(());
609 };
616 };
610
617
611 // Although the Rust standard library’s `SystemTime` type
618 // Although the Rust standard library’s `SystemTime` type
612 // has nanosecond precision, the times reported for a
619 // has nanosecond precision, the times reported for a
613 // directory’s (or file’s) modified time may have lower
620 // directory’s (or file’s) modified time may have lower
614 // resolution based on the filesystem (for example ext3
621 // resolution based on the filesystem (for example ext3
615 // only stores integer seconds), kernel (see
622 // only stores integer seconds), kernel (see
616 // https://stackoverflow.com/a/14393315/1162888), etc.
623 // https://stackoverflow.com/a/14393315/1162888), etc.
617 let metadata = match directory_entry.symlink_metadata() {
624 let metadata = match directory_entry.symlink_metadata() {
618 Ok(meta) => meta,
625 Ok(meta) => meta,
619 Err(_) => return Ok(()),
626 Err(_) => return Ok(()),
620 };
627 };
621
628
622 let directory_mtime = match TruncatedTimestamp::for_reliable_mtime_of(
629 let directory_mtime = match TruncatedTimestamp::for_reliable_mtime_of(
623 &metadata,
630 &metadata,
624 status_start,
631 status_start,
625 ) {
632 ) {
626 Ok(Some(directory_mtime)) => directory_mtime,
633 Ok(Some(directory_mtime)) => directory_mtime,
627 Ok(None) => {
634 Ok(None) => {
628 // The directory was modified too recently,
635 // The directory was modified too recently,
629 // don’t cache its `read_dir` results.
636 // don’t cache its `read_dir` results.
630 //
637 //
631 // 1. A change to this directory (direct child was
638 // 1. A change to this directory (direct child was
632 // added or removed) cause its mtime to be set
639 // added or removed) cause its mtime to be set
633 // (possibly truncated) to `directory_mtime`
640 // (possibly truncated) to `directory_mtime`
634 // 2. This `status` algorithm calls `read_dir`
641 // 2. This `status` algorithm calls `read_dir`
635 // 3. An other change is made to the same directory is
642 // 3. An other change is made to the same directory is
636 // made so that calling `read_dir` agin would give
643 // made so that calling `read_dir` agin would give
637 // different results, but soon enough after 1. that
644 // different results, but soon enough after 1. that
638 // the mtime stays the same
645 // the mtime stays the same
639 //
646 //
640 // On a system where the time resolution poor, this
647 // On a system where the time resolution poor, this
641 // scenario is not unlikely if all three steps are caused
648 // scenario is not unlikely if all three steps are caused
642 // by the same script.
649 // by the same script.
643 return Ok(());
650 return Ok(());
644 }
651 }
645 Err(_) => {
652 Err(_) => {
646 // OS/libc does not support mtime?
653 // OS/libc does not support mtime?
647 return Ok(());
654 return Ok(());
648 }
655 }
649 };
656 };
650 // We’ve observed (through `status_start`) that time has
657 // We’ve observed (through `status_start`) that time has
651 // “progressed” since `directory_mtime`, so any further
658 // “progressed” since `directory_mtime`, so any further
652 // change to this directory is extremely likely to cause a
659 // change to this directory is extremely likely to cause a
653 // different mtime.
660 // different mtime.
654 //
661 //
655 // Having the same mtime again is not entirely impossible
662 // Having the same mtime again is not entirely impossible
656 // since the system clock is not monotonous. It could jump
663 // since the system clock is not monotonous. It could jump
657 // backward to some point before `directory_mtime`, then a
664 // backward to some point before `directory_mtime`, then a
658 // directory change could potentially happen during exactly
665 // directory change could potentially happen during exactly
659 // the wrong tick.
666 // the wrong tick.
660 //
667 //
661 // We deem this scenario (unlike the previous one) to be
668 // We deem this scenario (unlike the previous one) to be
662 // unlikely enough in practice.
669 // unlikely enough in practice.
663
670
664 let is_up_to_date = if let Some(cached) =
671 let is_up_to_date = if let Some(cached) =
665 dirstate_node.cached_directory_mtime()?
672 dirstate_node.cached_directory_mtime()?
666 {
673 {
667 !outdated_directory_cache && cached.likely_equal(directory_mtime)
674 !outdated_directory_cache && cached.likely_equal(directory_mtime)
668 } else {
675 } else {
669 false
676 false
670 };
677 };
671 if !is_up_to_date {
678 if !is_up_to_date {
672 let hg_path = dirstate_node
679 let hg_path = dirstate_node
673 .full_path_borrowed(self.dmap.on_disk)?
680 .full_path_borrowed(self.dmap.on_disk)?
674 .detach_from_tree();
681 .detach_from_tree();
675 self.new_cacheable_directories
682 self.new_cacheable_directories
676 .lock()
683 .lock()
677 .unwrap()
684 .unwrap()
678 .push((hg_path, directory_mtime))
685 .push((hg_path, directory_mtime))
679 }
686 }
680 Ok(())
687 Ok(())
681 }
688 }
682
689
683 /// A file that is clean in the dirstate was found in the filesystem
690 /// A file that is clean in the dirstate was found in the filesystem
684 fn handle_normal_file(
691 fn handle_normal_file(
685 &self,
692 &self,
686 dirstate_node: &NodeRef<'tree, 'on_disk>,
693 dirstate_node: &NodeRef<'tree, 'on_disk>,
687 fs_entry: &DirEntry,
694 fs_entry: &DirEntry,
688 ) -> Result<(), DirstateV2ParseError> {
695 ) -> Result<(), DirstateV2ParseError> {
689 // Keep the low 31 bits
696 // Keep the low 31 bits
690 fn truncate_u64(value: u64) -> i32 {
697 fn truncate_u64(value: u64) -> i32 {
691 (value & 0x7FFF_FFFF) as i32
698 (value & 0x7FFF_FFFF) as i32
692 }
699 }
693
700
694 let fs_metadata = match fs_entry.symlink_metadata() {
701 let fs_metadata = match fs_entry.symlink_metadata() {
695 Ok(meta) => meta,
702 Ok(meta) => meta,
696 Err(_) => return Ok(()),
703 Err(_) => return Ok(()),
697 };
704 };
698
705
699 let entry = dirstate_node
706 let entry = dirstate_node
700 .entry()?
707 .entry()?
701 .expect("handle_normal_file called with entry-less node");
708 .expect("handle_normal_file called with entry-less node");
702 let mode_changed =
709 let mode_changed =
703 || self.options.check_exec && entry.mode_changed(&fs_metadata);
710 || self.options.check_exec && entry.mode_changed(&fs_metadata);
704 let size = entry.size();
711 let size = entry.size();
705 let size_changed = size != truncate_u64(fs_metadata.len());
712 let size_changed = size != truncate_u64(fs_metadata.len());
706 if size >= 0 && size_changed && fs_metadata.file_type().is_symlink() {
713 if size >= 0 && size_changed && fs_metadata.file_type().is_symlink() {
707 // issue6456: Size returned may be longer due to encryption
714 // issue6456: Size returned may be longer due to encryption
708 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
715 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
709 self.push_outcome(Outcome::Unsure, dirstate_node)?
716 self.push_outcome(Outcome::Unsure, dirstate_node)?
710 } else if dirstate_node.has_copy_source()
717 } else if dirstate_node.has_copy_source()
711 || entry.is_from_other_parent()
718 || entry.is_from_other_parent()
712 || (size >= 0 && (size_changed || mode_changed()))
719 || (size >= 0 && (size_changed || mode_changed()))
713 {
720 {
714 self.push_outcome(Outcome::Modified, dirstate_node)?
721 self.push_outcome(Outcome::Modified, dirstate_node)?
715 } else {
722 } else {
716 let mtime_looks_clean = if let Some(dirstate_mtime) =
723 let mtime_looks_clean = if let Some(dirstate_mtime) =
717 entry.truncated_mtime()
724 entry.truncated_mtime()
718 {
725 {
719 let fs_mtime = TruncatedTimestamp::for_mtime_of(&fs_metadata)
726 let fs_mtime = TruncatedTimestamp::for_mtime_of(&fs_metadata)
720 .expect("OS/libc does not support mtime?");
727 .expect("OS/libc does not support mtime?");
721 // There might be a change in the future if for example the
728 // There might be a change in the future if for example the
722 // internal clock become off while process run, but this is a
729 // internal clock become off while process run, but this is a
723 // case where the issues the user would face
730 // case where the issues the user would face
724 // would be a lot worse and there is nothing we
731 // would be a lot worse and there is nothing we
725 // can really do.
732 // can really do.
726 fs_mtime.likely_equal(dirstate_mtime)
733 fs_mtime.likely_equal(dirstate_mtime)
727 } else {
734 } else {
728 // No mtime in the dirstate entry
735 // No mtime in the dirstate entry
729 false
736 false
730 };
737 };
731 if !mtime_looks_clean {
738 if !mtime_looks_clean {
732 self.push_outcome(Outcome::Unsure, dirstate_node)?
739 self.push_outcome(Outcome::Unsure, dirstate_node)?
733 } else if self.options.list_clean {
740 } else if self.options.list_clean {
734 self.push_outcome(Outcome::Clean, dirstate_node)?
741 self.push_outcome(Outcome::Clean, dirstate_node)?
735 }
742 }
736 }
743 }
737 Ok(())
744 Ok(())
738 }
745 }
739
746
740 /// A node in the dirstate tree has no corresponding filesystem entry
747 /// A node in the dirstate tree has no corresponding filesystem entry
741 fn traverse_dirstate_only(
748 fn traverse_dirstate_only(
742 &self,
749 &self,
743 dirstate_node: NodeRef<'tree, 'on_disk>,
750 dirstate_node: NodeRef<'tree, 'on_disk>,
744 ) -> Result<(), DirstateV2ParseError> {
751 ) -> Result<(), DirstateV2ParseError> {
745 self.check_for_outdated_directory_cache(&dirstate_node)?;
752 self.check_for_outdated_directory_cache(&dirstate_node)?;
746 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
753 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
747 dirstate_node
754 dirstate_node
748 .children(self.dmap.on_disk)?
755 .children(self.dmap.on_disk)?
749 .par_iter()
756 .par_iter()
750 .map(|child_node| self.traverse_dirstate_only(child_node))
757 .map(|child_node| self.traverse_dirstate_only(child_node))
751 .collect()
758 .collect()
752 }
759 }
753
760
754 /// A node in the dirstate tree has no corresponding *file* on the
761 /// A node in the dirstate tree has no corresponding *file* on the
755 /// filesystem
762 /// filesystem
756 ///
763 ///
757 /// Does nothing on a "directory" node
764 /// Does nothing on a "directory" node
758 fn mark_removed_or_deleted_if_file(
765 fn mark_removed_or_deleted_if_file(
759 &self,
766 &self,
760 dirstate_node: &NodeRef<'tree, 'on_disk>,
767 dirstate_node: &NodeRef<'tree, 'on_disk>,
761 ) -> Result<(), DirstateV2ParseError> {
768 ) -> Result<(), DirstateV2ParseError> {
762 if let Some(entry) = dirstate_node.entry()? {
769 if let Some(entry) = dirstate_node.entry()? {
763 if !entry.any_tracked() {
770 if !entry.any_tracked() {
764 // Future-compat for when we start storing ignored and unknown
771 // Future-compat for when we start storing ignored and unknown
765 // files for caching reasons
772 // files for caching reasons
766 return Ok(());
773 return Ok(());
767 }
774 }
768 let path = dirstate_node.full_path(self.dmap.on_disk)?;
775 let path = dirstate_node.full_path(self.dmap.on_disk)?;
769 if self.matcher.matches(path) {
776 if self.matcher.matches(path) {
770 if entry.removed() {
777 if entry.removed() {
771 self.push_outcome(Outcome::Removed, dirstate_node)?
778 self.push_outcome(Outcome::Removed, dirstate_node)?
772 } else {
779 } else {
773 self.push_outcome(Outcome::Deleted, dirstate_node)?
780 self.push_outcome(Outcome::Deleted, dirstate_node)?
774 }
781 }
775 }
782 }
776 }
783 }
777 Ok(())
784 Ok(())
778 }
785 }
779
786
780 /// Something in the filesystem has no corresponding dirstate node
787 /// Something in the filesystem has no corresponding dirstate node
781 ///
788 ///
782 /// Returns whether that path is ignored
789 /// Returns whether that path is ignored
783 fn traverse_fs_only(
790 fn traverse_fs_only(
784 &self,
791 &self,
785 has_ignored_ancestor: bool,
792 has_ignored_ancestor: bool,
786 directory_hg_path: &HgPath,
793 directory_hg_path: &HgPath,
787 fs_entry: &DirEntry,
794 fs_entry: &DirEntry,
788 ) -> bool {
795 ) -> bool {
789 let hg_path = directory_hg_path.join(&fs_entry.hg_path);
796 let hg_path = directory_hg_path.join(&fs_entry.hg_path);
790 let file_or_symlink = fs_entry.is_file() || fs_entry.is_symlink();
797 let file_or_symlink = fs_entry.is_file() || fs_entry.is_symlink();
791 if fs_entry.is_dir() {
798 if fs_entry.is_dir() {
792 let is_ignored =
799 let is_ignored =
793 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
800 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
794 let traverse_children = if is_ignored {
801 let traverse_children = if is_ignored {
795 // Descendants of an ignored directory are all ignored
802 // Descendants of an ignored directory are all ignored
796 self.options.list_ignored
803 self.options.list_ignored
797 } else {
804 } else {
798 // Descendants of an unknown directory may be either unknown or
805 // Descendants of an unknown directory may be either unknown or
799 // ignored
806 // ignored
800 self.options.list_unknown || self.options.list_ignored
807 self.options.list_unknown || self.options.list_ignored
801 };
808 };
802 if traverse_children {
809 if traverse_children {
803 let is_at_repo_root = false;
810 let is_at_repo_root = false;
804 if let Ok(children_fs_entries) =
811 if let Ok(children_fs_entries) =
805 self.read_dir(&hg_path, &fs_entry.fs_path, is_at_repo_root)
812 self.read_dir(&hg_path, &fs_entry.fs_path, is_at_repo_root)
806 {
813 {
807 children_fs_entries.par_iter().for_each(|child_fs_entry| {
814 children_fs_entries.par_iter().for_each(|child_fs_entry| {
808 self.traverse_fs_only(
815 self.traverse_fs_only(
809 is_ignored,
816 is_ignored,
810 &hg_path,
817 &hg_path,
811 child_fs_entry,
818 child_fs_entry,
812 );
819 );
813 })
820 })
814 }
821 }
815 if self.options.collect_traversed_dirs {
822 if self.options.collect_traversed_dirs {
816 self.outcome.lock().unwrap().traversed.push(hg_path.into())
823 self.outcome.lock().unwrap().traversed.push(hg_path.into())
817 }
824 }
818 }
825 }
819 is_ignored
826 is_ignored
820 } else if file_or_symlink {
827 } else if file_or_symlink {
821 if self.matcher.matches(&hg_path) {
828 if self.matcher.matches(&hg_path) {
822 self.mark_unknown_or_ignored(
829 self.mark_unknown_or_ignored(
823 has_ignored_ancestor,
830 has_ignored_ancestor,
824 &BorrowedPath::InMemory(&hg_path),
831 &BorrowedPath::InMemory(&hg_path),
825 )
832 )
826 } else {
833 } else {
827 // We haven’t computed whether this path is ignored. It
834 // We haven’t computed whether this path is ignored. It
828 // might not be, and a future run of status might have a
835 // might not be, and a future run of status might have a
829 // different matcher that matches it. So treat it as not
836 // different matcher that matches it. So treat it as not
830 // ignored. That is, inhibit readdir caching of the parent
837 // ignored. That is, inhibit readdir caching of the parent
831 // directory.
838 // directory.
832 false
839 false
833 }
840 }
834 } else {
841 } else {
835 // This is neither a directory, a plain file, or a symlink.
842 // This is neither a directory, a plain file, or a symlink.
836 // Treat it like an ignored file.
843 // Treat it like an ignored file.
837 true
844 true
838 }
845 }
839 }
846 }
840
847
841 /// Returns whether that path is ignored
848 /// Returns whether that path is ignored
842 fn mark_unknown_or_ignored(
849 fn mark_unknown_or_ignored(
843 &self,
850 &self,
844 has_ignored_ancestor: bool,
851 has_ignored_ancestor: bool,
845 hg_path: &BorrowedPath<'_, 'on_disk>,
852 hg_path: &BorrowedPath<'_, 'on_disk>,
846 ) -> bool {
853 ) -> bool {
847 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
854 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
848 if is_ignored {
855 if is_ignored {
849 if self.options.list_ignored {
856 if self.options.list_ignored {
850 self.push_outcome_without_copy_source(
857 self.push_outcome_without_copy_source(
851 Outcome::Ignored,
858 Outcome::Ignored,
852 hg_path,
859 hg_path,
853 )
860 )
854 }
861 }
855 } else if self.options.list_unknown {
862 } else if self.options.list_unknown {
856 self.push_outcome_without_copy_source(Outcome::Unknown, hg_path)
863 self.push_outcome_without_copy_source(Outcome::Unknown, hg_path)
857 }
864 }
858 is_ignored
865 is_ignored
859 }
866 }
860 }
867 }
861
868
862 /// Since [`std::fs::FileType`] cannot be built directly, we emulate what we
869 /// Since [`std::fs::FileType`] cannot be built directly, we emulate what we
863 /// care about.
870 /// care about.
864 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
871 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
865 enum FakeFileType {
872 enum FakeFileType {
866 File,
873 File,
867 Directory,
874 Directory,
868 Symlink,
875 Symlink,
876 BadType(BadType),
869 }
877 }
870
878
871 impl TryFrom<std::fs::FileType> for FakeFileType {
879 impl From<std::fs::FileType> for FakeFileType {
872 type Error = ();
880 fn from(f: std::fs::FileType) -> Self {
873
874 fn try_from(f: std::fs::FileType) -> Result<Self, Self::Error> {
875 if f.is_dir() {
881 if f.is_dir() {
876 Ok(Self::Directory)
882 Self::Directory
877 } else if f.is_file() {
883 } else if f.is_file() {
878 Ok(Self::File)
884 Self::File
879 } else if f.is_symlink() {
885 } else if f.is_symlink() {
880 Ok(Self::Symlink)
886 Self::Symlink
887 } else if f.is_fifo() {
888 Self::BadType(BadType::FIFO)
889 } else if f.is_block_device() {
890 Self::BadType(BadType::BlockDevice)
891 } else if f.is_char_device() {
892 Self::BadType(BadType::CharacterDevice)
893 } else if f.is_socket() {
894 Self::BadType(BadType::Socket)
881 } else {
895 } else {
882 // Things like FIFO etc.
896 Self::BadType(BadType::Unknown)
883 Err(())
884 }
897 }
885 }
898 }
886 }
899 }
887
900
888 struct DirEntry<'a> {
901 struct DirEntry<'a> {
889 /// Path as stored in the dirstate, or just the filename for optimization.
902 /// Path as stored in the dirstate, or just the filename for optimization.
890 hg_path: HgPathCow<'a>,
903 hg_path: HgPathCow<'a>,
891 /// Filesystem path
904 /// Filesystem path
892 fs_path: Cow<'a, Path>,
905 fs_path: Cow<'a, Path>,
893 /// Lazily computed
906 /// Lazily computed
894 symlink_metadata: Option<std::fs::Metadata>,
907 symlink_metadata: Option<std::fs::Metadata>,
895 /// Already computed for ergonomics.
908 /// Already computed for ergonomics.
896 file_type: FakeFileType,
909 file_type: FakeFileType,
897 }
910 }
898
911
899 impl<'a> DirEntry<'a> {
912 impl<'a> DirEntry<'a> {
900 /// Returns **unsorted** entries in the given directory, with name,
913 /// Returns **unsorted** entries in the given directory, with name,
901 /// metadata and file type.
914 /// metadata and file type.
902 ///
915 ///
903 /// If a `.hg` sub-directory is encountered:
916 /// If a `.hg` sub-directory is encountered:
904 ///
917 ///
905 /// * At the repository root, ignore that sub-directory
918 /// * At the repository root, ignore that sub-directory
906 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
919 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
907 /// list instead.
920 /// list instead.
908 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
921 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
909 // `read_dir` returns a "not found" error for the empty path
922 // `read_dir` returns a "not found" error for the empty path
910 let at_cwd = path == Path::new("");
923 let at_cwd = path == Path::new("");
911 let read_dir_path = if at_cwd { Path::new(".") } else { path };
924 let read_dir_path = if at_cwd { Path::new(".") } else { path };
912 let mut results = Vec::new();
925 let mut results = Vec::new();
913 for entry in read_dir_path.read_dir()? {
926 for entry in read_dir_path.read_dir()? {
914 let entry = entry?;
927 let entry = entry?;
915 let file_type = match entry.file_type() {
928 let file_type = match entry.file_type() {
916 Ok(v) => v,
929 Ok(v) => v,
917 Err(e) => {
930 Err(e) => {
918 // race with file deletion?
931 // race with file deletion?
919 if e.kind() == std::io::ErrorKind::NotFound {
932 if e.kind() == std::io::ErrorKind::NotFound {
920 continue;
933 continue;
921 } else {
934 } else {
922 return Err(e);
935 return Err(e);
923 }
936 }
924 }
937 }
925 };
938 };
926 let file_name = entry.file_name();
939 let file_name = entry.file_name();
927 // FIXME don't do this when cached
940 // FIXME don't do this when cached
928 if file_name == ".hg" {
941 if file_name == ".hg" {
929 if is_at_repo_root {
942 if is_at_repo_root {
930 // Skip the repo’s own .hg (might be a symlink)
943 // Skip the repo’s own .hg (might be a symlink)
931 continue;
944 continue;
932 } else if file_type.is_dir() {
945 } else if file_type.is_dir() {
933 // A .hg sub-directory at another location means a subrepo,
946 // A .hg sub-directory at another location means a subrepo,
934 // skip it entirely.
947 // skip it entirely.
935 return Ok(Vec::new());
948 return Ok(Vec::new());
936 }
949 }
937 }
950 }
938 let full_path = if at_cwd {
951 let full_path = if at_cwd {
939 file_name.clone().into()
952 file_name.clone().into()
940 } else {
953 } else {
941 entry.path()
954 entry.path()
942 };
955 };
943 let filename =
956 let filename =
944 Cow::Owned(get_bytes_from_os_string(file_name).into());
957 Cow::Owned(get_bytes_from_os_string(file_name).into());
945 let file_type = match FakeFileType::try_from(file_type) {
958 let file_type = FakeFileType::from(file_type);
946 Ok(file_type) => file_type,
947 Err(_) => continue,
948 };
949 results.push(DirEntry {
959 results.push(DirEntry {
950 hg_path: filename,
960 hg_path: filename,
951 fs_path: Cow::Owned(full_path.to_path_buf()),
961 fs_path: Cow::Owned(full_path.to_path_buf()),
952 symlink_metadata: None,
962 symlink_metadata: None,
953 file_type,
963 file_type,
954 })
964 })
955 }
965 }
956 Ok(results)
966 Ok(results)
957 }
967 }
958
968
959 fn symlink_metadata(&self) -> Result<std::fs::Metadata, std::io::Error> {
969 fn symlink_metadata(&self) -> Result<std::fs::Metadata, std::io::Error> {
960 match &self.symlink_metadata {
970 match &self.symlink_metadata {
961 Some(meta) => Ok(meta.clone()),
971 Some(meta) => Ok(meta.clone()),
962 None => std::fs::symlink_metadata(&self.fs_path),
972 None => std::fs::symlink_metadata(&self.fs_path),
963 }
973 }
964 }
974 }
965
975
966 fn is_dir(&self) -> bool {
976 fn is_dir(&self) -> bool {
967 self.file_type == FakeFileType::Directory
977 self.file_type == FakeFileType::Directory
968 }
978 }
969
979
970 fn is_file(&self) -> bool {
980 fn is_file(&self) -> bool {
971 self.file_type == FakeFileType::File
981 self.file_type == FakeFileType::File
972 }
982 }
973
983
974 fn is_symlink(&self) -> bool {
984 fn is_symlink(&self) -> bool {
975 self.file_type == FakeFileType::Symlink
985 self.file_type == FakeFileType::Symlink
976 }
986 }
987
988 fn is_bad(&self) -> Option<BadType> {
989 match self.file_type {
990 FakeFileType::BadType(ty) => Some(ty),
991 _ => None,
992 }
993 }
977 }
994 }
978
995
979 /// Return the `mtime` of a temporary file newly-created in the `.hg` directory
996 /// Return the `mtime` of a temporary file newly-created in the `.hg` directory
980 /// of the give repository.
997 /// of the give repository.
981 ///
998 ///
982 /// This is similar to `SystemTime::now()`, with the result truncated to the
999 /// This is similar to `SystemTime::now()`, with the result truncated to the
983 /// same time resolution as other files’ modification times. Using `.hg`
1000 /// same time resolution as other files’ modification times. Using `.hg`
984 /// instead of the system’s default temporary directory (such as `/tmp`) makes
1001 /// instead of the system’s default temporary directory (such as `/tmp`) makes
985 /// it more likely the temporary file is in the same disk partition as contents
1002 /// it more likely the temporary file is in the same disk partition as contents
986 /// of the working directory, which can matter since different filesystems may
1003 /// of the working directory, which can matter since different filesystems may
987 /// store timestamps with different resolutions.
1004 /// store timestamps with different resolutions.
988 ///
1005 ///
989 /// This may fail, typically if we lack write permissions. In that case we
1006 /// This may fail, typically if we lack write permissions. In that case we
990 /// should continue the `status()` algoritm anyway and consider the current
1007 /// should continue the `status()` algoritm anyway and consider the current
991 /// date/time to be unknown.
1008 /// date/time to be unknown.
992 fn filesystem_now(repo_root: &Path) -> Result<SystemTime, io::Error> {
1009 fn filesystem_now(repo_root: &Path) -> Result<SystemTime, io::Error> {
993 tempfile::tempfile_in(repo_root.join(".hg"))?
1010 tempfile::tempfile_in(repo_root.join(".hg"))?
994 .metadata()?
1011 .metadata()?
995 .modified()
1012 .modified()
996 }
1013 }
General Comments 0
You need to be logged in to leave comments. Login now