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