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