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