##// END OF EJS Templates
rust: fix false comment about mpsc::Sender...
Yuya Nishihara -
r45444:ce8fe771 default
parent child Browse files
Show More
@@ -1,957 +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 traversed_sender: crossbeam::Sender<HgPathBuf>,
225 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
225 ) -> impl ParallelIterator<Item = IoResult<(&'a HgPath, Dispatch)>> {
226 files
226 files
227 .unwrap_or(&DEFAULT_WORK)
227 .unwrap_or(&DEFAULT_WORK)
228 .par_iter()
228 .par_iter()
229 .map(move |filename| {
229 .map(move |filename| {
230 // TODO normalization
230 // TODO normalization
231 let normalized = filename.as_ref();
231 let normalized = filename.as_ref();
232
232
233 let buf = match hg_path_to_path_buf(normalized) {
233 let buf = match hg_path_to_path_buf(normalized) {
234 Ok(x) => x,
234 Ok(x) => x,
235 Err(e) => return Some(Err(e.into())),
235 Err(e) => return Some(Err(e.into())),
236 };
236 };
237 let target = root_dir.as_ref().join(buf);
237 let target = root_dir.as_ref().join(buf);
238 let st = target.symlink_metadata();
238 let st = target.symlink_metadata();
239 let in_dmap = dmap.get(normalized);
239 let in_dmap = dmap.get(normalized);
240 match st {
240 match st {
241 Ok(meta) => {
241 Ok(meta) => {
242 let file_type = meta.file_type();
242 let file_type = meta.file_type();
243 return if file_type.is_file() || file_type.is_symlink() {
243 return if file_type.is_file() || file_type.is_symlink() {
244 if let Some(entry) = in_dmap {
244 if let Some(entry) = in_dmap {
245 return Some(Ok((
245 return Some(Ok((
246 normalized,
246 normalized,
247 dispatch_found(
247 dispatch_found(
248 &normalized,
248 &normalized,
249 *entry,
249 *entry,
250 HgMetadata::from_metadata(meta),
250 HgMetadata::from_metadata(meta),
251 &dmap.copy_map,
251 &dmap.copy_map,
252 options,
252 options,
253 ),
253 ),
254 )));
254 )));
255 }
255 }
256 Some(Ok((normalized, Dispatch::Unknown)))
256 Some(Ok((normalized, Dispatch::Unknown)))
257 } else {
257 } else {
258 if file_type.is_dir() {
258 if file_type.is_dir() {
259 if options.collect_traversed_dirs {
259 if options.collect_traversed_dirs {
260 // The receiver always outlives the sender,
260 // The receiver always outlives the sender,
261 // so unwrap.
261 // so unwrap.
262 traversed_sender
262 traversed_sender
263 .send(normalized.to_owned())
263 .send(normalized.to_owned())
264 .unwrap()
264 .unwrap()
265 }
265 }
266 Some(Ok((
266 Some(Ok((
267 normalized,
267 normalized,
268 Dispatch::Directory {
268 Dispatch::Directory {
269 was_file: in_dmap.is_some(),
269 was_file: in_dmap.is_some(),
270 },
270 },
271 )))
271 )))
272 } else {
272 } else {
273 Some(Ok((
273 Some(Ok((
274 normalized,
274 normalized,
275 Dispatch::Bad(BadMatch::BadType(
275 Dispatch::Bad(BadMatch::BadType(
276 // TODO do more than unknown
276 // TODO do more than unknown
277 // Support for all `BadType` variant
277 // Support for all `BadType` variant
278 // varies greatly between platforms.
278 // varies greatly between platforms.
279 // So far, no tests check the type and
279 // So far, no tests check the type and
280 // this should be good enough for most
280 // this should be good enough for most
281 // users.
281 // users.
282 BadType::Unknown,
282 BadType::Unknown,
283 )),
283 )),
284 )))
284 )))
285 }
285 }
286 };
286 };
287 }
287 }
288 Err(_) => {
288 Err(_) => {
289 if let Some(entry) = in_dmap {
289 if let Some(entry) = in_dmap {
290 return Some(Ok((
290 return Some(Ok((
291 normalized,
291 normalized,
292 dispatch_missing(entry.state),
292 dispatch_missing(entry.state),
293 )));
293 )));
294 }
294 }
295 }
295 }
296 };
296 };
297 None
297 None
298 })
298 })
299 .flatten()
299 .flatten()
300 }
300 }
301
301
302 #[derive(Debug, Copy, Clone)]
302 #[derive(Debug, Copy, Clone)]
303 pub struct StatusOptions {
303 pub struct StatusOptions {
304 /// Remember the most recent modification timeslot for status, to make
304 /// Remember the most recent modification timeslot for status, to make
305 /// sure we won't miss future size-preserving file content modifications
305 /// sure we won't miss future size-preserving file content modifications
306 /// that happen within the same timeslot.
306 /// that happen within the same timeslot.
307 pub last_normal_time: i64,
307 pub last_normal_time: i64,
308 /// Whether we are on a filesystem with UNIX-like exec flags
308 /// Whether we are on a filesystem with UNIX-like exec flags
309 pub check_exec: bool,
309 pub check_exec: bool,
310 pub list_clean: bool,
310 pub list_clean: bool,
311 pub list_unknown: bool,
311 pub list_unknown: bool,
312 pub list_ignored: bool,
312 pub list_ignored: bool,
313 /// Whether to collect traversed dirs for applying a callback later.
313 /// Whether to collect traversed dirs for applying a callback later.
314 /// Used by `hg purge` for example.
314 /// Used by `hg purge` for example.
315 pub collect_traversed_dirs: bool,
315 pub collect_traversed_dirs: bool,
316 }
316 }
317
317
318 /// Dispatch a single entry (file, folder, symlink...) found during `traverse`.
318 /// Dispatch a single entry (file, folder, symlink...) found during `traverse`.
319 /// 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
320 /// in a separate thread.
320 /// in a separate thread.
321 fn handle_traversed_entry<'a>(
321 fn handle_traversed_entry<'a>(
322 scope: &rayon::Scope<'a>,
322 scope: &rayon::Scope<'a>,
323 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
323 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
324 matcher: &'a (impl Matcher + Sync),
324 matcher: &'a (impl Matcher + Sync),
325 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
325 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
326 dmap: &'a DirstateMap,
326 dmap: &'a DirstateMap,
327 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
327 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
328 ignore_fn: &'a IgnoreFnType,
328 ignore_fn: &'a IgnoreFnType,
329 dir_ignore_fn: &'a IgnoreFnType,
329 dir_ignore_fn: &'a IgnoreFnType,
330 options: StatusOptions,
330 options: StatusOptions,
331 filename: HgPathBuf,
331 filename: HgPathBuf,
332 dir_entry: DirEntry,
332 dir_entry: DirEntry,
333 traversed_sender: crossbeam::Sender<HgPathBuf>,
333 traversed_sender: crossbeam::Sender<HgPathBuf>,
334 ) -> IoResult<()> {
334 ) -> IoResult<()> {
335 let file_type = dir_entry.file_type()?;
335 let file_type = dir_entry.file_type()?;
336 let entry_option = dmap.get(&filename);
336 let entry_option = dmap.get(&filename);
337
337
338 if filename.as_bytes() == b".hg" {
338 if filename.as_bytes() == b".hg" {
339 // Could be a directory or a symlink
339 // Could be a directory or a symlink
340 return Ok(());
340 return Ok(());
341 }
341 }
342
342
343 if file_type.is_dir() {
343 if file_type.is_dir() {
344 handle_traversed_dir(
344 handle_traversed_dir(
345 scope,
345 scope,
346 files_sender,
346 files_sender,
347 matcher,
347 matcher,
348 root_dir,
348 root_dir,
349 dmap,
349 dmap,
350 old_results,
350 old_results,
351 ignore_fn,
351 ignore_fn,
352 dir_ignore_fn,
352 dir_ignore_fn,
353 options,
353 options,
354 entry_option,
354 entry_option,
355 filename,
355 filename,
356 traversed_sender,
356 traversed_sender,
357 );
357 );
358 } else if file_type.is_file() || file_type.is_symlink() {
358 } else if file_type.is_file() || file_type.is_symlink() {
359 if let Some(entry) = entry_option {
359 if let Some(entry) = entry_option {
360 if matcher.matches_everything() || matcher.matches(&filename) {
360 if matcher.matches_everything() || matcher.matches(&filename) {
361 let metadata = dir_entry.metadata()?;
361 let metadata = dir_entry.metadata()?;
362 files_sender
362 files_sender
363 .send(Ok((
363 .send(Ok((
364 filename.to_owned(),
364 filename.to_owned(),
365 dispatch_found(
365 dispatch_found(
366 &filename,
366 &filename,
367 *entry,
367 *entry,
368 HgMetadata::from_metadata(metadata),
368 HgMetadata::from_metadata(metadata),
369 &dmap.copy_map,
369 &dmap.copy_map,
370 options,
370 options,
371 ),
371 ),
372 )))
372 )))
373 .unwrap();
373 .unwrap();
374 }
374 }
375 } else if (matcher.matches_everything() || matcher.matches(&filename))
375 } else if (matcher.matches_everything() || matcher.matches(&filename))
376 && !ignore_fn(&filename)
376 && !ignore_fn(&filename)
377 {
377 {
378 if (options.list_ignored || matcher.exact_match(&filename))
378 if (options.list_ignored || matcher.exact_match(&filename))
379 && dir_ignore_fn(&filename)
379 && dir_ignore_fn(&filename)
380 {
380 {
381 if options.list_ignored {
381 if options.list_ignored {
382 files_sender
382 files_sender
383 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
383 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
384 .unwrap();
384 .unwrap();
385 }
385 }
386 } else {
386 } else {
387 if options.list_unknown {
387 if options.list_unknown {
388 files_sender
388 files_sender
389 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
389 .send(Ok((filename.to_owned(), Dispatch::Unknown)))
390 .unwrap();
390 .unwrap();
391 }
391 }
392 }
392 }
393 } else if ignore_fn(&filename) && options.list_ignored {
393 } else if ignore_fn(&filename) && options.list_ignored {
394 files_sender
394 files_sender
395 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
395 .send(Ok((filename.to_owned(), Dispatch::Ignored)))
396 .unwrap();
396 .unwrap();
397 }
397 }
398 } else if let Some(entry) = entry_option {
398 } else if let Some(entry) = entry_option {
399 // Used to be a file or a folder, now something else.
399 // Used to be a file or a folder, now something else.
400 if matcher.matches_everything() || matcher.matches(&filename) {
400 if matcher.matches_everything() || matcher.matches(&filename) {
401 files_sender
401 files_sender
402 .send(Ok((filename.to_owned(), dispatch_missing(entry.state))))
402 .send(Ok((filename.to_owned(), dispatch_missing(entry.state))))
403 .unwrap();
403 .unwrap();
404 }
404 }
405 }
405 }
406
406
407 Ok(())
407 Ok(())
408 }
408 }
409
409
410 /// 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
411 fn handle_traversed_dir<'a>(
411 fn handle_traversed_dir<'a>(
412 scope: &rayon::Scope<'a>,
412 scope: &rayon::Scope<'a>,
413 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
413 files_sender: &'a crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
414 matcher: &'a (impl Matcher + Sync),
414 matcher: &'a (impl Matcher + Sync),
415 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
415 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'a,
416 dmap: &'a DirstateMap,
416 dmap: &'a DirstateMap,
417 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
417 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
418 ignore_fn: &'a IgnoreFnType,
418 ignore_fn: &'a IgnoreFnType,
419 dir_ignore_fn: &'a IgnoreFnType,
419 dir_ignore_fn: &'a IgnoreFnType,
420 options: StatusOptions,
420 options: StatusOptions,
421 entry_option: Option<&'a DirstateEntry>,
421 entry_option: Option<&'a DirstateEntry>,
422 directory: HgPathBuf,
422 directory: HgPathBuf,
423 traversed_sender: crossbeam::Sender<HgPathBuf>,
423 traversed_sender: crossbeam::Sender<HgPathBuf>,
424 ) {
424 ) {
425 scope.spawn(move |_| {
425 scope.spawn(move |_| {
426 // Nested `if` until `rust-lang/rust#53668` is stable
426 // Nested `if` until `rust-lang/rust#53668` is stable
427 if let Some(entry) = entry_option {
427 if let Some(entry) = entry_option {
428 // Used to be a file, is now a folder
428 // Used to be a file, is now a folder
429 if matcher.matches_everything() || matcher.matches(&directory) {
429 if matcher.matches_everything() || matcher.matches(&directory) {
430 files_sender
430 files_sender
431 .send(Ok((
431 .send(Ok((
432 directory.to_owned(),
432 directory.to_owned(),
433 dispatch_missing(entry.state),
433 dispatch_missing(entry.state),
434 )))
434 )))
435 .unwrap();
435 .unwrap();
436 }
436 }
437 }
437 }
438 // Do we need to traverse it?
438 // Do we need to traverse it?
439 if !ignore_fn(&directory) || options.list_ignored {
439 if !ignore_fn(&directory) || options.list_ignored {
440 traverse_dir(
440 traverse_dir(
441 files_sender,
441 files_sender,
442 matcher,
442 matcher,
443 root_dir,
443 root_dir,
444 dmap,
444 dmap,
445 directory,
445 directory,
446 &old_results,
446 &old_results,
447 ignore_fn,
447 ignore_fn,
448 dir_ignore_fn,
448 dir_ignore_fn,
449 options,
449 options,
450 traversed_sender,
450 traversed_sender,
451 )
451 )
452 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
452 .unwrap_or_else(|e| files_sender.send(Err(e)).unwrap())
453 }
453 }
454 });
454 });
455 }
455 }
456
456
457 /// 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
458 /// entries in a separate thread.
458 /// entries in a separate thread.
459 fn traverse_dir<'a>(
459 fn traverse_dir<'a>(
460 files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
460 files_sender: &crossbeam::Sender<IoResult<(HgPathBuf, Dispatch)>>,
461 matcher: &'a (impl Matcher + Sync),
461 matcher: &'a (impl Matcher + Sync),
462 root_dir: impl AsRef<Path> + Sync + Send + Copy,
462 root_dir: impl AsRef<Path> + Sync + Send + Copy,
463 dmap: &'a DirstateMap,
463 dmap: &'a DirstateMap,
464 directory: impl AsRef<HgPath>,
464 directory: impl AsRef<HgPath>,
465 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
465 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
466 ignore_fn: &IgnoreFnType,
466 ignore_fn: &IgnoreFnType,
467 dir_ignore_fn: &IgnoreFnType,
467 dir_ignore_fn: &IgnoreFnType,
468 options: StatusOptions,
468 options: StatusOptions,
469 traversed_sender: crossbeam::Sender<HgPathBuf>,
469 traversed_sender: crossbeam::Sender<HgPathBuf>,
470 ) -> IoResult<()> {
470 ) -> IoResult<()> {
471 let directory = directory.as_ref();
471 let directory = directory.as_ref();
472
472
473 if options.collect_traversed_dirs {
473 if options.collect_traversed_dirs {
474 // The receiver always outlives the sender, so unwrap.
474 // The receiver always outlives the sender, so unwrap.
475 traversed_sender.send(directory.to_owned()).unwrap()
475 traversed_sender.send(directory.to_owned()).unwrap()
476 }
476 }
477
477
478 let visit_entries = match matcher.visit_children_set(directory) {
478 let visit_entries = match matcher.visit_children_set(directory) {
479 VisitChildrenSet::Empty => return Ok(()),
479 VisitChildrenSet::Empty => return Ok(()),
480 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
480 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
481 VisitChildrenSet::Set(set) => Some(set),
481 VisitChildrenSet::Set(set) => Some(set),
482 };
482 };
483 let buf = hg_path_to_path_buf(directory)?;
483 let buf = hg_path_to_path_buf(directory)?;
484 let dir_path = root_dir.as_ref().join(buf);
484 let dir_path = root_dir.as_ref().join(buf);
485
485
486 let skip_dot_hg = !directory.as_bytes().is_empty();
486 let skip_dot_hg = !directory.as_bytes().is_empty();
487 let entries = match list_directory(dir_path, skip_dot_hg) {
487 let entries = match list_directory(dir_path, skip_dot_hg) {
488 Err(e) => match e.kind() {
488 Err(e) => match e.kind() {
489 ErrorKind::NotFound | ErrorKind::PermissionDenied => {
489 ErrorKind::NotFound | ErrorKind::PermissionDenied => {
490 files_sender
490 files_sender
491 .send(Ok((
491 .send(Ok((
492 directory.to_owned(),
492 directory.to_owned(),
493 Dispatch::Bad(BadMatch::OsError(
493 Dispatch::Bad(BadMatch::OsError(
494 // Unwrapping here is OK because the error always
494 // Unwrapping here is OK because the error always
495 // is a real os error
495 // is a real os error
496 e.raw_os_error().unwrap(),
496 e.raw_os_error().unwrap(),
497 )),
497 )),
498 )))
498 )))
499 .unwrap();
499 .unwrap();
500 return Ok(());
500 return Ok(());
501 }
501 }
502 _ => return Err(e),
502 _ => return Err(e),
503 },
503 },
504 Ok(entries) => entries,
504 Ok(entries) => entries,
505 };
505 };
506
506
507 rayon::scope(|scope| -> IoResult<()> {
507 rayon::scope(|scope| -> IoResult<()> {
508 for (filename, dir_entry) in entries {
508 for (filename, dir_entry) in entries {
509 if let Some(ref set) = visit_entries {
509 if let Some(ref set) = visit_entries {
510 if !set.contains(filename.deref()) {
510 if !set.contains(filename.deref()) {
511 continue;
511 continue;
512 }
512 }
513 }
513 }
514 // TODO normalize
514 // TODO normalize
515 let filename = if directory.is_empty() {
515 let filename = if directory.is_empty() {
516 filename.to_owned()
516 filename.to_owned()
517 } else {
517 } else {
518 directory.join(&filename)
518 directory.join(&filename)
519 };
519 };
520
520
521 if !old_results.contains_key(filename.deref()) {
521 if !old_results.contains_key(filename.deref()) {
522 handle_traversed_entry(
522 handle_traversed_entry(
523 scope,
523 scope,
524 files_sender,
524 files_sender,
525 matcher,
525 matcher,
526 root_dir,
526 root_dir,
527 dmap,
527 dmap,
528 old_results,
528 old_results,
529 ignore_fn,
529 ignore_fn,
530 dir_ignore_fn,
530 dir_ignore_fn,
531 options,
531 options,
532 filename,
532 filename,
533 dir_entry,
533 dir_entry,
534 traversed_sender.clone(),
534 traversed_sender.clone(),
535 )?;
535 )?;
536 }
536 }
537 }
537 }
538 Ok(())
538 Ok(())
539 })
539 })
540 }
540 }
541
541
542 /// 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
543 /// current `DirstateMap`.
543 /// current `DirstateMap`.
544 ///
544 ///
545 /// 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`
546 /// in timings
546 /// in timings
547 #[timed]
547 #[timed]
548 fn traverse<'a>(
548 fn traverse<'a>(
549 matcher: &'a (impl Matcher + Sync),
549 matcher: &'a (impl Matcher + Sync),
550 root_dir: impl AsRef<Path> + Sync + Send + Copy,
550 root_dir: impl AsRef<Path> + Sync + Send + Copy,
551 dmap: &'a DirstateMap,
551 dmap: &'a DirstateMap,
552 path: impl AsRef<HgPath>,
552 path: impl AsRef<HgPath>,
553 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
553 old_results: &FastHashMap<Cow<'a, HgPath>, Dispatch>,
554 ignore_fn: &IgnoreFnType,
554 ignore_fn: &IgnoreFnType,
555 dir_ignore_fn: &IgnoreFnType,
555 dir_ignore_fn: &IgnoreFnType,
556 options: StatusOptions,
556 options: StatusOptions,
557 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
557 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
558 traversed_sender: crossbeam::Sender<HgPathBuf>,
558 traversed_sender: crossbeam::Sender<HgPathBuf>,
559 ) -> IoResult<()> {
559 ) -> IoResult<()> {
560 let root_dir = root_dir.as_ref();
560 let root_dir = root_dir.as_ref();
561
561
562 // 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.
563 // `crossbeam::Sender` is `Send`, while `mpsc::Sender` is not.
563 // `crossbeam::Sender` is `Sync`, while `mpsc::Sender` is not.
564 let (files_transmitter, files_receiver) = crossbeam::channel::unbounded();
564 let (files_transmitter, files_receiver) = crossbeam::channel::unbounded();
565
565
566 traverse_dir(
566 traverse_dir(
567 &files_transmitter,
567 &files_transmitter,
568 matcher,
568 matcher,
569 root_dir,
569 root_dir,
570 &dmap,
570 &dmap,
571 path,
571 path,
572 &old_results,
572 &old_results,
573 &ignore_fn,
573 &ignore_fn,
574 &dir_ignore_fn,
574 &dir_ignore_fn,
575 options,
575 options,
576 traversed_sender,
576 traversed_sender,
577 )?;
577 )?;
578
578
579 // Disconnect the channel so the receiver stops waiting
579 // Disconnect the channel so the receiver stops waiting
580 drop(files_transmitter);
580 drop(files_transmitter);
581
581
582 // 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
583 // `itertools::process_results`, but for `rayon::ParallelIterator`
583 // `itertools::process_results`, but for `rayon::ParallelIterator`
584 let new_results: IoResult<Vec<(Cow<'a, HgPath>, Dispatch)>> =
584 let new_results: IoResult<Vec<(Cow<'a, HgPath>, Dispatch)>> =
585 files_receiver
585 files_receiver
586 .into_iter()
586 .into_iter()
587 .map(|item| {
587 .map(|item| {
588 let (f, d) = item?;
588 let (f, d) = item?;
589 Ok((Cow::Owned(f), d))
589 Ok((Cow::Owned(f), d))
590 })
590 })
591 .collect();
591 .collect();
592
592
593 results.par_extend(new_results?);
593 results.par_extend(new_results?);
594
594
595 Ok(())
595 Ok(())
596 }
596 }
597
597
598 /// Stat all entries in the `DirstateMap` and mark them for dispatch.
598 /// Stat all entries in the `DirstateMap` and mark them for dispatch.
599 fn stat_dmap_entries(
599 fn stat_dmap_entries(
600 dmap: &DirstateMap,
600 dmap: &DirstateMap,
601 root_dir: impl AsRef<Path> + Sync + Send,
601 root_dir: impl AsRef<Path> + Sync + Send,
602 options: StatusOptions,
602 options: StatusOptions,
603 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
603 ) -> impl ParallelIterator<Item = IoResult<(&HgPath, Dispatch)>> {
604 dmap.par_iter().map(move |(filename, entry)| {
604 dmap.par_iter().map(move |(filename, entry)| {
605 let filename: &HgPath = filename;
605 let filename: &HgPath = filename;
606 let filename_as_path = hg_path_to_path_buf(filename)?;
606 let filename_as_path = hg_path_to_path_buf(filename)?;
607 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();
608
608
609 match meta {
609 match meta {
610 Ok(ref m)
610 Ok(ref m)
611 if !(m.file_type().is_file()
611 if !(m.file_type().is_file()
612 || m.file_type().is_symlink()) =>
612 || m.file_type().is_symlink()) =>
613 {
613 {
614 Ok((filename, dispatch_missing(entry.state)))
614 Ok((filename, dispatch_missing(entry.state)))
615 }
615 }
616 Ok(m) => Ok((
616 Ok(m) => Ok((
617 filename,
617 filename,
618 dispatch_found(
618 dispatch_found(
619 filename,
619 filename,
620 *entry,
620 *entry,
621 HgMetadata::from_metadata(m),
621 HgMetadata::from_metadata(m),
622 &dmap.copy_map,
622 &dmap.copy_map,
623 options,
623 options,
624 ),
624 ),
625 )),
625 )),
626 Err(ref e)
626 Err(ref e)
627 if e.kind() == ErrorKind::NotFound
627 if e.kind() == ErrorKind::NotFound
628 || e.raw_os_error() == Some(20) =>
628 || e.raw_os_error() == Some(20) =>
629 {
629 {
630 // Rust does not yet have an `ErrorKind` for
630 // Rust does not yet have an `ErrorKind` for
631 // `NotADirectory` (errno 20)
631 // `NotADirectory` (errno 20)
632 // It happens if the dirstate contains `foo/bar` and
632 // It happens if the dirstate contains `foo/bar` and
633 // foo is not a directory
633 // foo is not a directory
634 Ok((filename, dispatch_missing(entry.state)))
634 Ok((filename, dispatch_missing(entry.state)))
635 }
635 }
636 Err(e) => Err(e),
636 Err(e) => Err(e),
637 }
637 }
638 })
638 })
639 }
639 }
640
640
641 /// 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`
642 /// in timings
642 /// in timings
643 #[timed]
643 #[timed]
644 fn extend_from_dmap<'a>(
644 fn extend_from_dmap<'a>(
645 dmap: &'a DirstateMap,
645 dmap: &'a DirstateMap,
646 root_dir: impl AsRef<Path> + Sync + Send,
646 root_dir: impl AsRef<Path> + Sync + Send,
647 options: StatusOptions,
647 options: StatusOptions,
648 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
648 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
649 ) {
649 ) {
650 results.par_extend(
650 results.par_extend(
651 stat_dmap_entries(dmap, root_dir, options)
651 stat_dmap_entries(dmap, root_dir, options)
652 .flatten()
652 .flatten()
653 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch)),
653 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch)),
654 );
654 );
655 }
655 }
656
656
657 #[derive(Debug)]
657 #[derive(Debug)]
658 pub struct DirstateStatus<'a> {
658 pub struct DirstateStatus<'a> {
659 pub modified: Vec<Cow<'a, HgPath>>,
659 pub modified: Vec<Cow<'a, HgPath>>,
660 pub added: Vec<Cow<'a, HgPath>>,
660 pub added: Vec<Cow<'a, HgPath>>,
661 pub removed: Vec<Cow<'a, HgPath>>,
661 pub removed: Vec<Cow<'a, HgPath>>,
662 pub deleted: Vec<Cow<'a, HgPath>>,
662 pub deleted: Vec<Cow<'a, HgPath>>,
663 pub clean: Vec<Cow<'a, HgPath>>,
663 pub clean: Vec<Cow<'a, HgPath>>,
664 pub ignored: Vec<Cow<'a, HgPath>>,
664 pub ignored: Vec<Cow<'a, HgPath>>,
665 pub unknown: Vec<Cow<'a, HgPath>>,
665 pub unknown: Vec<Cow<'a, HgPath>>,
666 pub bad: Vec<(Cow<'a, HgPath>, BadMatch)>,
666 pub bad: Vec<(Cow<'a, HgPath>, BadMatch)>,
667 /// Only filled if `collect_traversed_dirs` is `true`
667 /// Only filled if `collect_traversed_dirs` is `true`
668 pub traversed: Vec<HgPathBuf>,
668 pub traversed: Vec<HgPathBuf>,
669 }
669 }
670
670
671 #[timed]
671 #[timed]
672 fn build_response<'a>(
672 fn build_response<'a>(
673 results: impl IntoIterator<Item = (Cow<'a, HgPath>, Dispatch)>,
673 results: impl IntoIterator<Item = (Cow<'a, HgPath>, Dispatch)>,
674 traversed: Vec<HgPathBuf>,
674 traversed: Vec<HgPathBuf>,
675 ) -> (Vec<Cow<'a, HgPath>>, DirstateStatus<'a>) {
675 ) -> (Vec<Cow<'a, HgPath>>, DirstateStatus<'a>) {
676 let mut lookup = vec![];
676 let mut lookup = vec![];
677 let mut modified = vec![];
677 let mut modified = vec![];
678 let mut added = vec![];
678 let mut added = vec![];
679 let mut removed = vec![];
679 let mut removed = vec![];
680 let mut deleted = vec![];
680 let mut deleted = vec![];
681 let mut clean = vec![];
681 let mut clean = vec![];
682 let mut ignored = vec![];
682 let mut ignored = vec![];
683 let mut unknown = vec![];
683 let mut unknown = vec![];
684 let mut bad = vec![];
684 let mut bad = vec![];
685
685
686 for (filename, dispatch) in results.into_iter() {
686 for (filename, dispatch) in results.into_iter() {
687 match dispatch {
687 match dispatch {
688 Dispatch::Unknown => unknown.push(filename),
688 Dispatch::Unknown => unknown.push(filename),
689 Dispatch::Unsure => lookup.push(filename),
689 Dispatch::Unsure => lookup.push(filename),
690 Dispatch::Modified => modified.push(filename),
690 Dispatch::Modified => modified.push(filename),
691 Dispatch::Added => added.push(filename),
691 Dispatch::Added => added.push(filename),
692 Dispatch::Removed => removed.push(filename),
692 Dispatch::Removed => removed.push(filename),
693 Dispatch::Deleted => deleted.push(filename),
693 Dispatch::Deleted => deleted.push(filename),
694 Dispatch::Clean => clean.push(filename),
694 Dispatch::Clean => clean.push(filename),
695 Dispatch::Ignored => ignored.push(filename),
695 Dispatch::Ignored => ignored.push(filename),
696 Dispatch::None => {}
696 Dispatch::None => {}
697 Dispatch::Bad(reason) => bad.push((filename, reason)),
697 Dispatch::Bad(reason) => bad.push((filename, reason)),
698 Dispatch::Directory { .. } => {}
698 Dispatch::Directory { .. } => {}
699 }
699 }
700 }
700 }
701
701
702 (
702 (
703 lookup,
703 lookup,
704 DirstateStatus {
704 DirstateStatus {
705 modified,
705 modified,
706 added,
706 added,
707 removed,
707 removed,
708 deleted,
708 deleted,
709 clean,
709 clean,
710 ignored,
710 ignored,
711 unknown,
711 unknown,
712 bad,
712 bad,
713 traversed,
713 traversed,
714 },
714 },
715 )
715 )
716 }
716 }
717
717
718 #[derive(Debug)]
718 #[derive(Debug)]
719 pub enum StatusError {
719 pub enum StatusError {
720 IO(std::io::Error),
720 IO(std::io::Error),
721 Path(HgPathError),
721 Path(HgPathError),
722 Pattern(PatternError),
722 Pattern(PatternError),
723 }
723 }
724
724
725 pub type StatusResult<T> = Result<T, StatusError>;
725 pub type StatusResult<T> = Result<T, StatusError>;
726
726
727 impl From<PatternError> for StatusError {
727 impl From<PatternError> for StatusError {
728 fn from(e: PatternError) -> Self {
728 fn from(e: PatternError) -> Self {
729 StatusError::Pattern(e)
729 StatusError::Pattern(e)
730 }
730 }
731 }
731 }
732 impl From<HgPathError> for StatusError {
732 impl From<HgPathError> for StatusError {
733 fn from(e: HgPathError) -> Self {
733 fn from(e: HgPathError) -> Self {
734 StatusError::Path(e)
734 StatusError::Path(e)
735 }
735 }
736 }
736 }
737 impl From<std::io::Error> for StatusError {
737 impl From<std::io::Error> for StatusError {
738 fn from(e: std::io::Error) -> Self {
738 fn from(e: std::io::Error) -> Self {
739 StatusError::IO(e)
739 StatusError::IO(e)
740 }
740 }
741 }
741 }
742
742
743 impl ToString for StatusError {
743 impl ToString for StatusError {
744 fn to_string(&self) -> String {
744 fn to_string(&self) -> String {
745 match self {
745 match self {
746 StatusError::IO(e) => e.to_string(),
746 StatusError::IO(e) => e.to_string(),
747 StatusError::Path(e) => e.to_string(),
747 StatusError::Path(e) => e.to_string(),
748 StatusError::Pattern(e) => e.to_string(),
748 StatusError::Pattern(e) => e.to_string(),
749 }
749 }
750 }
750 }
751 }
751 }
752
752
753 /// 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`
754 /// in timings
754 /// in timings
755 #[timed]
755 #[timed]
756 fn handle_unknowns<'a>(
756 fn handle_unknowns<'a>(
757 dmap: &'a DirstateMap,
757 dmap: &'a DirstateMap,
758 matcher: &(impl Matcher + Sync),
758 matcher: &(impl Matcher + Sync),
759 root_dir: impl AsRef<Path> + Sync + Send + Copy,
759 root_dir: impl AsRef<Path> + Sync + Send + Copy,
760 options: StatusOptions,
760 options: StatusOptions,
761 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
761 results: &mut Vec<(Cow<'a, HgPath>, Dispatch)>,
762 ) -> IoResult<()> {
762 ) -> IoResult<()> {
763 let to_visit: Vec<(&HgPath, &DirstateEntry)> = if results.is_empty()
763 let to_visit: Vec<(&HgPath, &DirstateEntry)> = if results.is_empty()
764 && matcher.matches_everything()
764 && matcher.matches_everything()
765 {
765 {
766 dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
766 dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
767 } else {
767 } else {
768 // Only convert to a hashmap if needed.
768 // Only convert to a hashmap if needed.
769 let old_results: FastHashMap<_, _> = results.iter().cloned().collect();
769 let old_results: FastHashMap<_, _> = results.iter().cloned().collect();
770 dmap.iter()
770 dmap.iter()
771 .filter_map(move |(f, e)| {
771 .filter_map(move |(f, e)| {
772 if !old_results.contains_key(f.deref()) && matcher.matches(f) {
772 if !old_results.contains_key(f.deref()) && matcher.matches(f) {
773 Some((f.deref(), e))
773 Some((f.deref(), e))
774 } else {
774 } else {
775 None
775 None
776 }
776 }
777 })
777 })
778 .collect()
778 .collect()
779 };
779 };
780
780
781 // 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
782 // everything that matched was stat'ed and is already in results.
782 // everything that matched was stat'ed and is already in results.
783 // The rest must thus be ignored or under a symlink.
783 // The rest must thus be ignored or under a symlink.
784 let path_auditor = PathAuditor::new(root_dir);
784 let path_auditor = PathAuditor::new(root_dir);
785
785
786 // 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
787 // `itertools::process_results`, but for `rayon::ParallelIterator`
787 // `itertools::process_results`, but for `rayon::ParallelIterator`
788 let new_results: IoResult<Vec<_>> = to_visit
788 let new_results: IoResult<Vec<_>> = to_visit
789 .into_par_iter()
789 .into_par_iter()
790 .filter_map(|(filename, entry)| -> Option<IoResult<_>> {
790 .filter_map(|(filename, entry)| -> Option<IoResult<_>> {
791 // 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
792 // under a symlink directory.
792 // under a symlink directory.
793 if path_auditor.check(filename) {
793 if path_auditor.check(filename) {
794 // TODO normalize for case-insensitive filesystems
794 // TODO normalize for case-insensitive filesystems
795 let buf = match hg_path_to_path_buf(filename) {
795 let buf = match hg_path_to_path_buf(filename) {
796 Ok(x) => x,
796 Ok(x) => x,
797 Err(e) => return Some(Err(e.into())),
797 Err(e) => return Some(Err(e.into())),
798 };
798 };
799 Some(Ok((
799 Some(Ok((
800 Cow::Borrowed(filename),
800 Cow::Borrowed(filename),
801 match root_dir.as_ref().join(&buf).symlink_metadata() {
801 match root_dir.as_ref().join(&buf).symlink_metadata() {
802 // File was just ignored, no links, and exists
802 // File was just ignored, no links, and exists
803 Ok(meta) => {
803 Ok(meta) => {
804 let metadata = HgMetadata::from_metadata(meta);
804 let metadata = HgMetadata::from_metadata(meta);
805 dispatch_found(
805 dispatch_found(
806 filename,
806 filename,
807 *entry,
807 *entry,
808 metadata,
808 metadata,
809 &dmap.copy_map,
809 &dmap.copy_map,
810 options,
810 options,
811 )
811 )
812 }
812 }
813 // File doesn't exist
813 // File doesn't exist
814 Err(_) => dispatch_missing(entry.state),
814 Err(_) => dispatch_missing(entry.state),
815 },
815 },
816 )))
816 )))
817 } else {
817 } else {
818 // It's either missing or under a symlink directory which
818 // It's either missing or under a symlink directory which
819 // we, in this case, report as missing.
819 // we, in this case, report as missing.
820 Some(Ok((
820 Some(Ok((
821 Cow::Borrowed(filename),
821 Cow::Borrowed(filename),
822 dispatch_missing(entry.state),
822 dispatch_missing(entry.state),
823 )))
823 )))
824 }
824 }
825 })
825 })
826 .collect();
826 .collect();
827
827
828 results.par_extend(new_results?);
828 results.par_extend(new_results?);
829
829
830 Ok(())
830 Ok(())
831 }
831 }
832
832
833 /// Get the status of files in the working directory.
833 /// Get the status of files in the working directory.
834 ///
834 ///
835 /// 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
836 /// 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
837 /// information that will not be necessary in the future.
837 /// information that will not be necessary in the future.
838 #[timed]
838 #[timed]
839 pub fn status<'a: 'c, 'b: 'c, 'c>(
839 pub fn status<'a: 'c, 'b: 'c, 'c>(
840 dmap: &'a DirstateMap,
840 dmap: &'a DirstateMap,
841 matcher: &'b (impl Matcher + Sync),
841 matcher: &'b (impl Matcher + Sync),
842 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'c,
842 root_dir: impl AsRef<Path> + Sync + Send + Copy + 'c,
843 ignore_files: Vec<PathBuf>,
843 ignore_files: Vec<PathBuf>,
844 options: StatusOptions,
844 options: StatusOptions,
845 ) -> StatusResult<(
845 ) -> StatusResult<(
846 (Vec<Cow<'c, HgPath>>, DirstateStatus<'c>),
846 (Vec<Cow<'c, HgPath>>, DirstateStatus<'c>),
847 Vec<PatternFileWarning>,
847 Vec<PatternFileWarning>,
848 )> {
848 )> {
849 // Needs to outlive `dir_ignore_fn` since it's captured.
849 // Needs to outlive `dir_ignore_fn` since it's captured.
850 let ignore_fn: IgnoreFnType;
850 let ignore_fn: IgnoreFnType;
851
851
852 // 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.
853 let (dir_ignore_fn, warnings): (IgnoreFnType, _) = if options.list_ignored
853 let (dir_ignore_fn, warnings): (IgnoreFnType, _) = if options.list_ignored
854 || options.list_unknown
854 || options.list_unknown
855 {
855 {
856 let (ignore, warnings) = get_ignore_function(ignore_files, root_dir)?;
856 let (ignore, warnings) = get_ignore_function(ignore_files, root_dir)?;
857
857
858 ignore_fn = ignore;
858 ignore_fn = ignore;
859 let dir_ignore_fn = Box::new(|dir: &_| {
859 let dir_ignore_fn = Box::new(|dir: &_| {
860 // Is the path or one of its ancestors ignored?
860 // Is the path or one of its ancestors ignored?
861 if ignore_fn(dir) {
861 if ignore_fn(dir) {
862 true
862 true
863 } else {
863 } else {
864 for p in find_dirs(dir) {
864 for p in find_dirs(dir) {
865 if ignore_fn(p) {
865 if ignore_fn(p) {
866 return true;
866 return true;
867 }
867 }
868 }
868 }
869 false
869 false
870 }
870 }
871 });
871 });
872 (dir_ignore_fn, warnings)
872 (dir_ignore_fn, warnings)
873 } else {
873 } else {
874 ignore_fn = Box::new(|&_| true);
874 ignore_fn = Box::new(|&_| true);
875 (Box::new(|&_| true), vec![])
875 (Box::new(|&_| true), vec![])
876 };
876 };
877
877
878 let files = matcher.file_set();
878 let files = matcher.file_set();
879
879
880 // `crossbeam::Sender` is `Send`, while `mpsc::Sender` is not.
880 // `crossbeam::Sender` is `Sync`, while `mpsc::Sender` is not.
881 let (traversed_sender, traversed_recv) = crossbeam::channel::unbounded();
881 let (traversed_sender, traversed_recv) = crossbeam::channel::unbounded();
882
882
883 // Step 1: check the files explicitly mentioned by the user
883 // Step 1: check the files explicitly mentioned by the user
884 let explicit = walk_explicit(
884 let explicit = walk_explicit(
885 files,
885 files,
886 &dmap,
886 &dmap,
887 root_dir,
887 root_dir,
888 options,
888 options,
889 traversed_sender.clone(),
889 traversed_sender.clone(),
890 );
890 );
891
891
892 // 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
893 // cases.
893 // cases.
894 let (work, mut results): (Vec<_>, Vec<_>) = explicit
894 let (work, mut results): (Vec<_>, Vec<_>) = explicit
895 .filter_map(Result::ok)
895 .filter_map(Result::ok)
896 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch))
896 .map(|(filename, dispatch)| (Cow::Borrowed(filename), dispatch))
897 .partition(|(_, dispatch)| match dispatch {
897 .partition(|(_, dispatch)| match dispatch {
898 Dispatch::Directory { .. } => true,
898 Dispatch::Directory { .. } => true,
899 _ => false,
899 _ => false,
900 });
900 });
901
901
902 if !work.is_empty() {
902 if !work.is_empty() {
903 // 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
904 // if needed.
904 // if needed.
905 let old_results = results.iter().cloned().collect();
905 let old_results = results.iter().cloned().collect();
906
906
907 // Step 2: recursively check the working directory for changes if
907 // Step 2: recursively check the working directory for changes if
908 // needed
908 // needed
909 for (dir, dispatch) in work {
909 for (dir, dispatch) in work {
910 match dispatch {
910 match dispatch {
911 Dispatch::Directory { was_file } => {
911 Dispatch::Directory { was_file } => {
912 if was_file {
912 if was_file {
913 results.push((dir.to_owned(), Dispatch::Removed));
913 results.push((dir.to_owned(), Dispatch::Removed));
914 }
914 }
915 if options.list_ignored
915 if options.list_ignored
916 || options.list_unknown && !dir_ignore_fn(&dir)
916 || options.list_unknown && !dir_ignore_fn(&dir)
917 {
917 {
918 traverse(
918 traverse(
919 matcher,
919 matcher,
920 root_dir,
920 root_dir,
921 &dmap,
921 &dmap,
922 &dir,
922 &dir,
923 &old_results,
923 &old_results,
924 &ignore_fn,
924 &ignore_fn,
925 &dir_ignore_fn,
925 &dir_ignore_fn,
926 options,
926 options,
927 &mut results,
927 &mut results,
928 traversed_sender.clone(),
928 traversed_sender.clone(),
929 )?;
929 )?;
930 }
930 }
931 }
931 }
932 _ => unreachable!("There can only be directories in `work`"),
932 _ => unreachable!("There can only be directories in `work`"),
933 }
933 }
934 }
934 }
935 }
935 }
936
936
937 if !matcher.is_exact() {
937 if !matcher.is_exact() {
938 // Step 3: Check the remaining files from the dmap.
938 // Step 3: Check the remaining files from the dmap.
939 // 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
940 // a) not matched b) ignored, c) missing, or d) under a
940 // a) not matched b) ignored, c) missing, or d) under a
941 // symlink directory.
941 // symlink directory.
942
942
943 if options.list_unknown {
943 if options.list_unknown {
944 handle_unknowns(dmap, matcher, root_dir, options, &mut results)?;
944 handle_unknowns(dmap, matcher, root_dir, options, &mut results)?;
945 } else {
945 } else {
946 // 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
947 // and check everything we missed.
947 // and check everything we missed.
948 extend_from_dmap(&dmap, root_dir, options, &mut results);
948 extend_from_dmap(&dmap, root_dir, options, &mut results);
949 }
949 }
950 }
950 }
951
951
952 // Close the channel
952 // Close the channel
953 drop(traversed_sender);
953 drop(traversed_sender);
954 let traversed_dirs = traversed_recv.into_iter().collect();
954 let traversed_dirs = traversed_recv.into_iter().collect();
955
955
956 Ok((build_response(results, traversed_dirs), warnings))
956 Ok((build_response(results, traversed_dirs), warnings))
957 }
957 }
General Comments 0
You need to be logged in to leave comments. Login now