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