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