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