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