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