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