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