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