##// END OF EJS Templates
rust-status: collect traversed directories if required...
Raphaël Gomès -
r45353:c802ec4f default
parent child Browse files
Show More
@@ -1,915 +1,957 b''
1 // status.rs
1 // status.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! Rust implementation of dirstate.status (dirstate.py).
8 //! Rust implementation of dirstate.status (dirstate.py).
9 //! It is currently missing a lot of functionality compared to the Python one
9 //! It is currently missing a lot of functionality compared to the Python one
10 //! and will only be triggered in narrow cases.
10 //! and will only be triggered in narrow cases.
11
11
12 use crate::{
12 use crate::{
13 dirstate::SIZE_FROM_OTHER_PARENT,
13 dirstate::SIZE_FROM_OTHER_PARENT,
14 filepatterns::PatternFileWarning,
14 filepatterns::PatternFileWarning,
15 matchers::{get_ignore_function, Matcher, VisitChildrenSet},
15 matchers::{get_ignore_function, Matcher, VisitChildrenSet},
16 utils::{
16 utils::{
17 files::{find_dirs, HgMetadata},
17 files::{find_dirs, HgMetadata},
18 hg_path::{
18 hg_path::{
19 hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf,
19 hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf,
20 HgPathError,
20 HgPathError,
21 },
21 },
22 path_auditor::PathAuditor,
22 path_auditor::PathAuditor,
23 },
23 },
24 CopyMap, DirstateEntry, DirstateMap, EntryState, FastHashMap,
24 CopyMap, DirstateEntry, DirstateMap, EntryState, FastHashMap,
25 PatternError,
25 PatternError,
26 };
26 };
27 use lazy_static::lazy_static;
27 use lazy_static::lazy_static;
28 use micro_timer::timed;
28 use micro_timer::timed;
29 use rayon::prelude::*;
29 use rayon::prelude::*;
30 use std::{
30 use std::{
31 borrow::Cow,
31 borrow::Cow,
32 collections::HashSet,
32 collections::HashSet,
33 fs::{read_dir, DirEntry},
33 fs::{read_dir, DirEntry},
34 io::ErrorKind,
34 io::ErrorKind,
35 ops::Deref,
35 ops::Deref,
36 path::{Path, PathBuf},
36 path::{Path, PathBuf},
37 };
37 };
38
38
39 /// Wrong type of file from a `BadMatch`
39 /// Wrong type of file from a `BadMatch`
40 /// Note: a lot of those don't exist on all platforms.
40 /// Note: a lot of those don't exist on all platforms.
41 #[derive(Debug, Copy, Clone)]
41 #[derive(Debug, Copy, Clone)]
42 pub enum BadType {
42 pub enum BadType {
43 CharacterDevice,
43 CharacterDevice,
44 BlockDevice,
44 BlockDevice,
45 FIFO,
45 FIFO,
46 Socket,
46 Socket,
47 Directory,
47 Directory,
48 Unknown,
48 Unknown,
49 }
49 }
50
50
51 impl ToString for BadType {
51 impl ToString for BadType {
52 fn to_string(&self) -> String {
52 fn to_string(&self) -> String {
53 match self {
53 match self {
54 BadType::CharacterDevice => "character device",
54 BadType::CharacterDevice => "character device",
55 BadType::BlockDevice => "block device",
55 BadType::BlockDevice => "block device",
56 BadType::FIFO => "fifo",
56 BadType::FIFO => "fifo",
57 BadType::Socket => "socket",
57 BadType::Socket => "socket",
58 BadType::Directory => "directory",
58 BadType::Directory => "directory",
59 BadType::Unknown => "unknown",
59 BadType::Unknown => "unknown",
60 }
60 }
61 .to_string()
61 .to_string()
62 }
62 }
63 }
63 }
64
64
65 /// Was explicitly matched but cannot be found/accessed
65 /// Was explicitly matched but cannot be found/accessed
66 #[derive(Debug, Copy, Clone)]
66 #[derive(Debug, Copy, Clone)]
67 pub enum BadMatch {
67 pub enum BadMatch {
68 OsError(i32),
68 OsError(i32),
69 BadType(BadType),
69 BadType(BadType),
70 }
70 }
71
71
72 /// Marker enum used to dispatch new status entries into the right collections.
72 /// Marker enum used to dispatch new status entries into the right collections.
73 /// Is similar to `crate::EntryState`, but represents the transient state of
73 /// Is similar to `crate::EntryState`, but represents the transient state of
74 /// entries during the lifetime of a command.
74 /// entries during the lifetime of a command.
75 #[derive(Debug, Copy, Clone)]
75 #[derive(Debug, Copy, Clone)]
76 enum Dispatch {
76 enum Dispatch {
77 Unsure,
77 Unsure,
78 Modified,
78 Modified,
79 Added,
79 Added,
80 Removed,
80 Removed,
81 Deleted,
81 Deleted,
82 Clean,
82 Clean,
83 Unknown,
83 Unknown,
84 Ignored,
84 Ignored,
85 /// Empty dispatch, the file is not worth listing
85 /// Empty dispatch, the file is not worth listing
86 None,
86 None,
87 /// Was explicitly matched but cannot be found/accessed
87 /// Was explicitly matched but cannot be found/accessed
88 Bad(BadMatch),
88 Bad(BadMatch),
89 Directory {
89 Directory {
90 /// True if the directory used to be a file in the dmap so we can say
90 /// True if the directory used to be a file in the dmap so we can say
91 /// that it's been removed.
91 /// that it's been removed.
92 was_file: bool,
92 was_file: bool,
93 },
93 },
94 }
94 }
95
95
96 type IoResult<T> = std::io::Result<T>;
96 type IoResult<T> = std::io::Result<T>;
97 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait, 'static>`, so add
97 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait, 'static>`, so add
98 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
98 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
99 type IgnoreFnType<'a> = Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
99 type IgnoreFnType<'a> = Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
100
100
101 /// Dates and times that are outside the 31-bit signed range are compared
101 /// Dates and times that are outside the 31-bit signed range are compared
102 /// modulo 2^31. This should prevent hg from behaving badly with very large
102 /// modulo 2^31. This should prevent hg from behaving badly with very large
103 /// files or corrupt dates while still having a high probability of detecting
103 /// files or corrupt dates while still having a high probability of detecting
104 /// changes. (issue2608)
104 /// changes. (issue2608)
105 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
105 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
106 /// is not defined for `i32`, and there is no `As` trait. This forces the
106 /// is not defined for `i32`, and there is no `As` trait. This forces the
107 /// caller to cast `b` as `i32`.
107 /// caller to cast `b` as `i32`.
108 fn mod_compare(a: i32, b: i32) -> bool {
108 fn mod_compare(a: i32, b: i32) -> bool {
109 a & i32::max_value() != b & i32::max_value()
109 a & i32::max_value() != b & i32::max_value()
110 }
110 }
111
111
112 /// Return a sorted list containing information about the entries
112 /// Return a sorted list containing information about the entries
113 /// in the directory.
113 /// in the directory.
114 ///
114 ///
115 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
115 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
116 fn list_directory(
116 fn list_directory(
117 path: impl AsRef<Path>,
117 path: impl AsRef<Path>,
118 skip_dot_hg: bool,
118 skip_dot_hg: bool,
119 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
119 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
120 let mut results = vec![];
120 let mut results = vec![];
121 let entries = read_dir(path.as_ref())?;
121 let entries = read_dir(path.as_ref())?;
122
122
123 for entry in entries {
123 for entry in entries {
124 let entry = entry?;
124 let entry = entry?;
125 let filename = os_string_to_hg_path_buf(entry.file_name())?;
125 let filename = os_string_to_hg_path_buf(entry.file_name())?;
126 let file_type = entry.file_type()?;
126 let file_type = entry.file_type()?;
127 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
127 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
128 return Ok(vec![]);
128 return Ok(vec![]);
129 } else {
129 } else {
130 results.push((HgPathBuf::from(filename), entry))
130 results.push((HgPathBuf::from(filename), entry))
131 }
131 }
132 }
132 }
133
133
134 results.sort_unstable_by_key(|e| e.0.clone());
134 results.sort_unstable_by_key(|e| e.0.clone());
135 Ok(results)
135 Ok(results)
136 }
136 }
137
137
138 /// The file corresponding to the dirstate entry was found on the filesystem.
138 /// The file corresponding to the dirstate entry was found on the filesystem.
139 fn dispatch_found(
139 fn dispatch_found(
140 filename: impl AsRef<HgPath>,
140 filename: impl AsRef<HgPath>,
141 entry: DirstateEntry,
141 entry: DirstateEntry,
142 metadata: HgMetadata,
142 metadata: HgMetadata,
143 copy_map: &CopyMap,
143 copy_map: &CopyMap,
144 options: StatusOptions,
144 options: StatusOptions,
145 ) -> Dispatch {
145 ) -> Dispatch {
146 let DirstateEntry {
146 let DirstateEntry {
147 state,
147 state,
148 mode,
148 mode,
149 mtime,
149 mtime,
150 size,
150 size,
151 } = entry;
151 } = entry;
152
152
153 let HgMetadata {
153 let HgMetadata {
154 st_mode,
154 st_mode,
155 st_size,
155 st_size,
156 st_mtime,
156 st_mtime,
157 ..
157 ..
158 } = metadata;
158 } = metadata;
159
159
160 match state {
160 match state {
161 EntryState::Normal => {
161 EntryState::Normal => {
162 let size_changed = mod_compare(size, st_size as i32);
162 let size_changed = mod_compare(size, st_size as i32);
163 let mode_changed =
163 let mode_changed =
164 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
164 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
165 let metadata_changed = size >= 0 && (size_changed || mode_changed);
165 let metadata_changed = size >= 0 && (size_changed || mode_changed);
166 let other_parent = size == SIZE_FROM_OTHER_PARENT;
166 let other_parent = size == SIZE_FROM_OTHER_PARENT;
167 if metadata_changed
167 if metadata_changed
168 || other_parent
168 || other_parent
169 || copy_map.contains_key(filename.as_ref())
169 || copy_map.contains_key(filename.as_ref())
170 {
170 {
171 Dispatch::Modified
171 Dispatch::Modified
172 } else if mod_compare(mtime, st_mtime as i32) {
172 } else if mod_compare(mtime, st_mtime as i32) {
173 Dispatch::Unsure
173 Dispatch::Unsure
174 } else if st_mtime == options.last_normal_time {
174 } else if st_mtime == options.last_normal_time {
175 // the file may have just been marked as normal and
175 // the file may have just been marked as normal and
176 // it may have changed in the same second without
176 // it may have changed in the same second without
177 // changing its size. This can happen if we quickly
177 // changing its size. This can happen if we quickly
178 // do multiple commits. Force lookup, so we don't
178 // do multiple commits. Force lookup, so we don't
179 // miss such a racy file change.
179 // miss such a racy file change.
180 Dispatch::Unsure
180 Dispatch::Unsure
181 } else if options.list_clean {
181 } else if options.list_clean {
182 Dispatch::Clean
182 Dispatch::Clean
183 } else {
183 } else {
184 Dispatch::None
184 Dispatch::None
185 }
185 }
186 }
186 }
187 EntryState::Merged => Dispatch::Modified,
187 EntryState::Merged => Dispatch::Modified,
188 EntryState::Added => Dispatch::Added,
188 EntryState::Added => Dispatch::Added,
189 EntryState::Removed => Dispatch::Removed,
189 EntryState::Removed => Dispatch::Removed,
190 EntryState::Unknown => Dispatch::Unknown,
190 EntryState::Unknown => Dispatch::Unknown,
191 }
191 }
192 }
192 }
193
193
194 /// The file corresponding to this Dirstate entry is missing.
194 /// The file corresponding to this Dirstate entry is missing.
195 fn dispatch_missing(state: EntryState) -> Dispatch {
195 fn dispatch_missing(state: EntryState) -> Dispatch {
196 match state {
196 match state {
197 // File was removed from the filesystem during commands
197 // File was removed from the filesystem during commands
198 EntryState::Normal | EntryState::Merged | EntryState::Added => {
198 EntryState::Normal | EntryState::Merged | EntryState::Added => {
199 Dispatch::Deleted
199 Dispatch::Deleted
200 }
200 }
201 // File was removed, everything is normal
201 // File was removed, everything is normal
202 EntryState::Removed => Dispatch::Removed,
202 EntryState::Removed => Dispatch::Removed,
203 // File is unknown to Mercurial, everything is normal
203 // File is unknown to Mercurial, everything is normal
204 EntryState::Unknown => Dispatch::Unknown,
204 EntryState::Unknown => Dispatch::Unknown,
205 }
205 }
206 }
206 }
207
207
208 lazy_static! {
208 lazy_static! {
209 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
209 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
210 let mut h = HashSet::new();
210 let mut h = HashSet::new();
211 h.insert(HgPath::new(b""));
211 h.insert(HgPath::new(b""));
212 h
212 h
213 };
213 };
214 }
214 }
215
215
216 /// Get stat data about the files explicitly specified by match.
216 /// Get stat data about the files explicitly specified by match.
217 /// TODO subrepos
217 /// TODO subrepos
218 #[timed]
218 #[timed]
219 fn walk_explicit<'a>(
219 fn walk_explicit<'a>(
220 files: Option<&'a HashSet<&HgPath>>,
220 files: Option<&'a HashSet<&HgPath>>,
221 dmap: &'a DirstateMap,
221 dmap: &'a DirstateMap,
222 root_dir: impl AsRef<Path> + Sync + Send + 'a,
222 root_dir: impl AsRef<Path> + Sync + Send + 'a,
223 options: StatusOptions,
223 options: StatusOptions,
224 traversed_sender: crossbeam::Sender<HgPathBuf>,
224 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
225 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
225 files
226 files
226 .unwrap_or(&DEFAULT_WORK)
227 .unwrap_or(&DEFAULT_WORK)
227 .par_iter()
228 .par_iter()
228 .map(move |filename| {
229 .map(move |filename| {
229 // TODO normalization
230 // TODO normalization
230 let normalized = filename.as_ref();
231 let normalized = filename.as_ref();
231
232
232 let buf = match hg_path_to_path_buf(normalized) {
233 let buf = match hg_path_to_path_buf(normalized) {
233 Ok(x) => x,
234 Ok(x) => x,
234 Err(e) => return Some(Err(e.into())),
235 Err(e) => return Some(Err(e.into())),
235 };
236 };
236 let target = root_dir.as_ref().join(buf);
237 let target = root_dir.as_ref().join(buf);
237 let st = target.symlink_metadata();
238 let st = target.symlink_metadata();
238 let in_dmap = dmap.get(normalized);
239 let in_dmap = dmap.get(normalized);
239 match st {
240 match st {
240 Ok(meta) => {
241 Ok(meta) => {
241 let file_type = meta.file_type();
242 let file_type = meta.file_type();
242 return if file_type.is_file() || file_type.is_symlink() {
243 return if file_type.is_file() || file_type.is_symlink() {
243 if let Some(entry) = in_dmap {
244 if let Some(entry) = in_dmap {
244 return Some(Ok((
245 return Some(Ok((
245 normalized,
246 normalized,
246 dispatch_found(
247 dispatch_found(
247 &normalized,
248 &normalized,
248 *entry,
249 *entry,
249 HgMetadata::from_metadata(meta),
250 HgMetadata::from_metadata(meta),
250 &dmap.copy_map,
251 &dmap.copy_map,
251 options,
252 options,
252 ),
253 ),
253 )));
254 )));
254 }
255 }
255 Some(Ok((normalized, Dispatch::Unknown)))
256 Some(Ok((normalized, Dispatch::Unknown)))
256 } else {
257 } else {
257 if file_type.is_dir() {
258 if file_type.is_dir() {
259 if options.collect_traversed_dirs {
260 // The receiver always outlives the sender,
261 // so unwrap.
262 traversed_sender
263 .send(normalized.to_owned())
264 .unwrap()
265 }
258 Some(Ok((
266 Some(Ok((
259 normalized,
267 normalized,
260 Dispatch::Directory {
268 Dispatch::Directory {
261 was_file: in_dmap.is_some(),
269 was_file: in_dmap.is_some(),
262 },
270 },
263 )))
271 )))
264 } else {
272 } else {
265 Some(Ok((
273 Some(Ok((
266 normalized,
274 normalized,
267 Dispatch::Bad(BadMatch::BadType(
275 Dispatch::Bad(BadMatch::BadType(
268 // TODO do more than unknown
276 // TODO do more than unknown
269 // Support for all `BadType` variant
277 // Support for all `BadType` variant
270 // varies greatly between platforms.
278 // varies greatly between platforms.
271 // So far, no tests check the type and
279 // So far, no tests check the type and
272 // this should be good enough for most
280 // this should be good enough for most
273 // users.
281 // users.
274 BadType::Unknown,
282 BadType::Unknown,
275 )),
283 )),
276 )))
284 )))
277 }
285 }
278 };
286 };
279 }
287 }
280 Err(_) => {
288 Err(_) => {
281 if let Some(entry) = in_dmap {
289 if let Some(entry) = in_dmap {
282 return Some(Ok((
290 return Some(Ok((
283 normalized,
291 normalized,
284 dispatch_missing(entry.state),
292 dispatch_missing(entry.state),
285 )));
293 )));
286 }
294 }
287 }
295 }
288 };
296 };
289 None
297 None
290 })
298 })
291 .flatten()
299 .flatten()
292 }
300 }
293
301
294 #[derive(Debug, Copy, Clone)]
302 #[derive(Debug, Copy, Clone)]
295 pub struct StatusOptions {
303 pub struct StatusOptions {
296 /// Remember the most recent modification timeslot for status, to make
304 /// Remember the most recent modification timeslot for status, to make
297 /// sure we won't miss future size-preserving file content modifications
305 /// sure we won't miss future size-preserving file content modifications
298 /// that happen within the same timeslot.
306 /// that happen within the same timeslot.
299 pub last_normal_time: i64,
307 pub last_normal_time: i64,
300 /// Whether we are on a filesystem with UNIX-like exec flags
308 /// Whether we are on a filesystem with UNIX-like exec flags
301 pub check_exec: bool,
309 pub check_exec: bool,
302 pub list_clean: bool,
310 pub list_clean: bool,
303 pub list_unknown: bool,
311 pub list_unknown: bool,
304 pub list_ignored: bool,
312 pub list_ignored: bool,
313 /// Whether to collect traversed dirs for applying a callback later.
314 /// Used by `hg purge` for example.
315 pub collect_traversed_dirs: bool,
305 }
316 }
306
317
307 /// Dispatch a single entry (file, folder, symlink...) found during `traverse`.
318 /// Dispatch a single entry (file, folder, symlink...) found during `traverse`.
308 /// If the entry is a folder that needs to be traversed, it will be handled
319 /// If the entry is a folder that needs to be traversed, it will be handled
309 /// in a separate thread.
320 /// in a separate thread.
310 fn handle_traversed_entry<'a>(
321 fn handle_traversed_entry<'a>(
311 scope: &rayon::Scope<'a>,
322 scope: &rayon::Scope<'a>,
312 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
323 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
313 matcher: &'a (impl Matcher + Sync),
324 matcher: &'a (impl Matcher + Sync),
314 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
325 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
315 dmap: &'a DirstateMap,
326 dmap: &'a DirstateMap,
316 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
327 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
317 ignore_fn: &'a IgnoreFnType,
328 ignore_fn: &'a IgnoreFnType,
318 dir_ignore_fn: &'a IgnoreFnType,
329 dir_ignore_fn: &'a IgnoreFnType,
319 options: StatusOptions,
330 options: StatusOptions,
320 filename: HgPathBuf,
331 filename: HgPathBuf,
321 dir_entry: DirEntry,
332 dir_entry: DirEntry,
333 traversed_sender: crossbeam::Sender<HgPathBuf>,
322 ) -> IoResult<()> {
334 ) -> IoResult<()> {
323 let file_type = dir_entry.file_type()?;
335 let file_type = dir_entry.file_type()?;
324 let entry_option = dmap.get(&filename);
336 let entry_option = dmap.get(&filename);
325
337
326 if filename.as_bytes() == b".hg" {
338 if filename.as_bytes() == b".hg" {
327 // Could be a directory or a symlink
339 // Could be a directory or a symlink
328 return Ok(());
340 return Ok(());
329 }
341 }
330
342
331 if file_type.is_dir() {
343 if file_type.is_dir() {
332 handle_traversed_dir(
344 handle_traversed_dir(
333 scope,
345 scope,
334 files_sender,
346 files_sender,
335 matcher,
347 matcher,
336 root_dir,
348 root_dir,
337 dmap,
349 dmap,
338 old_results,
350 old_results,
339 ignore_fn,
351 ignore_fn,
340 dir_ignore_fn,
352 dir_ignore_fn,
341 options,
353 options,
342 entry_option,
354 entry_option,
343 filename,
355 filename,
356 traversed_sender,
344 );
357 );
345 } else if file_type.is_file() || file_type.is_symlink() {
358 } else if file_type.is_file() || file_type.is_symlink() {
346 if let Some(entry) = entry_option {
359 if let Some(entry) = entry_option {
347 if matcher.matches_everything() || matcher.matches(&filename) {
360 if matcher.matches_everything() || matcher.matches(&filename) {
348 let metadata = dir_entry.metadata()?;
361 let metadata = dir_entry.metadata()?;
349 files_sender
362 files_sender
350 .send(Ok((
363 .send(Ok((
351 filename.to_owned(),
364 filename.to_owned(),
352 dispatch_found(
365 dispatch_found(
353 &filename,
366 &filename,
354 *entry,
367 *entry,
355 HgMetadata::from_metadata(metadata),
368 HgMetadata::from_metadata(metadata),
356 &dmap.copy_map,
369 &dmap.copy_map,
357 options,
370 options,
358 ),
371 ),
359 )))
372 )))
360 .unwrap();
373 .unwrap();
361 }
374 }
362 } else if (matcher.matches_everything() || matcher.matches(&filename))
375 } else if (matcher.matches_everything() || matcher.matches(&filename))
363 && !ignore_fn(&filename)
376 && !ignore_fn(&filename)
364 {
377 {
365 if (options.list_ignored || matcher.exact_match(&filename))
378 if (options.list_ignored || matcher.exact_match(&filename))
366 && dir_ignore_fn(&filename)
379 && dir_ignore_fn(&filename)
367 {
380 {
368 if options.list_ignored {
381 if options.list_ignored {
369 files_sender
382 files_sender
370 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
383 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
371 .unwrap();
384 .unwrap();
372 }
385 }
373 } else {
386 } else {
374 if options.list_unknown {
387 if options.list_unknown {
375 files_sender
388 files_sender
376 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
389 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
377 .unwrap();
390 .unwrap();
378 }
391 }
379 }
392 }
380 } else if ignore_fn(&filename) && options.list_ignored {
393 } else if ignore_fn(&filename) && options.list_ignored {
381 files_sender
394 files_sender
382 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
395 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
383 .unwrap();
396 .unwrap();
384 }
397 }
385 } else if let Some(entry) = entry_option {
398 } else if let Some(entry) = entry_option {
386 // Used to be a file or a folder, now something else.
399 // Used to be a file or a folder, now something else.
387 if matcher.matches_everything() || matcher.matches(&filename) {
400 if matcher.matches_everything() || matcher.matches(&filename) {
388 files_sender
401 files_sender
389 .send(Ok((filename.to_owned(), dispatch_missing(entry.state))))
402 .send(Ok((filename.to_owned(), dispatch_missing(entry.state))))
390 .unwrap();
403 .unwrap();
391 }
404 }
392 }
405 }
393
406
394 Ok(())
407 Ok(())
395 }
408 }
396
409
397 /// A directory was found in the filesystem and needs to be traversed
410 /// A directory was found in the filesystem and needs to be traversed
398 fn handle_traversed_dir<'a>(
411 fn handle_traversed_dir<'a>(
399 scope: &rayon::Scope<'a>,
412 scope: &rayon::Scope<'a>,
400 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
413 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
401 matcher: &'a (impl Matcher + Sync),
414 matcher: &'a (impl Matcher + Sync),
402 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
415 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
403 dmap: &'a DirstateMap,
416 dmap: &'a DirstateMap,
404 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
417 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
405 ignore_fn: &'a IgnoreFnType,
418 ignore_fn: &'a IgnoreFnType,
406 dir_ignore_fn: &'a IgnoreFnType,
419 dir_ignore_fn: &'a IgnoreFnType,
407 options: StatusOptions,
420 options: StatusOptions,
408 entry_option: Option<&'a DirstateEntry>,
421 entry_option: Option<&'a DirstateEntry>,
409 directory: HgPathBuf,
422 directory: HgPathBuf,
423 traversed_sender: crossbeam::Sender<HgPathBuf>,
410 ) {
424 ) {
411 scope.spawn(move |_| {
425 scope.spawn(move |_| {
412 // Nested `if` until `rust-lang/rust#53668` is stable
426 // Nested `if` until `rust-lang/rust#53668` is stable
413 if let Some(entry) = entry_option {
427 if let Some(entry) = entry_option {
414 // Used to be a file, is now a folder
428 // Used to be a file, is now a folder
415 if matcher.matches_everything() || matcher.matches(&directory) {
429 if matcher.matches_everything() || matcher.matches(&directory) {
416 files_sender
430 files_sender
417 .send(Ok((
431 .send(Ok((
418 directory.to_owned(),
432 directory.to_owned(),
419 dispatch_missing(entry.state),
433 dispatch_missing(entry.state),
420 )))
434 )))
421 .unwrap();
435 .unwrap();
422 }
436 }
423 }
437 }
424 // Do we need to traverse it?
438 // Do we need to traverse it?
425 if !ignore_fn(&directory) || options.list_ignored {
439 if !ignore_fn(&directory) || options.list_ignored {
426 traverse_dir(
440 traverse_dir(
427 files_sender,
441 files_sender,
428 matcher,
442 matcher,
429 root_dir,
443 root_dir,
430 dmap,
444 dmap,
431 directory,
445 directory,
432 &old_results,
446 &old_results,
433 ignore_fn,
447 ignore_fn,
434 dir_ignore_fn,
448 dir_ignore_fn,
435 options,
449 options,
450 traversed_sender,
436 )
451 )
437 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
452 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
438 }
453 }
439 });
454 });
440 }
455 }
441
456
442 /// Decides whether the directory needs to be listed, and if so handles the
457 /// Decides whether the directory needs to be listed, and if so handles the
443 /// entries in a separate thread.
458 /// entries in a separate thread.
444 fn traverse_dir<'a>(
459 fn traverse_dir<'a>(
445 files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
460 files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
446 matcher: &'a (impl Matcher + Sync),
461 matcher: &'a (impl Matcher + Sync),
447 root_dir: impl AsRef<Path> + Sync + Send + Copy,
462 root_dir: impl AsRef<Path> + Sync + Send + Copy,
448 dmap: &'a DirstateMap,
463 dmap: &'a DirstateMap,
449 directory: impl AsRef<HgPath>,
464 directory: impl AsRef<HgPath>,
450 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
465 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
451 ignore_fn: &IgnoreFnType,
466 ignore_fn: &IgnoreFnType,
452 dir_ignore_fn: &IgnoreFnType,
467 dir_ignore_fn: &IgnoreFnType,
453 options: StatusOptions,
468 options: StatusOptions,
469 traversed_sender: crossbeam::Sender<HgPathBuf>,
454 ) -> IoResult<()> {
470 ) -> IoResult<()> {
455 let directory = directory.as_ref();
471 let directory = directory.as_ref();
456
472
473 if options.collect_traversed_dirs {
474 // The receiver always outlives the sender, so unwrap.
475 traversed_sender.send(directory.to_owned()).unwrap()
476 }
477
457 let visit_entries = match matcher.visit_children_set(directory) {
478 let visit_entries = match matcher.visit_children_set(directory) {
458 VisitChildrenSet::Empty => return Ok(()),
479 VisitChildrenSet::Empty => return Ok(()),
459 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
480 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
460 VisitChildrenSet::Set(set) => Some(set),
481 VisitChildrenSet::Set(set) => Some(set),
461 };
482 };
462 let buf = hg_path_to_path_buf(directory)?;
483 let buf = hg_path_to_path_buf(directory)?;
463 let dir_path = root_dir.as_ref().join(buf);
484 let dir_path = root_dir.as_ref().join(buf);
464
485
465 let skip_dot_hg = !directory.as_bytes().is_empty();
486 let skip_dot_hg = !directory.as_bytes().is_empty();
466 let entries = match list_directory(dir_path, skip_dot_hg) {
487 let entries = match list_directory(dir_path, skip_dot_hg) {
467 Err(e) => match e.kind() {
488 Err(e) => match e.kind() {
468 ErrorKind::NotFound | ErrorKind::PermissionDenied => {
489 ErrorKind::NotFound | ErrorKind::PermissionDenied => {
469 files_sender
490 files_sender
470 .send(Ok((
491 .send(Ok((
471 directory.to_owned(),
492 directory.to_owned(),
472 Dispatch::Bad(BadMatch::OsError(
493 Dispatch::Bad(BadMatch::OsError(
473 // Unwrapping here is OK because the error always
494 // Unwrapping here is OK because the error always
474 // is a real os error
495 // is a real os error
475 e.raw_os_error().unwrap(),
496 e.raw_os_error().unwrap(),
476 )),
497 )),
477 )))
498 )))
478 .unwrap();
499 .unwrap();
479 return Ok(());
500 return Ok(());
480 }
501 }
481 _ => return Err(e),
502 _ => return Err(e),
482 },
503 },
483 Ok(entries) => entries,
504 Ok(entries) => entries,
484 };
505 };
485
506
486 rayon::scope(|scope| -> IoResult<()> {
507 rayon::scope(|scope| -> IoResult<()> {
487 for (filename, dir_entry) in entries {
508 for (filename, dir_entry) in entries {
488 if let Some(ref set) = visit_entries {
509 if let Some(ref set) = visit_entries {
489 if !set.contains(filename.deref()) {
510 if !set.contains(filename.deref()) {
490 continue;
511 continue;
491 }
512 }
492 }
513 }
493 // TODO normalize
514 // TODO normalize
494 let filename = if directory.is_empty() {
515 let filename = if directory.is_empty() {
495 filename.to_owned()
516 filename.to_owned()
496 } else {
517 } else {
497 directory.join(&filename)
518 directory.join(&filename)
498 };
519 };
499
520
500 if !old_results.contains_key(filename.deref()) {
521 if !old_results.contains_key(filename.deref()) {
501 handle_traversed_entry(
522 handle_traversed_entry(
502 scope,
523 scope,
503 files_sender,
524 files_sender,
504 matcher,
525 matcher,
505 root_dir,
526 root_dir,
506 dmap,
527 dmap,
507 old_results,
528 old_results,
508 ignore_fn,
529 ignore_fn,
509 dir_ignore_fn,
530 dir_ignore_fn,
510 options,
531 options,
511 filename,
532 filename,
512 dir_entry,
533 dir_entry,
534 traversed_sender.clone(),
513 )?;
535 )?;
514 }
536 }
515 }
537 }
516 Ok(())
538 Ok(())
517 })
539 })
518 }
540 }
519
541
520 /// Walk the working directory recursively to look for changes compared to the
542 /// Walk the working directory recursively to look for changes compared to the
521 /// current `DirstateMap`.
543 /// current `DirstateMap`.
522 ///
544 ///
523 /// This takes a mutable reference to the results to account for the `extend`
545 /// This takes a mutable reference to the results to account for the `extend`
524 /// in timings
546 /// in timings
525 #[timed]
547 #[timed]
526 fn traverse<'a>(
548 fn traverse<'a>(
527 matcher: &'a (impl Matcher + Sync),
549 matcher: &'a (impl Matcher + Sync),
528 root_dir: impl AsRef<Path> + Sync + Send + Copy,
550 root_dir: impl AsRef<Path> + Sync + Send + Copy,
529 dmap: &'a DirstateMap,
551 dmap: &'a DirstateMap,
530 path: impl AsRef<HgPath>,
552 path: impl AsRef<HgPath>,
531 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
553 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
532 ignore_fn: &IgnoreFnType,
554 ignore_fn: &IgnoreFnType,
533 dir_ignore_fn: &IgnoreFnType,
555 dir_ignore_fn: &IgnoreFnType,
534 options: StatusOptions,
556 options: StatusOptions,
535 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
557 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
558 traversed_sender: crossbeam::Sender<HgPathBuf>,
536 ) -> IoResult<()> {
559 ) -> IoResult<()> {
537 let root_dir = root_dir.as_ref();
560 let root_dir = root_dir.as_ref();
538
561
539 // The traversal is done in parallel, so use a channel to gather entries.
562 // The traversal is done in parallel, so use a channel to gather entries.
540 // `crossbeam::Sender` is `Send`, while `mpsc::Sender` is not.
563 // `crossbeam::Sender` is `Send`, while `mpsc::Sender` is not.
541 let (files_transmitter, files_receiver) = crossbeam::channel::unbounded();
564 let (files_transmitter, files_receiver) = crossbeam::channel::unbounded();
542
565
543 traverse_dir(
566 traverse_dir(
544 &files_transmitter,
567 &files_transmitter,
545 matcher,
568 matcher,
546 root_dir,
569 root_dir,
547 &dmap,
570 &dmap,
548 path,
571 path,
549 &old_results,
572 &old_results,
550 &ignore_fn,
573 &ignore_fn,
551 &dir_ignore_fn,
574 &dir_ignore_fn,
552 options,
575 options,
576 traversed_sender,
553 )?;
577 )?;
554
578
555 // Disconnect the channel so the receiver stops waiting
579 // Disconnect the channel so the receiver stops waiting
556 drop(files_transmitter);
580 drop(files_transmitter);
557
581
558 // TODO don't collect. Find a way of replicating the behavior of
582 // TODO don't collect. Find a way of replicating the behavior of
559 // `itertools::process_results`, but for `rayon::ParallelIterator`
583 // `itertools::process_results`, but for `rayon::ParallelIterator`
560 let new_results: IoResult<Vec<(Cow<'a, HgPath>, Dispatch)>> =
584 let new_results: IoResult<Vec<(Cow<'a, HgPath>, Dispatch)>> =
561 files_receiver
585 files_receiver
562 .into_iter()
586 .into_iter()
563 .map(|item| {
587 .map(|item| {
564 let (f, d) = item?;
588 let (f, d) = item?;
565 Ok((Cow::Owned(f), d))
589 Ok((Cow::Owned(f), d))
566 })
590 })
567 .collect();
591 .collect();
568
592
569 results.par_extend(new_results?);
593 results.par_extend(new_results?);
570
594
571 Ok(())
595 Ok(())
572 }
596 }
573
597
574 /// Stat all entries in the `DirstateMap` and mark them for dispatch.
598 /// Stat all entries in the `DirstateMap` and mark them for dispatch.
575 fn stat_dmap_entries(
599 fn stat_dmap_entries(
576 dmap: &DirstateMap,
600 dmap: &DirstateMap,
577 root_dir: impl AsRef<Path> + Sync + Send,
601 root_dir: impl AsRef<Path> + Sync + Send,
578 options: StatusOptions,
602 options: StatusOptions,
579 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
603 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
580 dmap.par_iter().map(move |(filename, entry)| {
604 dmap.par_iter().map(move |(filename, entry)| {
581 let filename: &HgPath = filename;
605 let filename: &HgPath = filename;
582 let filename_as_path = hg_path_to_path_buf(filename)?;
606 let filename_as_path = hg_path_to_path_buf(filename)?;
583 let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
607 let meta = root_dir.as_ref().join(filename_as_path).symlink_metadata();
584
608
585 match meta {
609 match meta {
586 Ok(ref m)
610 Ok(ref m)
587 if !(m.file_type().is_file()
611 if !(m.file_type().is_file()
588 || m.file_type().is_symlink()) =>
612 || m.file_type().is_symlink()) =>
589 {
613 {
590 Ok((filename, dispatch_missing(entry.state)))
614 Ok((filename, dispatch_missing(entry.state)))
591 }
615 }
592 Ok(m) => Ok((
616 Ok(m) => Ok((
593 filename,
617 filename,
594 dispatch_found(
618 dispatch_found(
595 filename,
619 filename,
596 *entry,
620 *entry,
597 HgMetadata::from_metadata(m),
621 HgMetadata::from_metadata(m),
598 &dmap.copy_map,
622 &dmap.copy_map,
599 options,
623 options,
600 ),
624 ),
601 )),
625 )),
602 Err(ref e)
626 Err(ref e)
603 if e.kind() == ErrorKind::NotFound
627 if e.kind() == ErrorKind::NotFound
604 || e.raw_os_error() == Some(20) =>
628 || e.raw_os_error() == Some(20) =>
605 {
629 {
606 // Rust does not yet have an `ErrorKind` for
630 // Rust does not yet have an `ErrorKind` for
607 // `NotADirectory` (errno 20)
631 // `NotADirectory` (errno 20)
608 // It happens if the dirstate contains `foo/bar` and
632 // It happens if the dirstate contains `foo/bar` and
609 // foo is not a directory
633 // foo is not a directory
610 Ok((filename, dispatch_missing(entry.state)))
634 Ok((filename, dispatch_missing(entry.state)))
611 }
635 }
612 Err(e) => Err(e),
636 Err(e) => Err(e),
613 }
637 }
614 })
638 })
615 }
639 }
616
640
617 /// This takes a mutable reference to the results to account for the `extend`
641 /// This takes a mutable reference to the results to account for the `extend`
618 /// in timings
642 /// in timings
619 #[timed]
643 #[timed]
620 fn extend_from_dmap<'a>(
644 fn extend_from_dmap<'a>(
621 dmap: &'a DirstateMap,
645 dmap: &'a DirstateMap,
622 root_dir: impl AsRef<Path> + Sync + Send,
646 root_dir: impl AsRef<Path> + Sync + Send,
623 options: StatusOptions,
647 options: StatusOptions,
624 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
648 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
625 ) {
649 ) {
626 results.par_extend(
650 results.par_extend(
627 stat_dmap_entries(dmap, root_dir, options)
651 stat_dmap_entries(dmap, root_dir, options)
628 .flatten()
652 .flatten()
629 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch)),
653 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch)),
630 );
654 );
631 }
655 }
632
656
633 #[derive(Debug)]
657 #[derive(Debug)]
634 pub struct DirstateStatus<'a> {
658 pub struct DirstateStatus<'a> {
635 pub modified: Vec<Cow<'a, HgPath>>,
659 pub modified: Vec<Cow<'a, HgPath>>,
636 pub added: Vec<Cow<'a, HgPath>>,
660 pub added: Vec<Cow<'a, HgPath>>,
637 pub removed: Vec<Cow<'a, HgPath>>,
661 pub removed: Vec<Cow<'a, HgPath>>,
638 pub deleted: Vec<Cow<'a, HgPath>>,
662 pub deleted: Vec<Cow<'a, HgPath>>,
639 pub clean: Vec<Cow<'a, HgPath>>,
663 pub clean: Vec<Cow<'a, HgPath>>,
640 pub ignored: Vec<Cow<'a, HgPath>>,
664 pub ignored: Vec<Cow<'a, HgPath>>,
641 pub unknown: Vec<Cow<'a, HgPath>>,
665 pub unknown: Vec<Cow<'a, HgPath>>,
642 pub bad: Vec<(Cow<'a, HgPath>, BadMatch)>,
666 pub bad: Vec<(Cow<'a, HgPath>, BadMatch)>,
667 /// Only filled if `collect_traversed_dirs` is `true`
668 pub traversed: Vec<HgPathBuf>,
643 }
669 }
644
670
645 #[timed]
671 #[timed]
646 fn build_response<'a>(
672 fn build_response<'a>(
647 results: impl IntoIterator<Item = (Cow<'a, HgPath>, Dispatch)>,
673 results: impl IntoIterator<Item = (Cow<'a, HgPath>, Dispatch)>,
674 traversed: Vec<HgPathBuf>,
648 ) -> (Vec<Cow<'a, HgPath>>, DirstateStatus<'a>) {
675 ) -> (Vec<Cow<'a, HgPath>>, DirstateStatus<'a>) {
649 let mut lookup = vec![];
676 let mut lookup = vec![];
650 let mut modified = vec![];
677 let mut modified = vec![];
651 let mut added = vec![];
678 let mut added = vec![];
652 let mut removed = vec![];
679 let mut removed = vec![];
653 let mut deleted = vec![];
680 let mut deleted = vec![];
654 let mut clean = vec![];
681 let mut clean = vec![];
655 let mut ignored = vec![];
682 let mut ignored = vec![];
656 let mut unknown = vec![];
683 let mut unknown = vec![];
657 let mut bad = vec![];
684 let mut bad = vec![];
658
685
659 for (filename, dispatch) in results.into_iter() {
686 for (filename, dispatch) in results.into_iter() {
660 match dispatch {
687 match dispatch {
661 Dispatch::Unknown => unknown.push(filename),
688 Dispatch::Unknown => unknown.push(filename),
662 Dispatch::Unsure => lookup.push(filename),
689 Dispatch::Unsure => lookup.push(filename),
663 Dispatch::Modified => modified.push(filename),
690 Dispatch::Modified => modified.push(filename),
664 Dispatch::Added => added.push(filename),
691 Dispatch::Added => added.push(filename),
665 Dispatch::Removed => removed.push(filename),
692 Dispatch::Removed => removed.push(filename),
666 Dispatch::Deleted => deleted.push(filename),
693 Dispatch::Deleted => deleted.push(filename),
667 Dispatch::Clean => clean.push(filename),
694 Dispatch::Clean => clean.push(filename),
668 Dispatch::Ignored => ignored.push(filename),
695 Dispatch::Ignored => ignored.push(filename),
669 Dispatch::None => {}
696 Dispatch::None => {}
670 Dispatch::Bad(reason) => bad.push((filename, reason)),
697 Dispatch::Bad(reason) => bad.push((filename, reason)),
671 Dispatch::Directory { .. } => {}
698 Dispatch::Directory { .. } => {}
672 }
699 }
673 }
700 }
674
701
675 (
702 (
676 lookup,
703 lookup,
677 DirstateStatus {
704 DirstateStatus {
678 modified,
705 modified,
679 added,
706 added,
680 removed,
707 removed,
681 deleted,
708 deleted,
682 clean,
709 clean,
683 ignored,
710 ignored,
684 unknown,
711 unknown,
685 bad,
712 bad,
713 traversed,
686 },
714 },
687 )
715 )
688 }
716 }
689
717
690 #[derive(Debug)]
718 #[derive(Debug)]
691 pub enum StatusError {
719 pub enum StatusError {
692 IO(std::io::Error),
720 IO(std::io::Error),
693 Path(HgPathError),
721 Path(HgPathError),
694 Pattern(PatternError),
722 Pattern(PatternError),
695 }
723 }
696
724
697 pub type StatusResult<T> = Result<T, StatusError>;
725 pub type StatusResult<T> = Result<T, StatusError>;
698
726
699 impl From<PatternError> for StatusError {
727 impl From<PatternError> for StatusError {
700 fn from(e: PatternError) -> Self {
728 fn from(e: PatternError) -> Self {
701 StatusError::Pattern(e)
729 StatusError::Pattern(e)
702 }
730 }
703 }
731 }
704 impl From<HgPathError> for StatusError {
732 impl From<HgPathError> for StatusError {
705 fn from(e: HgPathError) -> Self {
733 fn from(e: HgPathError) -> Self {
706 StatusError::Path(e)
734 StatusError::Path(e)
707 }
735 }
708 }
736 }
709 impl From<std::io::Error> for StatusError {
737 impl From<std::io::Error> for StatusError {
710 fn from(e: std::io::Error) -> Self {
738 fn from(e: std::io::Error) -> Self {
711 StatusError::IO(e)
739 StatusError::IO(e)
712 }
740 }
713 }
741 }
714
742
715 impl ToString for StatusError {
743 impl ToString for StatusError {
716 fn to_string(&self) -> String {
744 fn to_string(&self) -> String {
717 match self {
745 match self {
718 StatusError::IO(e) => e.to_string(),
746 StatusError::IO(e) => e.to_string(),
719 StatusError::Path(e) => e.to_string(),
747 StatusError::Path(e) => e.to_string(),
720 StatusError::Pattern(e) => e.to_string(),
748 StatusError::Pattern(e) => e.to_string(),
721 }
749 }
722 }
750 }
723 }
751 }
724
752
725 /// This takes a mutable reference to the results to account for the `extend`
753 /// This takes a mutable reference to the results to account for the `extend`
726 /// in timings
754 /// in timings
727 #[timed]
755 #[timed]
728 fn handle_unknowns<'a>(
756 fn handle_unknowns<'a>(
729 dmap: &'a DirstateMap,
757 dmap: &'a DirstateMap,
730 matcher: &(impl Matcher + Sync),
758 matcher: &(impl Matcher + Sync),
731 root_dir: impl AsRef<Path> + Sync + Send + Copy,
759 root_dir: impl AsRef<Path> + Sync + Send + Copy,
732 options: StatusOptions,
760 options: StatusOptions,
733 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
761 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
734 ) -> IoResult<()> {
762 ) -> IoResult<()> {
735 let to_visit: Vec<(&HgPath, &DirstateEntry)> = if results.is_empty()
763 let to_visit: Vec<(&HgPath, &DirstateEntry)> = if results.is_empty()
736 && matcher.matches_everything()
764 && matcher.matches_everything()
737 {
765 {
738 dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
766 dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
739 } else {
767 } else {
740 // Only convert to a hashmap if needed.
768 // Only convert to a hashmap if needed.
741 let old_results: FastHashMap<_, _> = results.iter().cloned().collect();
769 let old_results: FastHashMap<_, _> = results.iter().cloned().collect();
742 dmap.iter()
770 dmap.iter()
743 .filter_map(move |(f, e)| {
771 .filter_map(move |(f, e)| {
744 if !old_results.contains_key(f.deref()) && matcher.matches(f) {
772 if !old_results.contains_key(f.deref()) && matcher.matches(f) {
745 Some((f.deref(), e))
773 Some((f.deref(), e))
746 } else {
774 } else {
747 None
775 None
748 }
776 }
749 })
777 })
750 .collect()
778 .collect()
751 };
779 };
752
780
753 // We walked all dirs under the roots that weren't ignored, and
781 // We walked all dirs under the roots that weren't ignored, and
754 // everything that matched was stat'ed and is already in results.
782 // everything that matched was stat'ed and is already in results.
755 // The rest must thus be ignored or under a symlink.
783 // The rest must thus be ignored or under a symlink.
756 let path_auditor = PathAuditor::new(root_dir);
784 let path_auditor = PathAuditor::new(root_dir);
757
785
758 // TODO don't collect. Find a way of replicating the behavior of
786 // TODO don't collect. Find a way of replicating the behavior of
759 // `itertools::process_results`, but for `rayon::ParallelIterator`
787 // `itertools::process_results`, but for `rayon::ParallelIterator`
760 let new_results: IoResult<Vec<_>> = to_visit
788 let new_results: IoResult<Vec<_>> = to_visit
761 .into_par_iter()
789 .into_par_iter()
762 .filter_map(|(filename, entry)| -> Option<IoResult<_>> {
790 .filter_map(|(filename, entry)| -> Option<IoResult<_>> {
763 // Report ignored items in the dmap as long as they are not
791 // Report ignored items in the dmap as long as they are not
764 // under a symlink directory.
792 // under a symlink directory.
765 if path_auditor.check(filename) {
793 if path_auditor.check(filename) {
766 // TODO normalize for case-insensitive filesystems
794 // TODO normalize for case-insensitive filesystems
767 let buf = match hg_path_to_path_buf(filename) {
795 let buf = match hg_path_to_path_buf(filename) {
768 Ok(x) => x,
796 Ok(x) => x,
769 Err(e) => return Some(Err(e.into())),
797 Err(e) => return Some(Err(e.into())),
770 };
798 };
771 Some(Ok((
799 Some(Ok((
772 Cow::Borrowed(filename),
800 Cow::Borrowed(filename),
773 match root_dir.as_ref().join(&buf).symlink_metadata() {
801 match root_dir.as_ref().join(&buf).symlink_metadata() {
774 // File was just ignored, no links, and exists
802 // File was just ignored, no links, and exists
775 Ok(meta) => {
803 Ok(meta) => {
776 let metadata = HgMetadata::from_metadata(meta);
804 let metadata = HgMetadata::from_metadata(meta);
777 dispatch_found(
805 dispatch_found(
778 filename,
806 filename,
779 *entry,
807 *entry,
780 metadata,
808 metadata,
781 &dmap.copy_map,
809 &dmap.copy_map,
782 options,
810 options,
783 )
811 )
784 }
812 }
785 // File doesn't exist
813 // File doesn't exist
786 Err(_) => dispatch_missing(entry.state),
814 Err(_) => dispatch_missing(entry.state),
787 },
815 },
788 )))
816 )))
789 } else {
817 } else {
790 // It's either missing or under a symlink directory which
818 // It's either missing or under a symlink directory which
791 // we, in this case, report as missing.
819 // we, in this case, report as missing.
792 Some(Ok((
820 Some(Ok((
793 Cow::Borrowed(filename),
821 Cow::Borrowed(filename),
794 dispatch_missing(entry.state),
822 dispatch_missing(entry.state),
795 )))
823 )))
796 }
824 }
797 })
825 })
798 .collect();
826 .collect();
799
827
800 results.par_extend(new_results?);
828 results.par_extend(new_results?);
801
829
802 Ok(())
830 Ok(())
803 }
831 }
804
832
805 /// Get the status of files in the working directory.
833 /// Get the status of files in the working directory.
806 ///
834 ///
807 /// This is the current entry-point for `hg-core` and is realistically unusable
835 /// This is the current entry-point for `hg-core` and is realistically unusable
808 /// outside of a Python context because its arguments need to provide a lot of
836 /// outside of a Python context because its arguments need to provide a lot of
809 /// information that will not be necessary in the future.
837 /// information that will not be necessary in the future.
810 #[timed]
838 #[timed]
811 pub fn status<'a: 'c, 'b: 'c, 'c>(
839 pub fn status<'a: 'c, 'b: 'c, 'c>(
812 dmap: &'a DirstateMap,
840 dmap: &'a DirstateMap,
813 matcher: &'b (impl Matcher + Sync),
841 matcher: &'b (impl Matcher + Sync),
814 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'c,
842 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'c,
815 ignore_files: Vec<PathBuf>,
843 ignore_files: Vec<PathBuf>,
816 options: StatusOptions,
844 options: StatusOptions,
817 ) -> StatusResult<(
845 ) -> StatusResult<(
818 (Vec<Cow<'c, HgPath>>, DirstateStatus<'c>),
846 (Vec<Cow<'c, HgPath>>, DirstateStatus<'c>),
819 Vec<PatternFileWarning>,
847 Vec<PatternFileWarning>,
820 )> {
848 )> {
821 // Needs to outlive `dir_ignore_fn` since it's captured.
849 // Needs to outlive `dir_ignore_fn` since it's captured.
822 let mut ignore_fn: IgnoreFnType;
850 let mut ignore_fn: IgnoreFnType;
823
851
824 // Only involve real ignore mechanism if we're listing unknowns or ignored.
852 // Only involve real ignore mechanism if we're listing unknowns or ignored.
825 let (dir_ignore_fn, warnings): (IgnoreFnType, _) = if options.list_ignored
853 let (dir_ignore_fn, warnings): (IgnoreFnType, _) = if options.list_ignored
826 || options.list_unknown
854 || options.list_unknown
827 {
855 {
828 let (ignore, warnings) = get_ignore_function(ignore_files, root_dir)?;
856 let (ignore, warnings) = get_ignore_function(ignore_files, root_dir)?;
829
857
830 ignore_fn = ignore;
858 ignore_fn = ignore;
831 let dir_ignore_fn = Box::new(|dir: &_| {
859 let dir_ignore_fn = Box::new(|dir: &_| {
832 // Is the path or one of its ancestors ignored?
860 // Is the path or one of its ancestors ignored?
833 if ignore_fn(dir) {
861 if ignore_fn(dir) {
834 true
862 true
835 } else {
863 } else {
836 for p in find_dirs(dir) {
864 for p in find_dirs(dir) {
837 if ignore_fn(p) {
865 if ignore_fn(p) {
838 return true;
866 return true;
839 }
867 }
840 }
868 }
841 false
869 false
842 }
870 }
843 });
871 });
844 (dir_ignore_fn, warnings)
872 (dir_ignore_fn, warnings)
845 } else {
873 } else {
846 ignore_fn = Box::new(|&_| true);
874 ignore_fn = Box::new(|&_| true);
847 (Box::new(|&_| true), vec![])
875 (Box::new(|&_| true), vec![])
848 };
876 };
849
877
850 let files = matcher.file_set();
878 let files = matcher.file_set();
851
879
880 // `crossbeam::Sender` is `Send`, while `mpsc::Sender` is not.
881 let (traversed_sender, traversed_recv) = crossbeam::channel::unbounded();
882
852 // Step 1: check the files explicitly mentioned by the user
883 // Step 1: check the files explicitly mentioned by the user
853 let explicit = walk_explicit(files, &dmap, root_dir, options);
884 let explicit = walk_explicit(
885 files,
886 &dmap,
887 root_dir,
888 options,
889 traversed_sender.clone(),
890 );
854
891
855 // Collect results into a `Vec` because we do very few lookups in most
892 // Collect results into a `Vec` because we do very few lookups in most
856 // cases.
893 // cases.
857 let (work, mut results): (Vec<_>, Vec<_>) = explicit
894 let (work, mut results): (Vec<_>, Vec<_>) = explicit
858 .filter_map(Result::ok)
895 .filter_map(Result::ok)
859 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch))
896 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch))
860 .partition(|(_, dispatch)| match dispatch {
897 .partition(|(_, dispatch)| match dispatch {
861 Dispatch::Directory { .. } => true,
898 Dispatch::Directory { .. } => true,
862 _ => false,
899 _ => false,
863 });
900 });
864
901
865 if !work.is_empty() {
902 if !work.is_empty() {
866 // Hashmaps are quite a bit slower to build than vecs, so only build it
903 // Hashmaps are quite a bit slower to build than vecs, so only build it
867 // if needed.
904 // if needed.
868 let old_results = results.iter().cloned().collect();
905 let old_results = results.iter().cloned().collect();
869
906
870 // Step 2: recursively check the working directory for changes if
907 // Step 2: recursively check the working directory for changes if
871 // needed
908 // needed
872 for (dir, dispatch) in work {
909 for (dir, dispatch) in work {
873 match dispatch {
910 match dispatch {
874 Dispatch::Directory { was_file } => {
911 Dispatch::Directory { was_file } => {
875 if was_file {
912 if was_file {
876 results.push((dir.to_owned(), Dispatch::Removed));
913 results.push((dir.to_owned(), Dispatch::Removed));
877 }
914 }
878 if options.list_ignored
915 if options.list_ignored
879 || options.list_unknown && !dir_ignore_fn(&dir)
916 || options.list_unknown && !dir_ignore_fn(&dir)
880 {
917 {
881 traverse(
918 traverse(
882 matcher,
919 matcher,
883 root_dir,
920 root_dir,
884 &dmap,
921 &dmap,
885 &dir,
922 &dir,
886 &old_results,
923 &old_results,
887 &ignore_fn,
924 &ignore_fn,
888 &dir_ignore_fn,
925 &dir_ignore_fn,
889 options,
926 options,
890 &mut results,
927 &mut results,
928 traversed_sender.clone(),
891 )?;
929 )?;
892 }
930 }
893 }
931 }
894 _ => unreachable!("There can only be directories in `work`"),
932 _ => unreachable!("There can only be directories in `work`"),
895 }
933 }
896 }
934 }
897 }
935 }
898
936
899 if !matcher.is_exact() {
937 if !matcher.is_exact() {
900 // Step 3: Check the remaining files from the dmap.
938 // Step 3: Check the remaining files from the dmap.
901 // If a dmap file is not in results yet, it was either
939 // If a dmap file is not in results yet, it was either
902 // a) not matched b) ignored, c) missing, or d) under a
940 // a) not matched b) ignored, c) missing, or d) under a
903 // symlink directory.
941 // symlink directory.
904
942
905 if options.list_unknown {
943 if options.list_unknown {
906 handle_unknowns(dmap, matcher, root_dir, options, &mut results)?;
944 handle_unknowns(dmap, matcher, root_dir, options, &mut results)?;
907 } else {
945 } else {
908 // We may not have walked the full directory tree above, so stat
946 // We may not have walked the full directory tree above, so stat
909 // and check everything we missed.
947 // and check everything we missed.
910 extend_from_dmap(&dmap, root_dir, options, &mut results);
948 extend_from_dmap(&dmap, root_dir, options, &mut results);
911 }
949 }
912 }
950 }
913
951
914 Ok((build_response(results), warnings))
952 // Close the channel
953 drop(traversed_sender);
954 let traversed_dirs = traversed_recv.into_iter().collect();
955
956 Ok((build_response(results, traversed_dirs), warnings))
915 }
957 }
General Comments 0
You need to be logged in to leave comments. Login now