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