##// END OF EJS Templates
status: prefer relative paths in Rust code...
Simon Sapin -
r49591:94e36b23 default
parent child Browse files
Show More
@@ -1,798 +1,827 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::NodeData;
7 use crate::dirstate_tree::dirstate_map::NodeData;
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_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::EntryState;
17 use crate::EntryState;
18 use crate::HgPathBuf;
18 use crate::HgPathBuf;
19 use crate::HgPathCow;
19 use crate::HgPathCow;
20 use crate::PatternFileWarning;
20 use crate::PatternFileWarning;
21 use crate::StatusError;
21 use crate::StatusError;
22 use crate::StatusOptions;
22 use crate::StatusOptions;
23 use micro_timer::timed;
23 use micro_timer::timed;
24 use rayon::prelude::*;
24 use rayon::prelude::*;
25 use sha1::{Digest, Sha1};
25 use sha1::{Digest, Sha1};
26 use std::borrow::Cow;
26 use std::borrow::Cow;
27 use std::io;
27 use std::io;
28 use std::path::Path;
28 use std::path::Path;
29 use std::path::PathBuf;
29 use std::path::PathBuf;
30 use std::sync::Mutex;
30 use std::sync::Mutex;
31 use std::time::SystemTime;
31 use std::time::SystemTime;
32
32
33 /// Returns the status of the working directory compared to its parent
33 /// Returns the status of the working directory compared to its parent
34 /// changeset.
34 /// changeset.
35 ///
35 ///
36 /// This algorithm is based on traversing the filesystem tree (`fs` in function
36 /// This algorithm is based on traversing the filesystem tree (`fs` in function
37 /// and variable names) and dirstate tree at the same time. The core of this
37 /// and variable names) and dirstate tree at the same time. The core of this
38 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
38 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
39 /// and its use of `itertools::merge_join_by`. When reaching a path that only
39 /// and its use of `itertools::merge_join_by`. When reaching a path that only
40 /// exists in one of the two trees, depending on information requested by
40 /// exists in one of the two trees, depending on information requested by
41 /// `options` we may need to traverse the remaining subtree.
41 /// `options` we may need to traverse the remaining subtree.
42 #[timed]
42 #[timed]
43 pub fn status<'tree, 'on_disk: 'tree>(
43 pub fn status<'tree, 'on_disk: 'tree>(
44 dmap: &'tree mut DirstateMap<'on_disk>,
44 dmap: &'tree mut DirstateMap<'on_disk>,
45 matcher: &(dyn Matcher + Sync),
45 matcher: &(dyn Matcher + Sync),
46 root_dir: PathBuf,
46 root_dir: PathBuf,
47 ignore_files: Vec<PathBuf>,
47 ignore_files: Vec<PathBuf>,
48 options: StatusOptions,
48 options: StatusOptions,
49 ) -> Result<(DirstateStatus<'on_disk>, Vec<PatternFileWarning>), StatusError> {
49 ) -> Result<(DirstateStatus<'on_disk>, Vec<PatternFileWarning>), StatusError> {
50 let (ignore_fn, warnings, patterns_changed): (IgnoreFnType, _, _) =
50 let (ignore_fn, warnings, patterns_changed): (IgnoreFnType, _, _) =
51 if options.list_ignored || options.list_unknown {
51 if options.list_ignored || options.list_unknown {
52 let mut hasher = Sha1::new();
52 let mut hasher = Sha1::new();
53 let (ignore_fn, warnings) = get_ignore_function(
53 let (ignore_fn, warnings) = get_ignore_function(
54 ignore_files,
54 ignore_files,
55 &root_dir,
55 &root_dir,
56 &mut |pattern_bytes| hasher.update(pattern_bytes),
56 &mut |pattern_bytes| hasher.update(pattern_bytes),
57 )?;
57 )?;
58 let new_hash = *hasher.finalize().as_ref();
58 let new_hash = *hasher.finalize().as_ref();
59 let changed = new_hash != dmap.ignore_patterns_hash;
59 let changed = new_hash != dmap.ignore_patterns_hash;
60 dmap.ignore_patterns_hash = new_hash;
60 dmap.ignore_patterns_hash = new_hash;
61 (ignore_fn, warnings, Some(changed))
61 (ignore_fn, warnings, Some(changed))
62 } else {
62 } else {
63 (Box::new(|&_| true), vec![], None)
63 (Box::new(|&_| true), vec![], None)
64 };
64 };
65
65
66 let filesystem_time_at_status_start =
66 let filesystem_time_at_status_start =
67 filesystem_now(&root_dir).ok().map(TruncatedTimestamp::from);
67 filesystem_now(&root_dir).ok().map(TruncatedTimestamp::from);
68
69 // If the repository is under the current directory, prefer using a
70 // relative path, so the kernel needs to traverse fewer directory in every
71 // call to `read_dir` or `symlink_metadata`.
72 // This is effective in the common case where the current directory is the
73 // repository root.
74
75 // TODO: Better yet would be to use libc functions like `openat` and
76 // `fstatat` to remove such repeated traversals entirely, but the standard
77 // library does not provide APIs based on those.
78 // Maybe with a crate like https://crates.io/crates/openat instead?
79 let root_dir = if let Some(relative) = std::env::current_dir()
80 .ok()
81 .and_then(|cwd| root_dir.strip_prefix(cwd).ok())
82 {
83 relative
84 } else {
85 &root_dir
86 };
87
68 let outcome = DirstateStatus {
88 let outcome = DirstateStatus {
69 filesystem_time_at_status_start,
89 filesystem_time_at_status_start,
70 ..Default::default()
90 ..Default::default()
71 };
91 };
72 let common = StatusCommon {
92 let common = StatusCommon {
73 dmap,
93 dmap,
74 options,
94 options,
75 matcher,
95 matcher,
76 ignore_fn,
96 ignore_fn,
77 outcome: Mutex::new(outcome),
97 outcome: Mutex::new(outcome),
78 ignore_patterns_have_changed: patterns_changed,
98 ignore_patterns_have_changed: patterns_changed,
79 new_cachable_directories: Default::default(),
99 new_cachable_directories: Default::default(),
80 outated_cached_directories: Default::default(),
100 outated_cached_directories: Default::default(),
81 filesystem_time_at_status_start,
101 filesystem_time_at_status_start,
82 };
102 };
83 let is_at_repo_root = true;
103 let is_at_repo_root = true;
84 let hg_path = &BorrowedPath::OnDisk(HgPath::new(""));
104 let hg_path = &BorrowedPath::OnDisk(HgPath::new(""));
85 let has_ignored_ancestor = false;
105 let has_ignored_ancestor = false;
86 let root_cached_mtime = None;
106 let root_cached_mtime = None;
87 let root_dir_metadata = None;
107 let root_dir_metadata = None;
88 // If the path we have for the repository root is a symlink, do follow it.
108 // If the path we have for the repository root is a symlink, do follow it.
89 // (As opposed to symlinks within the working directory which are not
109 // (As opposed to symlinks within the working directory which are not
90 // followed, using `std::fs::symlink_metadata`.)
110 // followed, using `std::fs::symlink_metadata`.)
91 common.traverse_fs_directory_and_dirstate(
111 common.traverse_fs_directory_and_dirstate(
92 has_ignored_ancestor,
112 has_ignored_ancestor,
93 dmap.root.as_ref(),
113 dmap.root.as_ref(),
94 hg_path,
114 hg_path,
95 &root_dir,
115 &root_dir,
96 root_dir_metadata,
116 root_dir_metadata,
97 root_cached_mtime,
117 root_cached_mtime,
98 is_at_repo_root,
118 is_at_repo_root,
99 )?;
119 )?;
100 let mut outcome = common.outcome.into_inner().unwrap();
120 let mut outcome = common.outcome.into_inner().unwrap();
101 let new_cachable = common.new_cachable_directories.into_inner().unwrap();
121 let new_cachable = common.new_cachable_directories.into_inner().unwrap();
102 let outdated = common.outated_cached_directories.into_inner().unwrap();
122 let outdated = common.outated_cached_directories.into_inner().unwrap();
103
123
104 outcome.dirty = common.ignore_patterns_have_changed == Some(true)
124 outcome.dirty = common.ignore_patterns_have_changed == Some(true)
105 || !outdated.is_empty()
125 || !outdated.is_empty()
106 || !new_cachable.is_empty();
126 || !new_cachable.is_empty();
107
127
108 // Remove outdated mtimes before adding new mtimes, in case a given
128 // Remove outdated mtimes before adding new mtimes, in case a given
109 // directory is both
129 // directory is both
110 for path in &outdated {
130 for path in &outdated {
111 let node = dmap.get_or_insert(path)?;
131 let node = dmap.get_or_insert(path)?;
112 if let NodeData::CachedDirectory { .. } = &node.data {
132 if let NodeData::CachedDirectory { .. } = &node.data {
113 node.data = NodeData::None
133 node.data = NodeData::None
114 }
134 }
115 }
135 }
116 for (path, mtime) in &new_cachable {
136 for (path, mtime) in &new_cachable {
117 let node = dmap.get_or_insert(path)?;
137 let node = dmap.get_or_insert(path)?;
118 match &node.data {
138 match &node.data {
119 NodeData::Entry(_) => {} // Don’t overwrite an entry
139 NodeData::Entry(_) => {} // Don’t overwrite an entry
120 NodeData::CachedDirectory { .. } | NodeData::None => {
140 NodeData::CachedDirectory { .. } | NodeData::None => {
121 node.data = NodeData::CachedDirectory { mtime: *mtime }
141 node.data = NodeData::CachedDirectory { mtime: *mtime }
122 }
142 }
123 }
143 }
124 }
144 }
125
145
126 Ok((outcome, warnings))
146 Ok((outcome, warnings))
127 }
147 }
128
148
129 /// Bag of random things needed by various parts of the algorithm. Reduces the
149 /// Bag of random things needed by various parts of the algorithm. Reduces the
130 /// number of parameters passed to functions.
150 /// number of parameters passed to functions.
131 struct StatusCommon<'a, 'tree, 'on_disk: 'tree> {
151 struct StatusCommon<'a, 'tree, 'on_disk: 'tree> {
132 dmap: &'tree DirstateMap<'on_disk>,
152 dmap: &'tree DirstateMap<'on_disk>,
133 options: StatusOptions,
153 options: StatusOptions,
134 matcher: &'a (dyn Matcher + Sync),
154 matcher: &'a (dyn Matcher + Sync),
135 ignore_fn: IgnoreFnType<'a>,
155 ignore_fn: IgnoreFnType<'a>,
136 outcome: Mutex<DirstateStatus<'on_disk>>,
156 outcome: Mutex<DirstateStatus<'on_disk>>,
137 new_cachable_directories:
157 new_cachable_directories:
138 Mutex<Vec<(Cow<'on_disk, HgPath>, TruncatedTimestamp)>>,
158 Mutex<Vec<(Cow<'on_disk, HgPath>, TruncatedTimestamp)>>,
139 outated_cached_directories: Mutex<Vec<Cow<'on_disk, HgPath>>>,
159 outated_cached_directories: Mutex<Vec<Cow<'on_disk, HgPath>>>,
140
160
141 /// Whether ignore files like `.hgignore` have changed since the previous
161 /// Whether ignore files like `.hgignore` have changed since the previous
142 /// time a `status()` call wrote their hash to the dirstate. `None` means
162 /// time a `status()` call wrote their hash to the dirstate. `None` means
143 /// we don’t know as this run doesn’t list either ignored or uknown files
163 /// we don’t know as this run doesn’t list either ignored or uknown files
144 /// and therefore isn’t reading `.hgignore`.
164 /// and therefore isn’t reading `.hgignore`.
145 ignore_patterns_have_changed: Option<bool>,
165 ignore_patterns_have_changed: Option<bool>,
146
166
147 /// The current time at the start of the `status()` algorithm, as measured
167 /// The current time at the start of the `status()` algorithm, as measured
148 /// and possibly truncated by the filesystem.
168 /// and possibly truncated by the filesystem.
149 filesystem_time_at_status_start: Option<TruncatedTimestamp>,
169 filesystem_time_at_status_start: Option<TruncatedTimestamp>,
150 }
170 }
151
171
152 enum Outcome {
172 enum Outcome {
153 Modified,
173 Modified,
154 Added,
174 Added,
155 Removed,
175 Removed,
156 Deleted,
176 Deleted,
157 Clean,
177 Clean,
158 Ignored,
178 Ignored,
159 Unknown,
179 Unknown,
160 Unsure,
180 Unsure,
161 }
181 }
162
182
163 impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> {
183 impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> {
164 fn push_outcome(
184 fn push_outcome(
165 &self,
185 &self,
166 which: Outcome,
186 which: Outcome,
167 dirstate_node: &NodeRef<'tree, 'on_disk>,
187 dirstate_node: &NodeRef<'tree, 'on_disk>,
168 ) -> Result<(), DirstateV2ParseError> {
188 ) -> Result<(), DirstateV2ParseError> {
169 let path = dirstate_node
189 let path = dirstate_node
170 .full_path_borrowed(self.dmap.on_disk)?
190 .full_path_borrowed(self.dmap.on_disk)?
171 .detach_from_tree();
191 .detach_from_tree();
172 let copy_source = if self.options.list_copies {
192 let copy_source = if self.options.list_copies {
173 dirstate_node
193 dirstate_node
174 .copy_source_borrowed(self.dmap.on_disk)?
194 .copy_source_borrowed(self.dmap.on_disk)?
175 .map(|source| source.detach_from_tree())
195 .map(|source| source.detach_from_tree())
176 } else {
196 } else {
177 None
197 None
178 };
198 };
179 self.push_outcome_common(which, path, copy_source);
199 self.push_outcome_common(which, path, copy_source);
180 Ok(())
200 Ok(())
181 }
201 }
182
202
183 fn push_outcome_without_copy_source(
203 fn push_outcome_without_copy_source(
184 &self,
204 &self,
185 which: Outcome,
205 which: Outcome,
186 path: &BorrowedPath<'_, 'on_disk>,
206 path: &BorrowedPath<'_, 'on_disk>,
187 ) {
207 ) {
188 self.push_outcome_common(which, path.detach_from_tree(), None)
208 self.push_outcome_common(which, path.detach_from_tree(), None)
189 }
209 }
190
210
191 fn push_outcome_common(
211 fn push_outcome_common(
192 &self,
212 &self,
193 which: Outcome,
213 which: Outcome,
194 path: HgPathCow<'on_disk>,
214 path: HgPathCow<'on_disk>,
195 copy_source: Option<HgPathCow<'on_disk>>,
215 copy_source: Option<HgPathCow<'on_disk>>,
196 ) {
216 ) {
197 let mut outcome = self.outcome.lock().unwrap();
217 let mut outcome = self.outcome.lock().unwrap();
198 let vec = match which {
218 let vec = match which {
199 Outcome::Modified => &mut outcome.modified,
219 Outcome::Modified => &mut outcome.modified,
200 Outcome::Added => &mut outcome.added,
220 Outcome::Added => &mut outcome.added,
201 Outcome::Removed => &mut outcome.removed,
221 Outcome::Removed => &mut outcome.removed,
202 Outcome::Deleted => &mut outcome.deleted,
222 Outcome::Deleted => &mut outcome.deleted,
203 Outcome::Clean => &mut outcome.clean,
223 Outcome::Clean => &mut outcome.clean,
204 Outcome::Ignored => &mut outcome.ignored,
224 Outcome::Ignored => &mut outcome.ignored,
205 Outcome::Unknown => &mut outcome.unknown,
225 Outcome::Unknown => &mut outcome.unknown,
206 Outcome::Unsure => &mut outcome.unsure,
226 Outcome::Unsure => &mut outcome.unsure,
207 };
227 };
208 vec.push(StatusPath { path, copy_source });
228 vec.push(StatusPath { path, copy_source });
209 }
229 }
210
230
211 fn read_dir(
231 fn read_dir(
212 &self,
232 &self,
213 hg_path: &HgPath,
233 hg_path: &HgPath,
214 fs_path: &Path,
234 fs_path: &Path,
215 is_at_repo_root: bool,
235 is_at_repo_root: bool,
216 ) -> Result<Vec<DirEntry>, ()> {
236 ) -> Result<Vec<DirEntry>, ()> {
217 DirEntry::read_dir(fs_path, is_at_repo_root)
237 DirEntry::read_dir(fs_path, is_at_repo_root)
218 .map_err(|error| self.io_error(error, hg_path))
238 .map_err(|error| self.io_error(error, hg_path))
219 }
239 }
220
240
221 fn io_error(&self, error: std::io::Error, hg_path: &HgPath) {
241 fn io_error(&self, error: std::io::Error, hg_path: &HgPath) {
222 let errno = error.raw_os_error().expect("expected real OS error");
242 let errno = error.raw_os_error().expect("expected real OS error");
223 self.outcome
243 self.outcome
224 .lock()
244 .lock()
225 .unwrap()
245 .unwrap()
226 .bad
246 .bad
227 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
247 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
228 }
248 }
229
249
230 fn check_for_outdated_directory_cache(
250 fn check_for_outdated_directory_cache(
231 &self,
251 &self,
232 dirstate_node: &NodeRef<'tree, 'on_disk>,
252 dirstate_node: &NodeRef<'tree, 'on_disk>,
233 ) -> Result<(), DirstateV2ParseError> {
253 ) -> Result<(), DirstateV2ParseError> {
234 if self.ignore_patterns_have_changed == Some(true)
254 if self.ignore_patterns_have_changed == Some(true)
235 && dirstate_node.cached_directory_mtime()?.is_some()
255 && dirstate_node.cached_directory_mtime()?.is_some()
236 {
256 {
237 self.outated_cached_directories.lock().unwrap().push(
257 self.outated_cached_directories.lock().unwrap().push(
238 dirstate_node
258 dirstate_node
239 .full_path_borrowed(self.dmap.on_disk)?
259 .full_path_borrowed(self.dmap.on_disk)?
240 .detach_from_tree(),
260 .detach_from_tree(),
241 )
261 )
242 }
262 }
243 Ok(())
263 Ok(())
244 }
264 }
245
265
246 /// If this returns true, we can get accurate results by only using
266 /// If this returns true, we can get accurate results by only using
247 /// `symlink_metadata` for child nodes that exist in the dirstate and don’t
267 /// `symlink_metadata` for child nodes that exist in the dirstate and don’t
248 /// need to call `read_dir`.
268 /// need to call `read_dir`.
249 fn can_skip_fs_readdir(
269 fn can_skip_fs_readdir(
250 &self,
270 &self,
251 directory_metadata: Option<&std::fs::Metadata>,
271 directory_metadata: Option<&std::fs::Metadata>,
252 cached_directory_mtime: Option<TruncatedTimestamp>,
272 cached_directory_mtime: Option<TruncatedTimestamp>,
253 ) -> bool {
273 ) -> bool {
254 if !self.options.list_unknown && !self.options.list_ignored {
274 if !self.options.list_unknown && !self.options.list_ignored {
255 // All states that we care about listing have corresponding
275 // All states that we care about listing have corresponding
256 // dirstate entries.
276 // dirstate entries.
257 // This happens for example with `hg status -mard`.
277 // This happens for example with `hg status -mard`.
258 return true;
278 return true;
259 }
279 }
260 if !self.options.list_ignored
280 if !self.options.list_ignored
261 && self.ignore_patterns_have_changed == Some(false)
281 && self.ignore_patterns_have_changed == Some(false)
262 {
282 {
263 if let Some(cached_mtime) = cached_directory_mtime {
283 if let Some(cached_mtime) = cached_directory_mtime {
264 // The dirstate contains a cached mtime for this directory, set
284 // The dirstate contains a cached mtime for this directory, set
265 // by a previous run of the `status` algorithm which found this
285 // by a previous run of the `status` algorithm which found this
266 // directory eligible for `read_dir` caching.
286 // directory eligible for `read_dir` caching.
267 if let Some(meta) = directory_metadata {
287 if let Some(meta) = directory_metadata {
268 if cached_mtime
288 if cached_mtime
269 .likely_equal_to_mtime_of(meta)
289 .likely_equal_to_mtime_of(meta)
270 .unwrap_or(false)
290 .unwrap_or(false)
271 {
291 {
272 // The mtime of that directory has not changed
292 // The mtime of that directory has not changed
273 // since then, which means that the results of
293 // since then, which means that the results of
274 // `read_dir` should also be unchanged.
294 // `read_dir` should also be unchanged.
275 return true;
295 return true;
276 }
296 }
277 }
297 }
278 }
298 }
279 }
299 }
280 false
300 false
281 }
301 }
282
302
283 /// Returns whether all child entries of the filesystem directory have a
303 /// Returns whether all child entries of the filesystem directory have a
284 /// corresponding dirstate node or are ignored.
304 /// corresponding dirstate node or are ignored.
285 fn traverse_fs_directory_and_dirstate(
305 fn traverse_fs_directory_and_dirstate(
286 &self,
306 &self,
287 has_ignored_ancestor: bool,
307 has_ignored_ancestor: bool,
288 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>,
308 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>,
289 directory_hg_path: &BorrowedPath<'tree, 'on_disk>,
309 directory_hg_path: &BorrowedPath<'tree, 'on_disk>,
290 directory_fs_path: &Path,
310 directory_fs_path: &Path,
291 directory_metadata: Option<&std::fs::Metadata>,
311 directory_metadata: Option<&std::fs::Metadata>,
292 cached_directory_mtime: Option<TruncatedTimestamp>,
312 cached_directory_mtime: Option<TruncatedTimestamp>,
293 is_at_repo_root: bool,
313 is_at_repo_root: bool,
294 ) -> Result<bool, DirstateV2ParseError> {
314 ) -> Result<bool, DirstateV2ParseError> {
295 if self.can_skip_fs_readdir(directory_metadata, cached_directory_mtime)
315 if self.can_skip_fs_readdir(directory_metadata, cached_directory_mtime)
296 {
316 {
297 dirstate_nodes
317 dirstate_nodes
298 .par_iter()
318 .par_iter()
299 .map(|dirstate_node| {
319 .map(|dirstate_node| {
300 let fs_path = directory_fs_path.join(get_path_from_bytes(
320 let fs_path = directory_fs_path.join(get_path_from_bytes(
301 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(),
321 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(),
302 ));
322 ));
303 match std::fs::symlink_metadata(&fs_path) {
323 match std::fs::symlink_metadata(&fs_path) {
304 Ok(fs_metadata) => self.traverse_fs_and_dirstate(
324 Ok(fs_metadata) => self.traverse_fs_and_dirstate(
305 &fs_path,
325 &fs_path,
306 &fs_metadata,
326 &fs_metadata,
307 dirstate_node,
327 dirstate_node,
308 has_ignored_ancestor,
328 has_ignored_ancestor,
309 ),
329 ),
310 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
330 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
311 self.traverse_dirstate_only(dirstate_node)
331 self.traverse_dirstate_only(dirstate_node)
312 }
332 }
313 Err(error) => {
333 Err(error) => {
314 let hg_path =
334 let hg_path =
315 dirstate_node.full_path(self.dmap.on_disk)?;
335 dirstate_node.full_path(self.dmap.on_disk)?;
316 Ok(self.io_error(error, hg_path))
336 Ok(self.io_error(error, hg_path))
317 }
337 }
318 }
338 }
319 })
339 })
320 .collect::<Result<_, _>>()?;
340 .collect::<Result<_, _>>()?;
321
341
322 // We don’t know, so conservatively say this isn’t the case
342 // We don’t know, so conservatively say this isn’t the case
323 let children_all_have_dirstate_node_or_are_ignored = false;
343 let children_all_have_dirstate_node_or_are_ignored = false;
324
344
325 return Ok(children_all_have_dirstate_node_or_are_ignored);
345 return Ok(children_all_have_dirstate_node_or_are_ignored);
326 }
346 }
327
347
328 let mut fs_entries = if let Ok(entries) = self.read_dir(
348 let mut fs_entries = if let Ok(entries) = self.read_dir(
329 directory_hg_path,
349 directory_hg_path,
330 directory_fs_path,
350 directory_fs_path,
331 is_at_repo_root,
351 is_at_repo_root,
332 ) {
352 ) {
333 entries
353 entries
334 } else {
354 } else {
335 // Treat an unreadable directory (typically because of insufficient
355 // Treat an unreadable directory (typically because of insufficient
336 // permissions) like an empty directory. `self.read_dir` has
356 // permissions) like an empty directory. `self.read_dir` has
337 // already called `self.io_error` so a warning will be emitted.
357 // already called `self.io_error` so a warning will be emitted.
338 Vec::new()
358 Vec::new()
339 };
359 };
340
360
341 // `merge_join_by` requires both its input iterators to be sorted:
361 // `merge_join_by` requires both its input iterators to be sorted:
342
362
343 let dirstate_nodes = dirstate_nodes.sorted();
363 let dirstate_nodes = dirstate_nodes.sorted();
344 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
364 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
345 // https://github.com/rust-lang/rust/issues/34162
365 // https://github.com/rust-lang/rust/issues/34162
346 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
366 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
347
367
348 // Propagate here any error that would happen inside the comparison
368 // Propagate here any error that would happen inside the comparison
349 // callback below
369 // callback below
350 for dirstate_node in &dirstate_nodes {
370 for dirstate_node in &dirstate_nodes {
351 dirstate_node.base_name(self.dmap.on_disk)?;
371 dirstate_node.base_name(self.dmap.on_disk)?;
352 }
372 }
353 itertools::merge_join_by(
373 itertools::merge_join_by(
354 dirstate_nodes,
374 dirstate_nodes,
355 &fs_entries,
375 &fs_entries,
356 |dirstate_node, fs_entry| {
376 |dirstate_node, fs_entry| {
357 // This `unwrap` never panics because we already propagated
377 // This `unwrap` never panics because we already propagated
358 // those errors above
378 // those errors above
359 dirstate_node
379 dirstate_node
360 .base_name(self.dmap.on_disk)
380 .base_name(self.dmap.on_disk)
361 .unwrap()
381 .unwrap()
362 .cmp(&fs_entry.base_name)
382 .cmp(&fs_entry.base_name)
363 },
383 },
364 )
384 )
365 .par_bridge()
385 .par_bridge()
366 .map(|pair| {
386 .map(|pair| {
367 use itertools::EitherOrBoth::*;
387 use itertools::EitherOrBoth::*;
368 let has_dirstate_node_or_is_ignored;
388 let has_dirstate_node_or_is_ignored;
369 match pair {
389 match pair {
370 Both(dirstate_node, fs_entry) => {
390 Both(dirstate_node, fs_entry) => {
371 self.traverse_fs_and_dirstate(
391 self.traverse_fs_and_dirstate(
372 &fs_entry.full_path,
392 &fs_entry.full_path,
373 &fs_entry.metadata,
393 &fs_entry.metadata,
374 dirstate_node,
394 dirstate_node,
375 has_ignored_ancestor,
395 has_ignored_ancestor,
376 )?;
396 )?;
377 has_dirstate_node_or_is_ignored = true
397 has_dirstate_node_or_is_ignored = true
378 }
398 }
379 Left(dirstate_node) => {
399 Left(dirstate_node) => {
380 self.traverse_dirstate_only(dirstate_node)?;
400 self.traverse_dirstate_only(dirstate_node)?;
381 has_dirstate_node_or_is_ignored = true;
401 has_dirstate_node_or_is_ignored = true;
382 }
402 }
383 Right(fs_entry) => {
403 Right(fs_entry) => {
384 has_dirstate_node_or_is_ignored = self.traverse_fs_only(
404 has_dirstate_node_or_is_ignored = self.traverse_fs_only(
385 has_ignored_ancestor,
405 has_ignored_ancestor,
386 directory_hg_path,
406 directory_hg_path,
387 fs_entry,
407 fs_entry,
388 )
408 )
389 }
409 }
390 }
410 }
391 Ok(has_dirstate_node_or_is_ignored)
411 Ok(has_dirstate_node_or_is_ignored)
392 })
412 })
393 .try_reduce(|| true, |a, b| Ok(a && b))
413 .try_reduce(|| true, |a, b| Ok(a && b))
394 }
414 }
395
415
396 fn traverse_fs_and_dirstate(
416 fn traverse_fs_and_dirstate(
397 &self,
417 &self,
398 fs_path: &Path,
418 fs_path: &Path,
399 fs_metadata: &std::fs::Metadata,
419 fs_metadata: &std::fs::Metadata,
400 dirstate_node: NodeRef<'tree, 'on_disk>,
420 dirstate_node: NodeRef<'tree, 'on_disk>,
401 has_ignored_ancestor: bool,
421 has_ignored_ancestor: bool,
402 ) -> Result<(), DirstateV2ParseError> {
422 ) -> Result<(), DirstateV2ParseError> {
403 self.check_for_outdated_directory_cache(&dirstate_node)?;
423 self.check_for_outdated_directory_cache(&dirstate_node)?;
404 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
424 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
405 let file_type = fs_metadata.file_type();
425 let file_type = fs_metadata.file_type();
406 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
426 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
407 if !file_or_symlink {
427 if !file_or_symlink {
408 // If we previously had a file here, it was removed (with
428 // If we previously had a file here, it was removed (with
409 // `hg rm` or similar) or deleted before it could be
429 // `hg rm` or similar) or deleted before it could be
410 // replaced by a directory or something else.
430 // replaced by a directory or something else.
411 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
431 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
412 }
432 }
413 if file_type.is_dir() {
433 if file_type.is_dir() {
414 if self.options.collect_traversed_dirs {
434 if self.options.collect_traversed_dirs {
415 self.outcome
435 self.outcome
416 .lock()
436 .lock()
417 .unwrap()
437 .unwrap()
418 .traversed
438 .traversed
419 .push(hg_path.detach_from_tree())
439 .push(hg_path.detach_from_tree())
420 }
440 }
421 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
441 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
422 let is_at_repo_root = false;
442 let is_at_repo_root = false;
423 let children_all_have_dirstate_node_or_are_ignored = self
443 let children_all_have_dirstate_node_or_are_ignored = self
424 .traverse_fs_directory_and_dirstate(
444 .traverse_fs_directory_and_dirstate(
425 is_ignored,
445 is_ignored,
426 dirstate_node.children(self.dmap.on_disk)?,
446 dirstate_node.children(self.dmap.on_disk)?,
427 hg_path,
447 hg_path,
428 fs_path,
448 fs_path,
429 Some(fs_metadata),
449 Some(fs_metadata),
430 dirstate_node.cached_directory_mtime()?,
450 dirstate_node.cached_directory_mtime()?,
431 is_at_repo_root,
451 is_at_repo_root,
432 )?;
452 )?;
433 self.maybe_save_directory_mtime(
453 self.maybe_save_directory_mtime(
434 children_all_have_dirstate_node_or_are_ignored,
454 children_all_have_dirstate_node_or_are_ignored,
435 fs_metadata,
455 fs_metadata,
436 dirstate_node,
456 dirstate_node,
437 )?
457 )?
438 } else {
458 } else {
439 if file_or_symlink && self.matcher.matches(hg_path) {
459 if file_or_symlink && self.matcher.matches(hg_path) {
440 if let Some(state) = dirstate_node.state()? {
460 if let Some(state) = dirstate_node.state()? {
441 match state {
461 match state {
442 EntryState::Added => {
462 EntryState::Added => {
443 self.push_outcome(Outcome::Added, &dirstate_node)?
463 self.push_outcome(Outcome::Added, &dirstate_node)?
444 }
464 }
445 EntryState::Removed => self
465 EntryState::Removed => self
446 .push_outcome(Outcome::Removed, &dirstate_node)?,
466 .push_outcome(Outcome::Removed, &dirstate_node)?,
447 EntryState::Merged => self
467 EntryState::Merged => self
448 .push_outcome(Outcome::Modified, &dirstate_node)?,
468 .push_outcome(Outcome::Modified, &dirstate_node)?,
449 EntryState::Normal => self
469 EntryState::Normal => self
450 .handle_normal_file(&dirstate_node, fs_metadata)?,
470 .handle_normal_file(&dirstate_node, fs_metadata)?,
451 }
471 }
452 } else {
472 } else {
453 // `node.entry.is_none()` indicates a "directory"
473 // `node.entry.is_none()` indicates a "directory"
454 // node, but the filesystem has a file
474 // node, but the filesystem has a file
455 self.mark_unknown_or_ignored(
475 self.mark_unknown_or_ignored(
456 has_ignored_ancestor,
476 has_ignored_ancestor,
457 hg_path,
477 hg_path,
458 );
478 );
459 }
479 }
460 }
480 }
461
481
462 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter()
482 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter()
463 {
483 {
464 self.traverse_dirstate_only(child_node)?
484 self.traverse_dirstate_only(child_node)?
465 }
485 }
466 }
486 }
467 Ok(())
487 Ok(())
468 }
488 }
469
489
470 fn maybe_save_directory_mtime(
490 fn maybe_save_directory_mtime(
471 &self,
491 &self,
472 children_all_have_dirstate_node_or_are_ignored: bool,
492 children_all_have_dirstate_node_or_are_ignored: bool,
473 directory_metadata: &std::fs::Metadata,
493 directory_metadata: &std::fs::Metadata,
474 dirstate_node: NodeRef<'tree, 'on_disk>,
494 dirstate_node: NodeRef<'tree, 'on_disk>,
475 ) -> Result<(), DirstateV2ParseError> {
495 ) -> Result<(), DirstateV2ParseError> {
476 if !children_all_have_dirstate_node_or_are_ignored {
496 if !children_all_have_dirstate_node_or_are_ignored {
477 return Ok(());
497 return Ok(());
478 }
498 }
479 // All filesystem directory entries from `read_dir` have a
499 // All filesystem directory entries from `read_dir` have a
480 // corresponding node in the dirstate, so we can reconstitute the
500 // corresponding node in the dirstate, so we can reconstitute the
481 // names of those entries without calling `read_dir` again.
501 // names of those entries without calling `read_dir` again.
482
502
483 // TODO: use let-else here and below when available:
503 // TODO: use let-else here and below when available:
484 // https://github.com/rust-lang/rust/issues/87335
504 // https://github.com/rust-lang/rust/issues/87335
485 let status_start = if let Some(status_start) =
505 let status_start = if let Some(status_start) =
486 &self.filesystem_time_at_status_start
506 &self.filesystem_time_at_status_start
487 {
507 {
488 status_start
508 status_start
489 } else {
509 } else {
490 return Ok(());
510 return Ok(());
491 };
511 };
492
512
493 // Although the Rust standard library’s `SystemTime` type
513 // Although the Rust standard library’s `SystemTime` type
494 // has nanosecond precision, the times reported for a
514 // has nanosecond precision, the times reported for a
495 // directory’s (or file’s) modified time may have lower
515 // directory’s (or file’s) modified time may have lower
496 // resolution based on the filesystem (for example ext3
516 // resolution based on the filesystem (for example ext3
497 // only stores integer seconds), kernel (see
517 // only stores integer seconds), kernel (see
498 // https://stackoverflow.com/a/14393315/1162888), etc.
518 // https://stackoverflow.com/a/14393315/1162888), etc.
499 let directory_mtime = if let Ok(option) =
519 let directory_mtime = if let Ok(option) =
500 TruncatedTimestamp::for_reliable_mtime_of(
520 TruncatedTimestamp::for_reliable_mtime_of(
501 directory_metadata,
521 directory_metadata,
502 status_start,
522 status_start,
503 ) {
523 ) {
504 if let Some(directory_mtime) = option {
524 if let Some(directory_mtime) = option {
505 directory_mtime
525 directory_mtime
506 } else {
526 } else {
507 // The directory was modified too recently,
527 // The directory was modified too recently,
508 // don’t cache its `read_dir` results.
528 // don’t cache its `read_dir` results.
509 //
529 //
510 // 1. A change to this directory (direct child was
530 // 1. A change to this directory (direct child was
511 // added or removed) cause its mtime to be set
531 // added or removed) cause its mtime to be set
512 // (possibly truncated) to `directory_mtime`
532 // (possibly truncated) to `directory_mtime`
513 // 2. This `status` algorithm calls `read_dir`
533 // 2. This `status` algorithm calls `read_dir`
514 // 3. An other change is made to the same directory is
534 // 3. An other change is made to the same directory is
515 // made so that calling `read_dir` agin would give
535 // made so that calling `read_dir` agin would give
516 // different results, but soon enough after 1. that
536 // different results, but soon enough after 1. that
517 // the mtime stays the same
537 // the mtime stays the same
518 //
538 //
519 // On a system where the time resolution poor, this
539 // On a system where the time resolution poor, this
520 // scenario is not unlikely if all three steps are caused
540 // scenario is not unlikely if all three steps are caused
521 // by the same script.
541 // by the same script.
522 return Ok(());
542 return Ok(());
523 }
543 }
524 } else {
544 } else {
525 // OS/libc does not support mtime?
545 // OS/libc does not support mtime?
526 return Ok(());
546 return Ok(());
527 };
547 };
528 // We’ve observed (through `status_start`) that time has
548 // We’ve observed (through `status_start`) that time has
529 // “progressed” since `directory_mtime`, so any further
549 // “progressed” since `directory_mtime`, so any further
530 // change to this directory is extremely likely to cause a
550 // change to this directory is extremely likely to cause a
531 // different mtime.
551 // different mtime.
532 //
552 //
533 // Having the same mtime again is not entirely impossible
553 // Having the same mtime again is not entirely impossible
534 // since the system clock is not monotonous. It could jump
554 // since the system clock is not monotonous. It could jump
535 // backward to some point before `directory_mtime`, then a
555 // backward to some point before `directory_mtime`, then a
536 // directory change could potentially happen during exactly
556 // directory change could potentially happen during exactly
537 // the wrong tick.
557 // the wrong tick.
538 //
558 //
539 // We deem this scenario (unlike the previous one) to be
559 // We deem this scenario (unlike the previous one) to be
540 // unlikely enough in practice.
560 // unlikely enough in practice.
541
561
542 let is_up_to_date =
562 let is_up_to_date =
543 if let Some(cached) = dirstate_node.cached_directory_mtime()? {
563 if let Some(cached) = dirstate_node.cached_directory_mtime()? {
544 cached.likely_equal(directory_mtime)
564 cached.likely_equal(directory_mtime)
545 } else {
565 } else {
546 false
566 false
547 };
567 };
548 if !is_up_to_date {
568 if !is_up_to_date {
549 let hg_path = dirstate_node
569 let hg_path = dirstate_node
550 .full_path_borrowed(self.dmap.on_disk)?
570 .full_path_borrowed(self.dmap.on_disk)?
551 .detach_from_tree();
571 .detach_from_tree();
552 self.new_cachable_directories
572 self.new_cachable_directories
553 .lock()
573 .lock()
554 .unwrap()
574 .unwrap()
555 .push((hg_path, directory_mtime))
575 .push((hg_path, directory_mtime))
556 }
576 }
557 Ok(())
577 Ok(())
558 }
578 }
559
579
560 /// A file with `EntryState::Normal` in the dirstate was found in the
580 /// A file with `EntryState::Normal` in the dirstate was found in the
561 /// filesystem
581 /// filesystem
562 fn handle_normal_file(
582 fn handle_normal_file(
563 &self,
583 &self,
564 dirstate_node: &NodeRef<'tree, 'on_disk>,
584 dirstate_node: &NodeRef<'tree, 'on_disk>,
565 fs_metadata: &std::fs::Metadata,
585 fs_metadata: &std::fs::Metadata,
566 ) -> Result<(), DirstateV2ParseError> {
586 ) -> Result<(), DirstateV2ParseError> {
567 // Keep the low 31 bits
587 // Keep the low 31 bits
568 fn truncate_u64(value: u64) -> i32 {
588 fn truncate_u64(value: u64) -> i32 {
569 (value & 0x7FFF_FFFF) as i32
589 (value & 0x7FFF_FFFF) as i32
570 }
590 }
571
591
572 let entry = dirstate_node
592 let entry = dirstate_node
573 .entry()?
593 .entry()?
574 .expect("handle_normal_file called with entry-less node");
594 .expect("handle_normal_file called with entry-less node");
575 let mode_changed =
595 let mode_changed =
576 || self.options.check_exec && entry.mode_changed(fs_metadata);
596 || self.options.check_exec && entry.mode_changed(fs_metadata);
577 let size = entry.size();
597 let size = entry.size();
578 let size_changed = size != truncate_u64(fs_metadata.len());
598 let size_changed = size != truncate_u64(fs_metadata.len());
579 if size >= 0 && size_changed && fs_metadata.file_type().is_symlink() {
599 if size >= 0 && size_changed && fs_metadata.file_type().is_symlink() {
580 // issue6456: Size returned may be longer due to encryption
600 // issue6456: Size returned may be longer due to encryption
581 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
601 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
582 self.push_outcome(Outcome::Unsure, dirstate_node)?
602 self.push_outcome(Outcome::Unsure, dirstate_node)?
583 } else if dirstate_node.has_copy_source()
603 } else if dirstate_node.has_copy_source()
584 || entry.is_from_other_parent()
604 || entry.is_from_other_parent()
585 || (size >= 0 && (size_changed || mode_changed()))
605 || (size >= 0 && (size_changed || mode_changed()))
586 {
606 {
587 self.push_outcome(Outcome::Modified, dirstate_node)?
607 self.push_outcome(Outcome::Modified, dirstate_node)?
588 } else {
608 } else {
589 let mtime_looks_clean;
609 let mtime_looks_clean;
590 if let Some(dirstate_mtime) = entry.truncated_mtime() {
610 if let Some(dirstate_mtime) = entry.truncated_mtime() {
591 let fs_mtime = TruncatedTimestamp::for_mtime_of(fs_metadata)
611 let fs_mtime = TruncatedTimestamp::for_mtime_of(fs_metadata)
592 .expect("OS/libc does not support mtime?");
612 .expect("OS/libc does not support mtime?");
593 // There might be a change in the future if for example the
613 // There might be a change in the future if for example the
594 // internal clock become off while process run, but this is a
614 // internal clock become off while process run, but this is a
595 // case where the issues the user would face
615 // case where the issues the user would face
596 // would be a lot worse and there is nothing we
616 // would be a lot worse and there is nothing we
597 // can really do.
617 // can really do.
598 mtime_looks_clean = fs_mtime.likely_equal(dirstate_mtime)
618 mtime_looks_clean = fs_mtime.likely_equal(dirstate_mtime)
599 } else {
619 } else {
600 // No mtime in the dirstate entry
620 // No mtime in the dirstate entry
601 mtime_looks_clean = false
621 mtime_looks_clean = false
602 };
622 };
603 if !mtime_looks_clean {
623 if !mtime_looks_clean {
604 self.push_outcome(Outcome::Unsure, dirstate_node)?
624 self.push_outcome(Outcome::Unsure, dirstate_node)?
605 } else if self.options.list_clean {
625 } else if self.options.list_clean {
606 self.push_outcome(Outcome::Clean, dirstate_node)?
626 self.push_outcome(Outcome::Clean, dirstate_node)?
607 }
627 }
608 }
628 }
609 Ok(())
629 Ok(())
610 }
630 }
611
631
612 /// A node in the dirstate tree has no corresponding filesystem entry
632 /// A node in the dirstate tree has no corresponding filesystem entry
613 fn traverse_dirstate_only(
633 fn traverse_dirstate_only(
614 &self,
634 &self,
615 dirstate_node: NodeRef<'tree, 'on_disk>,
635 dirstate_node: NodeRef<'tree, 'on_disk>,
616 ) -> Result<(), DirstateV2ParseError> {
636 ) -> Result<(), DirstateV2ParseError> {
617 self.check_for_outdated_directory_cache(&dirstate_node)?;
637 self.check_for_outdated_directory_cache(&dirstate_node)?;
618 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
638 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
619 dirstate_node
639 dirstate_node
620 .children(self.dmap.on_disk)?
640 .children(self.dmap.on_disk)?
621 .par_iter()
641 .par_iter()
622 .map(|child_node| self.traverse_dirstate_only(child_node))
642 .map(|child_node| self.traverse_dirstate_only(child_node))
623 .collect()
643 .collect()
624 }
644 }
625
645
626 /// A node in the dirstate tree has no corresponding *file* on the
646 /// A node in the dirstate tree has no corresponding *file* on the
627 /// filesystem
647 /// filesystem
628 ///
648 ///
629 /// Does nothing on a "directory" node
649 /// Does nothing on a "directory" node
630 fn mark_removed_or_deleted_if_file(
650 fn mark_removed_or_deleted_if_file(
631 &self,
651 &self,
632 dirstate_node: &NodeRef<'tree, 'on_disk>,
652 dirstate_node: &NodeRef<'tree, 'on_disk>,
633 ) -> Result<(), DirstateV2ParseError> {
653 ) -> Result<(), DirstateV2ParseError> {
634 if let Some(state) = dirstate_node.state()? {
654 if let Some(state) = dirstate_node.state()? {
635 let path = dirstate_node.full_path(self.dmap.on_disk)?;
655 let path = dirstate_node.full_path(self.dmap.on_disk)?;
636 if self.matcher.matches(path) {
656 if self.matcher.matches(path) {
637 if let EntryState::Removed = state {
657 if let EntryState::Removed = state {
638 self.push_outcome(Outcome::Removed, dirstate_node)?
658 self.push_outcome(Outcome::Removed, dirstate_node)?
639 } else {
659 } else {
640 self.push_outcome(Outcome::Deleted, &dirstate_node)?
660 self.push_outcome(Outcome::Deleted, &dirstate_node)?
641 }
661 }
642 }
662 }
643 }
663 }
644 Ok(())
664 Ok(())
645 }
665 }
646
666
647 /// Something in the filesystem has no corresponding dirstate node
667 /// Something in the filesystem has no corresponding dirstate node
648 ///
668 ///
649 /// Returns whether that path is ignored
669 /// Returns whether that path is ignored
650 fn traverse_fs_only(
670 fn traverse_fs_only(
651 &self,
671 &self,
652 has_ignored_ancestor: bool,
672 has_ignored_ancestor: bool,
653 directory_hg_path: &HgPath,
673 directory_hg_path: &HgPath,
654 fs_entry: &DirEntry,
674 fs_entry: &DirEntry,
655 ) -> bool {
675 ) -> bool {
656 let hg_path = directory_hg_path.join(&fs_entry.base_name);
676 let hg_path = directory_hg_path.join(&fs_entry.base_name);
657 let file_type = fs_entry.metadata.file_type();
677 let file_type = fs_entry.metadata.file_type();
658 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
678 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
659 if file_type.is_dir() {
679 if file_type.is_dir() {
660 let is_ignored =
680 let is_ignored =
661 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
681 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
662 let traverse_children = if is_ignored {
682 let traverse_children = if is_ignored {
663 // Descendants of an ignored directory are all ignored
683 // Descendants of an ignored directory are all ignored
664 self.options.list_ignored
684 self.options.list_ignored
665 } else {
685 } else {
666 // Descendants of an unknown directory may be either unknown or
686 // Descendants of an unknown directory may be either unknown or
667 // ignored
687 // ignored
668 self.options.list_unknown || self.options.list_ignored
688 self.options.list_unknown || self.options.list_ignored
669 };
689 };
670 if traverse_children {
690 if traverse_children {
671 let is_at_repo_root = false;
691 let is_at_repo_root = false;
672 if let Ok(children_fs_entries) = self.read_dir(
692 if let Ok(children_fs_entries) = self.read_dir(
673 &hg_path,
693 &hg_path,
674 &fs_entry.full_path,
694 &fs_entry.full_path,
675 is_at_repo_root,
695 is_at_repo_root,
676 ) {
696 ) {
677 children_fs_entries.par_iter().for_each(|child_fs_entry| {
697 children_fs_entries.par_iter().for_each(|child_fs_entry| {
678 self.traverse_fs_only(
698 self.traverse_fs_only(
679 is_ignored,
699 is_ignored,
680 &hg_path,
700 &hg_path,
681 child_fs_entry,
701 child_fs_entry,
682 );
702 );
683 })
703 })
684 }
704 }
685 }
705 }
686 if self.options.collect_traversed_dirs {
706 if self.options.collect_traversed_dirs {
687 self.outcome.lock().unwrap().traversed.push(hg_path.into())
707 self.outcome.lock().unwrap().traversed.push(hg_path.into())
688 }
708 }
689 is_ignored
709 is_ignored
690 } else {
710 } else {
691 if file_or_symlink {
711 if file_or_symlink {
692 if self.matcher.matches(&hg_path) {
712 if self.matcher.matches(&hg_path) {
693 self.mark_unknown_or_ignored(
713 self.mark_unknown_or_ignored(
694 has_ignored_ancestor,
714 has_ignored_ancestor,
695 &BorrowedPath::InMemory(&hg_path),
715 &BorrowedPath::InMemory(&hg_path),
696 )
716 )
697 } else {
717 } else {
698 // We haven’t computed whether this path is ignored. It
718 // We haven’t computed whether this path is ignored. It
699 // might not be, and a future run of status might have a
719 // might not be, and a future run of status might have a
700 // different matcher that matches it. So treat it as not
720 // different matcher that matches it. So treat it as not
701 // ignored. That is, inhibit readdir caching of the parent
721 // ignored. That is, inhibit readdir caching of the parent
702 // directory.
722 // directory.
703 false
723 false
704 }
724 }
705 } else {
725 } else {
706 // This is neither a directory, a plain file, or a symlink.
726 // This is neither a directory, a plain file, or a symlink.
707 // Treat it like an ignored file.
727 // Treat it like an ignored file.
708 true
728 true
709 }
729 }
710 }
730 }
711 }
731 }
712
732
713 /// Returns whether that path is ignored
733 /// Returns whether that path is ignored
714 fn mark_unknown_or_ignored(
734 fn mark_unknown_or_ignored(
715 &self,
735 &self,
716 has_ignored_ancestor: bool,
736 has_ignored_ancestor: bool,
717 hg_path: &BorrowedPath<'_, 'on_disk>,
737 hg_path: &BorrowedPath<'_, 'on_disk>,
718 ) -> bool {
738 ) -> bool {
719 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
739 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
720 if is_ignored {
740 if is_ignored {
721 if self.options.list_ignored {
741 if self.options.list_ignored {
722 self.push_outcome_without_copy_source(
742 self.push_outcome_without_copy_source(
723 Outcome::Ignored,
743 Outcome::Ignored,
724 hg_path,
744 hg_path,
725 )
745 )
726 }
746 }
727 } else {
747 } else {
728 if self.options.list_unknown {
748 if self.options.list_unknown {
729 self.push_outcome_without_copy_source(
749 self.push_outcome_without_copy_source(
730 Outcome::Unknown,
750 Outcome::Unknown,
731 hg_path,
751 hg_path,
732 )
752 )
733 }
753 }
734 }
754 }
735 is_ignored
755 is_ignored
736 }
756 }
737 }
757 }
738
758
739 struct DirEntry {
759 struct DirEntry {
740 base_name: HgPathBuf,
760 base_name: HgPathBuf,
741 full_path: PathBuf,
761 full_path: PathBuf,
742 metadata: std::fs::Metadata,
762 metadata: std::fs::Metadata,
743 }
763 }
744
764
745 impl DirEntry {
765 impl DirEntry {
746 /// Returns **unsorted** entries in the given directory, with name and
766 /// Returns **unsorted** entries in the given directory, with name and
747 /// metadata.
767 /// metadata.
748 ///
768 ///
749 /// If a `.hg` sub-directory is encountered:
769 /// If a `.hg` sub-directory is encountered:
750 ///
770 ///
751 /// * At the repository root, ignore that sub-directory
771 /// * At the repository root, ignore that sub-directory
752 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
772 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
753 /// list instead.
773 /// list instead.
754 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
774 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
775 // `read_dir` returns a "not found" error for the empty path
776 let at_cwd = path == Path::new("");
777 let read_dir_path = if at_cwd { Path::new(".") } else { path };
755 let mut results = Vec::new();
778 let mut results = Vec::new();
756 for entry in path.read_dir()? {
779 for entry in read_dir_path.read_dir()? {
757 let entry = entry?;
780 let entry = entry?;
758 let metadata = entry.metadata()?;
781 let metadata = entry.metadata()?;
759 let name = get_bytes_from_os_string(entry.file_name());
782 let file_name = entry.file_name();
760 // FIXME don't do this when cached
783 // FIXME don't do this when cached
761 if name == b".hg" {
784 if file_name == ".hg" {
762 if is_at_repo_root {
785 if is_at_repo_root {
763 // Skip the repo’s own .hg (might be a symlink)
786 // Skip the repo’s own .hg (might be a symlink)
764 continue;
787 continue;
765 } else if metadata.is_dir() {
788 } else if metadata.is_dir() {
766 // A .hg sub-directory at another location means a subrepo,
789 // A .hg sub-directory at another location means a subrepo,
767 // skip it entirely.
790 // skip it entirely.
768 return Ok(Vec::new());
791 return Ok(Vec::new());
769 }
792 }
770 }
793 }
794 let full_path = if at_cwd {
795 file_name.clone().into()
796 } else {
797 entry.path()
798 };
799 let base_name = get_bytes_from_os_string(file_name).into();
771 results.push(DirEntry {
800 results.push(DirEntry {
772 base_name: name.into(),
801 base_name,
773 full_path: entry.path(),
802 full_path,
774 metadata,
803 metadata,
775 })
804 })
776 }
805 }
777 Ok(results)
806 Ok(results)
778 }
807 }
779 }
808 }
780
809
781 /// Return the `mtime` of a temporary file newly-created in the `.hg` directory
810 /// Return the `mtime` of a temporary file newly-created in the `.hg` directory
782 /// of the give repository.
811 /// of the give repository.
783 ///
812 ///
784 /// This is similar to `SystemTime::now()`, with the result truncated to the
813 /// This is similar to `SystemTime::now()`, with the result truncated to the
785 /// same time resolution as other files’ modification times. Using `.hg`
814 /// same time resolution as other files’ modification times. Using `.hg`
786 /// instead of the system’s default temporary directory (such as `/tmp`) makes
815 /// instead of the system’s default temporary directory (such as `/tmp`) makes
787 /// it more likely the temporary file is in the same disk partition as contents
816 /// it more likely the temporary file is in the same disk partition as contents
788 /// of the working directory, which can matter since different filesystems may
817 /// of the working directory, which can matter since different filesystems may
789 /// store timestamps with different resolutions.
818 /// store timestamps with different resolutions.
790 ///
819 ///
791 /// This may fail, typically if we lack write permissions. In that case we
820 /// This may fail, typically if we lack write permissions. In that case we
792 /// should continue the `status()` algoritm anyway and consider the current
821 /// should continue the `status()` algoritm anyway and consider the current
793 /// date/time to be unknown.
822 /// date/time to be unknown.
794 fn filesystem_now(repo_root: &Path) -> Result<SystemTime, io::Error> {
823 fn filesystem_now(repo_root: &Path) -> Result<SystemTime, io::Error> {
795 tempfile::tempfile_in(repo_root.join(".hg"))?
824 tempfile::tempfile_in(repo_root.join(".hg"))?
796 .metadata()?
825 .metadata()?
797 .modified()
826 .modified()
798 }
827 }
General Comments 0
You need to be logged in to leave comments. Login now