##// END OF EJS Templates
rust-status: save new dircache even if just invalidated...
Raphaël Gomès -
r50450:8ee3889b stable
parent child Browse files
Show More
@@ -1,903 +1,913 b''
1 1 use crate::dirstate::entry::TruncatedTimestamp;
2 2 use crate::dirstate::status::IgnoreFnType;
3 3 use crate::dirstate::status::StatusPath;
4 4 use crate::dirstate_tree::dirstate_map::BorrowedPath;
5 5 use crate::dirstate_tree::dirstate_map::ChildNodesRef;
6 6 use crate::dirstate_tree::dirstate_map::DirstateMap;
7 7 use crate::dirstate_tree::dirstate_map::DirstateVersion;
8 8 use crate::dirstate_tree::dirstate_map::NodeRef;
9 9 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
10 10 use crate::matchers::get_ignore_function;
11 11 use crate::matchers::Matcher;
12 12 use crate::utils::files::get_bytes_from_os_string;
13 13 use crate::utils::files::get_path_from_bytes;
14 14 use crate::utils::hg_path::HgPath;
15 15 use crate::BadMatch;
16 16 use crate::DirstateStatus;
17 17 use crate::HgPathBuf;
18 18 use crate::HgPathCow;
19 19 use crate::PatternFileWarning;
20 20 use crate::StatusError;
21 21 use crate::StatusOptions;
22 22 use micro_timer::timed;
23 23 use once_cell::sync::OnceCell;
24 24 use rayon::prelude::*;
25 25 use sha1::{Digest, Sha1};
26 26 use std::borrow::Cow;
27 27 use std::io;
28 28 use std::path::Path;
29 29 use std::path::PathBuf;
30 30 use std::sync::Mutex;
31 31 use std::time::SystemTime;
32 32
33 33 /// Returns the status of the working directory compared to its parent
34 34 /// changeset.
35 35 ///
36 36 /// This algorithm is based on traversing the filesystem tree (`fs` in function
37 37 /// and variable names) and dirstate tree at the same time. The core of this
38 38 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
39 39 /// and its use of `itertools::merge_join_by`. When reaching a path that only
40 40 /// exists in one of the two trees, depending on information requested by
41 41 /// `options` we may need to traverse the remaining subtree.
42 42 #[timed]
43 43 pub fn status<'dirstate>(
44 44 dmap: &'dirstate mut DirstateMap,
45 45 matcher: &(dyn Matcher + Sync),
46 46 root_dir: PathBuf,
47 47 ignore_files: Vec<PathBuf>,
48 48 options: StatusOptions,
49 49 ) -> Result<(DirstateStatus<'dirstate>, Vec<PatternFileWarning>), StatusError>
50 50 {
51 51 // Force the global rayon threadpool to not exceed 16 concurrent threads.
52 52 // This is a stop-gap measure until we figure out why using more than 16
53 53 // threads makes `status` slower for each additional thread.
54 54 // We use `ok()` in case the global threadpool has already been
55 55 // instantiated in `rhg` or some other caller.
56 56 // TODO find the underlying cause and fix it, then remove this.
57 57 rayon::ThreadPoolBuilder::new()
58 58 .num_threads(16)
59 59 .build_global()
60 60 .ok();
61 61
62 62 let (ignore_fn, warnings, patterns_changed): (IgnoreFnType, _, _) =
63 63 if options.list_ignored || options.list_unknown {
64 64 let (ignore_fn, warnings, changed) = match dmap.dirstate_version {
65 65 DirstateVersion::V1 => {
66 66 let (ignore_fn, warnings) = get_ignore_function(
67 67 ignore_files,
68 68 &root_dir,
69 69 &mut |_pattern_bytes| {},
70 70 )?;
71 71 (ignore_fn, warnings, None)
72 72 }
73 73 DirstateVersion::V2 => {
74 74 let mut hasher = Sha1::new();
75 75 let (ignore_fn, warnings) = get_ignore_function(
76 76 ignore_files,
77 77 &root_dir,
78 78 &mut |pattern_bytes| hasher.update(pattern_bytes),
79 79 )?;
80 80 let new_hash = *hasher.finalize().as_ref();
81 81 let changed = new_hash != dmap.ignore_patterns_hash;
82 82 dmap.ignore_patterns_hash = new_hash;
83 83 (ignore_fn, warnings, Some(changed))
84 84 }
85 85 };
86 86 (ignore_fn, warnings, changed)
87 87 } else {
88 88 (Box::new(|&_| true), vec![], None)
89 89 };
90 90
91 91 let filesystem_time_at_status_start =
92 92 filesystem_now(&root_dir).ok().map(TruncatedTimestamp::from);
93 93
94 94 // If the repository is under the current directory, prefer using a
95 95 // relative path, so the kernel needs to traverse fewer directory in every
96 96 // call to `read_dir` or `symlink_metadata`.
97 97 // This is effective in the common case where the current directory is the
98 98 // repository root.
99 99
100 100 // TODO: Better yet would be to use libc functions like `openat` and
101 101 // `fstatat` to remove such repeated traversals entirely, but the standard
102 102 // library does not provide APIs based on those.
103 103 // Maybe with a crate like https://crates.io/crates/openat instead?
104 104 let root_dir = if let Some(relative) = std::env::current_dir()
105 105 .ok()
106 106 .and_then(|cwd| root_dir.strip_prefix(cwd).ok())
107 107 {
108 108 relative
109 109 } else {
110 110 &root_dir
111 111 };
112 112
113 113 let outcome = DirstateStatus {
114 114 filesystem_time_at_status_start,
115 115 ..Default::default()
116 116 };
117 117 let common = StatusCommon {
118 118 dmap,
119 119 options,
120 120 matcher,
121 121 ignore_fn,
122 122 outcome: Mutex::new(outcome),
123 123 ignore_patterns_have_changed: patterns_changed,
124 124 new_cacheable_directories: Default::default(),
125 125 outdated_cached_directories: Default::default(),
126 126 filesystem_time_at_status_start,
127 127 };
128 128 let is_at_repo_root = true;
129 129 let hg_path = &BorrowedPath::OnDisk(HgPath::new(""));
130 130 let has_ignored_ancestor = HasIgnoredAncestor::create(None, hg_path);
131 131 let root_cached_mtime = None;
132 132 let root_dir_metadata = None;
133 133 // If the path we have for the repository root is a symlink, do follow it.
134 134 // (As opposed to symlinks within the working directory which are not
135 135 // followed, using `std::fs::symlink_metadata`.)
136 136 common.traverse_fs_directory_and_dirstate(
137 137 &has_ignored_ancestor,
138 138 dmap.root.as_ref(),
139 139 hg_path,
140 140 &root_dir,
141 141 root_dir_metadata,
142 142 root_cached_mtime,
143 143 is_at_repo_root,
144 144 )?;
145 145 let mut outcome = common.outcome.into_inner().unwrap();
146 146 let new_cacheable = common.new_cacheable_directories.into_inner().unwrap();
147 147 let outdated = common.outdated_cached_directories.into_inner().unwrap();
148 148
149 149 outcome.dirty = common.ignore_patterns_have_changed == Some(true)
150 150 || !outdated.is_empty()
151 151 || (!new_cacheable.is_empty()
152 152 && dmap.dirstate_version == DirstateVersion::V2);
153 153
154 154 // Remove outdated mtimes before adding new mtimes, in case a given
155 155 // directory is both
156 156 for path in &outdated {
157 157 dmap.clear_cached_mtime(path)?;
158 158 }
159 159 for (path, mtime) in &new_cacheable {
160 160 dmap.set_cached_mtime(path, *mtime)?;
161 161 }
162 162
163 163 Ok((outcome, warnings))
164 164 }
165 165
166 166 /// Bag of random things needed by various parts of the algorithm. Reduces the
167 167 /// number of parameters passed to functions.
168 168 struct StatusCommon<'a, 'tree, 'on_disk: 'tree> {
169 169 dmap: &'tree DirstateMap<'on_disk>,
170 170 options: StatusOptions,
171 171 matcher: &'a (dyn Matcher + Sync),
172 172 ignore_fn: IgnoreFnType<'a>,
173 173 outcome: Mutex<DirstateStatus<'on_disk>>,
174 174 /// New timestamps of directories to be used for caching their readdirs
175 175 new_cacheable_directories:
176 176 Mutex<Vec<(Cow<'on_disk, HgPath>, TruncatedTimestamp)>>,
177 177 /// Used to invalidate the readdir cache of directories
178 178 outdated_cached_directories: Mutex<Vec<Cow<'on_disk, HgPath>>>,
179 179
180 180 /// Whether ignore files like `.hgignore` have changed since the previous
181 181 /// time a `status()` call wrote their hash to the dirstate. `None` means
182 182 /// we don’t know as this run doesn’t list either ignored or uknown files
183 183 /// and therefore isn’t reading `.hgignore`.
184 184 ignore_patterns_have_changed: Option<bool>,
185 185
186 186 /// The current time at the start of the `status()` algorithm, as measured
187 187 /// and possibly truncated by the filesystem.
188 188 filesystem_time_at_status_start: Option<TruncatedTimestamp>,
189 189 }
190 190
191 191 enum Outcome {
192 192 Modified,
193 193 Added,
194 194 Removed,
195 195 Deleted,
196 196 Clean,
197 197 Ignored,
198 198 Unknown,
199 199 Unsure,
200 200 }
201 201
202 202 /// Lazy computation of whether a given path has a hgignored
203 203 /// ancestor.
204 204 struct HasIgnoredAncestor<'a> {
205 205 /// `path` and `parent` constitute the inputs to the computation,
206 206 /// `cache` stores the outcome.
207 207 path: &'a HgPath,
208 208 parent: Option<&'a HasIgnoredAncestor<'a>>,
209 209 cache: OnceCell<bool>,
210 210 }
211 211
212 212 impl<'a> HasIgnoredAncestor<'a> {
213 213 fn create(
214 214 parent: Option<&'a HasIgnoredAncestor<'a>>,
215 215 path: &'a HgPath,
216 216 ) -> HasIgnoredAncestor<'a> {
217 217 Self {
218 218 path,
219 219 parent,
220 220 cache: OnceCell::new(),
221 221 }
222 222 }
223 223
224 224 fn force<'b>(&self, ignore_fn: &IgnoreFnType<'b>) -> bool {
225 225 match self.parent {
226 226 None => false,
227 227 Some(parent) => {
228 228 *(parent.cache.get_or_init(|| {
229 229 parent.force(ignore_fn) || ignore_fn(&self.path)
230 230 }))
231 231 }
232 232 }
233 233 }
234 234 }
235 235
236 236 impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> {
237 237 fn push_outcome(
238 238 &self,
239 239 which: Outcome,
240 240 dirstate_node: &NodeRef<'tree, 'on_disk>,
241 241 ) -> Result<(), DirstateV2ParseError> {
242 242 let path = dirstate_node
243 243 .full_path_borrowed(self.dmap.on_disk)?
244 244 .detach_from_tree();
245 245 let copy_source = if self.options.list_copies {
246 246 dirstate_node
247 247 .copy_source_borrowed(self.dmap.on_disk)?
248 248 .map(|source| source.detach_from_tree())
249 249 } else {
250 250 None
251 251 };
252 252 self.push_outcome_common(which, path, copy_source);
253 253 Ok(())
254 254 }
255 255
256 256 fn push_outcome_without_copy_source(
257 257 &self,
258 258 which: Outcome,
259 259 path: &BorrowedPath<'_, 'on_disk>,
260 260 ) {
261 261 self.push_outcome_common(which, path.detach_from_tree(), None)
262 262 }
263 263
264 264 fn push_outcome_common(
265 265 &self,
266 266 which: Outcome,
267 267 path: HgPathCow<'on_disk>,
268 268 copy_source: Option<HgPathCow<'on_disk>>,
269 269 ) {
270 270 let mut outcome = self.outcome.lock().unwrap();
271 271 let vec = match which {
272 272 Outcome::Modified => &mut outcome.modified,
273 273 Outcome::Added => &mut outcome.added,
274 274 Outcome::Removed => &mut outcome.removed,
275 275 Outcome::Deleted => &mut outcome.deleted,
276 276 Outcome::Clean => &mut outcome.clean,
277 277 Outcome::Ignored => &mut outcome.ignored,
278 278 Outcome::Unknown => &mut outcome.unknown,
279 279 Outcome::Unsure => &mut outcome.unsure,
280 280 };
281 281 vec.push(StatusPath { path, copy_source });
282 282 }
283 283
284 284 fn read_dir(
285 285 &self,
286 286 hg_path: &HgPath,
287 287 fs_path: &Path,
288 288 is_at_repo_root: bool,
289 289 ) -> Result<Vec<DirEntry>, ()> {
290 290 DirEntry::read_dir(fs_path, is_at_repo_root)
291 291 .map_err(|error| self.io_error(error, hg_path))
292 292 }
293 293
294 294 fn io_error(&self, error: std::io::Error, hg_path: &HgPath) {
295 295 let errno = error.raw_os_error().expect("expected real OS error");
296 296 self.outcome
297 297 .lock()
298 298 .unwrap()
299 299 .bad
300 300 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
301 301 }
302 302
303 303 fn check_for_outdated_directory_cache(
304 304 &self,
305 305 dirstate_node: &NodeRef<'tree, 'on_disk>,
306 ) -> Result<(), DirstateV2ParseError> {
306 ) -> Result<bool, DirstateV2ParseError> {
307 307 if self.ignore_patterns_have_changed == Some(true)
308 308 && dirstate_node.cached_directory_mtime()?.is_some()
309 309 {
310 310 self.outdated_cached_directories.lock().unwrap().push(
311 311 dirstate_node
312 312 .full_path_borrowed(self.dmap.on_disk)?
313 313 .detach_from_tree(),
314 )
314 );
315 return Ok(true);
315 316 }
316 Ok(())
317 Ok(false)
317 318 }
318 319
319 320 /// If this returns true, we can get accurate results by only using
320 321 /// `symlink_metadata` for child nodes that exist in the dirstate and don’t
321 322 /// need to call `read_dir`.
322 323 fn can_skip_fs_readdir(
323 324 &self,
324 325 directory_metadata: Option<&std::fs::Metadata>,
325 326 cached_directory_mtime: Option<TruncatedTimestamp>,
326 327 ) -> bool {
327 328 if !self.options.list_unknown && !self.options.list_ignored {
328 329 // All states that we care about listing have corresponding
329 330 // dirstate entries.
330 331 // This happens for example with `hg status -mard`.
331 332 return true;
332 333 }
333 334 if !self.options.list_ignored
334 335 && self.ignore_patterns_have_changed == Some(false)
335 336 {
336 337 if let Some(cached_mtime) = cached_directory_mtime {
337 338 // The dirstate contains a cached mtime for this directory, set
338 339 // by a previous run of the `status` algorithm which found this
339 340 // directory eligible for `read_dir` caching.
340 341 if let Some(meta) = directory_metadata {
341 342 if cached_mtime
342 343 .likely_equal_to_mtime_of(meta)
343 344 .unwrap_or(false)
344 345 {
345 346 // The mtime of that directory has not changed
346 347 // since then, which means that the results of
347 348 // `read_dir` should also be unchanged.
348 349 return true;
349 350 }
350 351 }
351 352 }
352 353 }
353 354 false
354 355 }
355 356
356 357 /// Returns whether all child entries of the filesystem directory have a
357 358 /// corresponding dirstate node or are ignored.
358 359 fn traverse_fs_directory_and_dirstate<'ancestor>(
359 360 &self,
360 361 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>,
361 362 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>,
362 363 directory_hg_path: &BorrowedPath<'tree, 'on_disk>,
363 364 directory_fs_path: &Path,
364 365 directory_metadata: Option<&std::fs::Metadata>,
365 366 cached_directory_mtime: Option<TruncatedTimestamp>,
366 367 is_at_repo_root: bool,
367 368 ) -> Result<bool, DirstateV2ParseError> {
368 369 if self.can_skip_fs_readdir(directory_metadata, cached_directory_mtime)
369 370 {
370 371 dirstate_nodes
371 372 .par_iter()
372 373 .map(|dirstate_node| {
373 374 let fs_path = directory_fs_path.join(get_path_from_bytes(
374 375 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(),
375 376 ));
376 377 match std::fs::symlink_metadata(&fs_path) {
377 378 Ok(fs_metadata) => self.traverse_fs_and_dirstate(
378 379 &fs_path,
379 380 &fs_metadata,
380 381 dirstate_node,
381 382 has_ignored_ancestor,
382 383 ),
383 384 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
384 385 self.traverse_dirstate_only(dirstate_node)
385 386 }
386 387 Err(error) => {
387 388 let hg_path =
388 389 dirstate_node.full_path(self.dmap.on_disk)?;
389 390 Ok(self.io_error(error, hg_path))
390 391 }
391 392 }
392 393 })
393 394 .collect::<Result<_, _>>()?;
394 395
395 396 // We don’t know, so conservatively say this isn’t the case
396 397 let children_all_have_dirstate_node_or_are_ignored = false;
397 398
398 399 return Ok(children_all_have_dirstate_node_or_are_ignored);
399 400 }
400 401
401 402 let mut fs_entries = if let Ok(entries) = self.read_dir(
402 403 directory_hg_path,
403 404 directory_fs_path,
404 405 is_at_repo_root,
405 406 ) {
406 407 entries
407 408 } else {
408 409 // Treat an unreadable directory (typically because of insufficient
409 410 // permissions) like an empty directory. `self.read_dir` has
410 411 // already called `self.io_error` so a warning will be emitted.
411 412 Vec::new()
412 413 };
413 414
414 415 // `merge_join_by` requires both its input iterators to be sorted:
415 416
416 417 let dirstate_nodes = dirstate_nodes.sorted();
417 418 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
418 419 // https://github.com/rust-lang/rust/issues/34162
419 420 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
420 421
421 422 // Propagate here any error that would happen inside the comparison
422 423 // callback below
423 424 for dirstate_node in &dirstate_nodes {
424 425 dirstate_node.base_name(self.dmap.on_disk)?;
425 426 }
426 427 itertools::merge_join_by(
427 428 dirstate_nodes,
428 429 &fs_entries,
429 430 |dirstate_node, fs_entry| {
430 431 // This `unwrap` never panics because we already propagated
431 432 // those errors above
432 433 dirstate_node
433 434 .base_name(self.dmap.on_disk)
434 435 .unwrap()
435 436 .cmp(&fs_entry.base_name)
436 437 },
437 438 )
438 439 .par_bridge()
439 440 .map(|pair| {
440 441 use itertools::EitherOrBoth::*;
441 442 let has_dirstate_node_or_is_ignored;
442 443 match pair {
443 444 Both(dirstate_node, fs_entry) => {
444 445 self.traverse_fs_and_dirstate(
445 446 &fs_entry.full_path,
446 447 &fs_entry.metadata,
447 448 dirstate_node,
448 449 has_ignored_ancestor,
449 450 )?;
450 451 has_dirstate_node_or_is_ignored = true
451 452 }
452 453 Left(dirstate_node) => {
453 454 self.traverse_dirstate_only(dirstate_node)?;
454 455 has_dirstate_node_or_is_ignored = true;
455 456 }
456 457 Right(fs_entry) => {
457 458 has_dirstate_node_or_is_ignored = self.traverse_fs_only(
458 459 has_ignored_ancestor.force(&self.ignore_fn),
459 460 directory_hg_path,
460 461 fs_entry,
461 462 )
462 463 }
463 464 }
464 465 Ok(has_dirstate_node_or_is_ignored)
465 466 })
466 467 .try_reduce(|| true, |a, b| Ok(a && b))
467 468 }
468 469
469 470 fn traverse_fs_and_dirstate<'ancestor>(
470 471 &self,
471 472 fs_path: &Path,
472 473 fs_metadata: &std::fs::Metadata,
473 474 dirstate_node: NodeRef<'tree, 'on_disk>,
474 475 has_ignored_ancestor: &'ancestor HasIgnoredAncestor<'ancestor>,
475 476 ) -> Result<(), DirstateV2ParseError> {
476 self.check_for_outdated_directory_cache(&dirstate_node)?;
477 let outdated_dircache =
478 self.check_for_outdated_directory_cache(&dirstate_node)?;
477 479 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
478 480 let file_type = fs_metadata.file_type();
479 481 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
480 482 if !file_or_symlink {
481 483 // If we previously had a file here, it was removed (with
482 484 // `hg rm` or similar) or deleted before it could be
483 485 // replaced by a directory or something else.
484 486 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
485 487 }
486 488 if file_type.is_dir() {
487 489 if self.options.collect_traversed_dirs {
488 490 self.outcome
489 491 .lock()
490 492 .unwrap()
491 493 .traversed
492 494 .push(hg_path.detach_from_tree())
493 495 }
494 496 let is_ignored = HasIgnoredAncestor::create(
495 497 Some(&has_ignored_ancestor),
496 498 hg_path,
497 499 );
498 500 let is_at_repo_root = false;
499 501 let children_all_have_dirstate_node_or_are_ignored = self
500 502 .traverse_fs_directory_and_dirstate(
501 503 &is_ignored,
502 504 dirstate_node.children(self.dmap.on_disk)?,
503 505 hg_path,
504 506 fs_path,
505 507 Some(fs_metadata),
506 508 dirstate_node.cached_directory_mtime()?,
507 509 is_at_repo_root,
508 510 )?;
509 511 self.maybe_save_directory_mtime(
510 512 children_all_have_dirstate_node_or_are_ignored,
511 513 fs_metadata,
512 514 dirstate_node,
515 outdated_dircache,
513 516 )?
514 517 } else {
515 518 if file_or_symlink && self.matcher.matches(&hg_path) {
516 519 if let Some(entry) = dirstate_node.entry()? {
517 520 if !entry.any_tracked() {
518 521 // Forward-compat if we start tracking unknown/ignored
519 522 // files for caching reasons
520 523 self.mark_unknown_or_ignored(
521 524 has_ignored_ancestor.force(&self.ignore_fn),
522 525 &hg_path,
523 526 );
524 527 }
525 528 if entry.added() {
526 529 self.push_outcome(Outcome::Added, &dirstate_node)?;
527 530 } else if entry.removed() {
528 531 self.push_outcome(Outcome::Removed, &dirstate_node)?;
529 532 } else if entry.modified() {
530 533 self.push_outcome(Outcome::Modified, &dirstate_node)?;
531 534 } else {
532 535 self.handle_normal_file(&dirstate_node, fs_metadata)?;
533 536 }
534 537 } else {
535 538 // `node.entry.is_none()` indicates a "directory"
536 539 // node, but the filesystem has a file
537 540 self.mark_unknown_or_ignored(
538 541 has_ignored_ancestor.force(&self.ignore_fn),
539 542 hg_path,
540 543 );
541 544 }
542 545 }
543 546
544 547 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter()
545 548 {
546 549 self.traverse_dirstate_only(child_node)?
547 550 }
548 551 }
549 552 Ok(())
550 553 }
551 554
555 /// Save directory mtime if applicable.
556 ///
557 /// `outdated_directory_cache` is `true` if we've just invalidated the
558 /// cache for this directory in `check_for_outdated_directory_cache`,
559 /// which forces the update.
552 560 fn maybe_save_directory_mtime(
553 561 &self,
554 562 children_all_have_dirstate_node_or_are_ignored: bool,
555 563 directory_metadata: &std::fs::Metadata,
556 564 dirstate_node: NodeRef<'tree, 'on_disk>,
565 outdated_directory_cache: bool,
557 566 ) -> Result<(), DirstateV2ParseError> {
558 567 if !children_all_have_dirstate_node_or_are_ignored {
559 568 return Ok(());
560 569 }
561 570 // All filesystem directory entries from `read_dir` have a
562 571 // corresponding node in the dirstate, so we can reconstitute the
563 572 // names of those entries without calling `read_dir` again.
564 573
565 574 // TODO: use let-else here and below when available:
566 575 // https://github.com/rust-lang/rust/issues/87335
567 576 let status_start = if let Some(status_start) =
568 577 &self.filesystem_time_at_status_start
569 578 {
570 579 status_start
571 580 } else {
572 581 return Ok(());
573 582 };
574 583
575 584 // Although the Rust standard library’s `SystemTime` type
576 585 // has nanosecond precision, the times reported for a
577 586 // directory’s (or file’s) modified time may have lower
578 587 // resolution based on the filesystem (for example ext3
579 588 // only stores integer seconds), kernel (see
580 589 // https://stackoverflow.com/a/14393315/1162888), etc.
581 590 let directory_mtime = if let Ok(option) =
582 591 TruncatedTimestamp::for_reliable_mtime_of(
583 592 directory_metadata,
584 593 status_start,
585 594 ) {
586 595 if let Some(directory_mtime) = option {
587 596 directory_mtime
588 597 } else {
589 598 // The directory was modified too recently,
590 599 // don’t cache its `read_dir` results.
591 600 //
592 601 // 1. A change to this directory (direct child was
593 602 // added or removed) cause its mtime to be set
594 603 // (possibly truncated) to `directory_mtime`
595 604 // 2. This `status` algorithm calls `read_dir`
596 605 // 3. An other change is made to the same directory is
597 606 // made so that calling `read_dir` agin would give
598 607 // different results, but soon enough after 1. that
599 608 // the mtime stays the same
600 609 //
601 610 // On a system where the time resolution poor, this
602 611 // scenario is not unlikely if all three steps are caused
603 612 // by the same script.
604 613 return Ok(());
605 614 }
606 615 } else {
607 616 // OS/libc does not support mtime?
608 617 return Ok(());
609 618 };
610 619 // We’ve observed (through `status_start`) that time has
611 620 // “progressed” since `directory_mtime`, so any further
612 621 // change to this directory is extremely likely to cause a
613 622 // different mtime.
614 623 //
615 624 // Having the same mtime again is not entirely impossible
616 625 // since the system clock is not monotonous. It could jump
617 626 // backward to some point before `directory_mtime`, then a
618 627 // directory change could potentially happen during exactly
619 628 // the wrong tick.
620 629 //
621 630 // We deem this scenario (unlike the previous one) to be
622 631 // unlikely enough in practice.
623 632
624 let is_up_to_date =
625 if let Some(cached) = dirstate_node.cached_directory_mtime()? {
626 cached.likely_equal(directory_mtime)
627 } else {
628 false
629 };
633 let is_up_to_date = if let Some(cached) =
634 dirstate_node.cached_directory_mtime()?
635 {
636 !outdated_directory_cache && cached.likely_equal(directory_mtime)
637 } else {
638 false
639 };
630 640 if !is_up_to_date {
631 641 let hg_path = dirstate_node
632 642 .full_path_borrowed(self.dmap.on_disk)?
633 643 .detach_from_tree();
634 644 self.new_cacheable_directories
635 645 .lock()
636 646 .unwrap()
637 647 .push((hg_path, directory_mtime))
638 648 }
639 649 Ok(())
640 650 }
641 651
642 652 /// A file that is clean in the dirstate was found in the filesystem
643 653 fn handle_normal_file(
644 654 &self,
645 655 dirstate_node: &NodeRef<'tree, 'on_disk>,
646 656 fs_metadata: &std::fs::Metadata,
647 657 ) -> Result<(), DirstateV2ParseError> {
648 658 // Keep the low 31 bits
649 659 fn truncate_u64(value: u64) -> i32 {
650 660 (value & 0x7FFF_FFFF) as i32
651 661 }
652 662
653 663 let entry = dirstate_node
654 664 .entry()?
655 665 .expect("handle_normal_file called with entry-less node");
656 666 let mode_changed =
657 667 || self.options.check_exec && entry.mode_changed(fs_metadata);
658 668 let size = entry.size();
659 669 let size_changed = size != truncate_u64(fs_metadata.len());
660 670 if size >= 0 && size_changed && fs_metadata.file_type().is_symlink() {
661 671 // issue6456: Size returned may be longer due to encryption
662 672 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
663 673 self.push_outcome(Outcome::Unsure, dirstate_node)?
664 674 } else if dirstate_node.has_copy_source()
665 675 || entry.is_from_other_parent()
666 676 || (size >= 0 && (size_changed || mode_changed()))
667 677 {
668 678 self.push_outcome(Outcome::Modified, dirstate_node)?
669 679 } else {
670 680 let mtime_looks_clean;
671 681 if let Some(dirstate_mtime) = entry.truncated_mtime() {
672 682 let fs_mtime = TruncatedTimestamp::for_mtime_of(fs_metadata)
673 683 .expect("OS/libc does not support mtime?");
674 684 // There might be a change in the future if for example the
675 685 // internal clock become off while process run, but this is a
676 686 // case where the issues the user would face
677 687 // would be a lot worse and there is nothing we
678 688 // can really do.
679 689 mtime_looks_clean = fs_mtime.likely_equal(dirstate_mtime)
680 690 } else {
681 691 // No mtime in the dirstate entry
682 692 mtime_looks_clean = false
683 693 };
684 694 if !mtime_looks_clean {
685 695 self.push_outcome(Outcome::Unsure, dirstate_node)?
686 696 } else if self.options.list_clean {
687 697 self.push_outcome(Outcome::Clean, dirstate_node)?
688 698 }
689 699 }
690 700 Ok(())
691 701 }
692 702
693 703 /// A node in the dirstate tree has no corresponding filesystem entry
694 704 fn traverse_dirstate_only(
695 705 &self,
696 706 dirstate_node: NodeRef<'tree, 'on_disk>,
697 707 ) -> Result<(), DirstateV2ParseError> {
698 708 self.check_for_outdated_directory_cache(&dirstate_node)?;
699 709 self.mark_removed_or_deleted_if_file(&dirstate_node)?;
700 710 dirstate_node
701 711 .children(self.dmap.on_disk)?
702 712 .par_iter()
703 713 .map(|child_node| self.traverse_dirstate_only(child_node))
704 714 .collect()
705 715 }
706 716
707 717 /// A node in the dirstate tree has no corresponding *file* on the
708 718 /// filesystem
709 719 ///
710 720 /// Does nothing on a "directory" node
711 721 fn mark_removed_or_deleted_if_file(
712 722 &self,
713 723 dirstate_node: &NodeRef<'tree, 'on_disk>,
714 724 ) -> Result<(), DirstateV2ParseError> {
715 725 if let Some(entry) = dirstate_node.entry()? {
716 726 if !entry.any_tracked() {
717 727 // Future-compat for when we start storing ignored and unknown
718 728 // files for caching reasons
719 729 return Ok(());
720 730 }
721 731 let path = dirstate_node.full_path(self.dmap.on_disk)?;
722 732 if self.matcher.matches(path) {
723 733 if entry.removed() {
724 734 self.push_outcome(Outcome::Removed, dirstate_node)?
725 735 } else {
726 736 self.push_outcome(Outcome::Deleted, &dirstate_node)?
727 737 }
728 738 }
729 739 }
730 740 Ok(())
731 741 }
732 742
733 743 /// Something in the filesystem has no corresponding dirstate node
734 744 ///
735 745 /// Returns whether that path is ignored
736 746 fn traverse_fs_only(
737 747 &self,
738 748 has_ignored_ancestor: bool,
739 749 directory_hg_path: &HgPath,
740 750 fs_entry: &DirEntry,
741 751 ) -> bool {
742 752 let hg_path = directory_hg_path.join(&fs_entry.base_name);
743 753 let file_type = fs_entry.metadata.file_type();
744 754 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
745 755 if file_type.is_dir() {
746 756 let is_ignored =
747 757 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
748 758 let traverse_children = if is_ignored {
749 759 // Descendants of an ignored directory are all ignored
750 760 self.options.list_ignored
751 761 } else {
752 762 // Descendants of an unknown directory may be either unknown or
753 763 // ignored
754 764 self.options.list_unknown || self.options.list_ignored
755 765 };
756 766 if traverse_children {
757 767 let is_at_repo_root = false;
758 768 if let Ok(children_fs_entries) = self.read_dir(
759 769 &hg_path,
760 770 &fs_entry.full_path,
761 771 is_at_repo_root,
762 772 ) {
763 773 children_fs_entries.par_iter().for_each(|child_fs_entry| {
764 774 self.traverse_fs_only(
765 775 is_ignored,
766 776 &hg_path,
767 777 child_fs_entry,
768 778 );
769 779 })
770 780 }
771 781 if self.options.collect_traversed_dirs {
772 782 self.outcome.lock().unwrap().traversed.push(hg_path.into())
773 783 }
774 784 }
775 785 is_ignored
776 786 } else {
777 787 if file_or_symlink {
778 788 if self.matcher.matches(&hg_path) {
779 789 self.mark_unknown_or_ignored(
780 790 has_ignored_ancestor,
781 791 &BorrowedPath::InMemory(&hg_path),
782 792 )
783 793 } else {
784 794 // We haven’t computed whether this path is ignored. It
785 795 // might not be, and a future run of status might have a
786 796 // different matcher that matches it. So treat it as not
787 797 // ignored. That is, inhibit readdir caching of the parent
788 798 // directory.
789 799 false
790 800 }
791 801 } else {
792 802 // This is neither a directory, a plain file, or a symlink.
793 803 // Treat it like an ignored file.
794 804 true
795 805 }
796 806 }
797 807 }
798 808
799 809 /// Returns whether that path is ignored
800 810 fn mark_unknown_or_ignored(
801 811 &self,
802 812 has_ignored_ancestor: bool,
803 813 hg_path: &BorrowedPath<'_, 'on_disk>,
804 814 ) -> bool {
805 815 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
806 816 if is_ignored {
807 817 if self.options.list_ignored {
808 818 self.push_outcome_without_copy_source(
809 819 Outcome::Ignored,
810 820 hg_path,
811 821 )
812 822 }
813 823 } else {
814 824 if self.options.list_unknown {
815 825 self.push_outcome_without_copy_source(
816 826 Outcome::Unknown,
817 827 hg_path,
818 828 )
819 829 }
820 830 }
821 831 is_ignored
822 832 }
823 833 }
824 834
825 835 struct DirEntry {
826 836 base_name: HgPathBuf,
827 837 full_path: PathBuf,
828 838 metadata: std::fs::Metadata,
829 839 }
830 840
831 841 impl DirEntry {
832 842 /// Returns **unsorted** entries in the given directory, with name and
833 843 /// metadata.
834 844 ///
835 845 /// If a `.hg` sub-directory is encountered:
836 846 ///
837 847 /// * At the repository root, ignore that sub-directory
838 848 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
839 849 /// list instead.
840 850 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
841 851 // `read_dir` returns a "not found" error for the empty path
842 852 let at_cwd = path == Path::new("");
843 853 let read_dir_path = if at_cwd { Path::new(".") } else { path };
844 854 let mut results = Vec::new();
845 855 for entry in read_dir_path.read_dir()? {
846 856 let entry = entry?;
847 857 let metadata = match entry.metadata() {
848 858 Ok(v) => v,
849 859 Err(e) => {
850 860 // race with file deletion?
851 861 if e.kind() == std::io::ErrorKind::NotFound {
852 862 continue;
853 863 } else {
854 864 return Err(e);
855 865 }
856 866 }
857 867 };
858 868 let file_name = entry.file_name();
859 869 // FIXME don't do this when cached
860 870 if file_name == ".hg" {
861 871 if is_at_repo_root {
862 872 // Skip the repo’s own .hg (might be a symlink)
863 873 continue;
864 874 } else if metadata.is_dir() {
865 875 // A .hg sub-directory at another location means a subrepo,
866 876 // skip it entirely.
867 877 return Ok(Vec::new());
868 878 }
869 879 }
870 880 let full_path = if at_cwd {
871 881 file_name.clone().into()
872 882 } else {
873 883 entry.path()
874 884 };
875 885 let base_name = get_bytes_from_os_string(file_name).into();
876 886 results.push(DirEntry {
877 887 base_name,
878 888 full_path,
879 889 metadata,
880 890 })
881 891 }
882 892 Ok(results)
883 893 }
884 894 }
885 895
886 896 /// Return the `mtime` of a temporary file newly-created in the `.hg` directory
887 897 /// of the give repository.
888 898 ///
889 899 /// This is similar to `SystemTime::now()`, with the result truncated to the
890 900 /// same time resolution as other files’ modification times. Using `.hg`
891 901 /// instead of the system’s default temporary directory (such as `/tmp`) makes
892 902 /// it more likely the temporary file is in the same disk partition as contents
893 903 /// of the working directory, which can matter since different filesystems may
894 904 /// store timestamps with different resolutions.
895 905 ///
896 906 /// This may fail, typically if we lack write permissions. In that case we
897 907 /// should continue the `status()` algoritm anyway and consider the current
898 908 /// date/time to be unknown.
899 909 fn filesystem_now(repo_root: &Path) -> Result<SystemTime, io::Error> {
900 910 tempfile::tempfile_in(repo_root.join(".hg"))?
901 911 .metadata()?
902 912 .modified()
903 913 }
@@ -1,1010 +1,1002 b''
1 1 #testcases dirstate-v1 dirstate-v2
2 2
3 3 #if dirstate-v2
4 4 $ cat >> $HGRCPATH << EOF
5 5 > [format]
6 6 > use-dirstate-v2=1
7 7 > [storage]
8 8 > dirstate-v2.slow-path=allow
9 9 > EOF
10 10 #endif
11 11
12 12 $ hg init repo1
13 13 $ cd repo1
14 14 $ mkdir a b a/1 b/1 b/2
15 15 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
16 16
17 17 hg status in repo root:
18 18
19 19 $ hg status
20 20 ? a/1/in_a_1
21 21 ? a/in_a
22 22 ? b/1/in_b_1
23 23 ? b/2/in_b_2
24 24 ? b/in_b
25 25 ? in_root
26 26
27 27 hg status . in repo root:
28 28
29 29 $ hg status .
30 30 ? a/1/in_a_1
31 31 ? a/in_a
32 32 ? b/1/in_b_1
33 33 ? b/2/in_b_2
34 34 ? b/in_b
35 35 ? in_root
36 36
37 37 $ hg status --cwd a
38 38 ? a/1/in_a_1
39 39 ? a/in_a
40 40 ? b/1/in_b_1
41 41 ? b/2/in_b_2
42 42 ? b/in_b
43 43 ? in_root
44 44 $ hg status --cwd a .
45 45 ? 1/in_a_1
46 46 ? in_a
47 47 $ hg status --cwd a ..
48 48 ? 1/in_a_1
49 49 ? in_a
50 50 ? ../b/1/in_b_1
51 51 ? ../b/2/in_b_2
52 52 ? ../b/in_b
53 53 ? ../in_root
54 54
55 55 $ hg status --cwd b
56 56 ? a/1/in_a_1
57 57 ? a/in_a
58 58 ? b/1/in_b_1
59 59 ? b/2/in_b_2
60 60 ? b/in_b
61 61 ? in_root
62 62 $ hg status --cwd b .
63 63 ? 1/in_b_1
64 64 ? 2/in_b_2
65 65 ? in_b
66 66 $ hg status --cwd b ..
67 67 ? ../a/1/in_a_1
68 68 ? ../a/in_a
69 69 ? 1/in_b_1
70 70 ? 2/in_b_2
71 71 ? in_b
72 72 ? ../in_root
73 73
74 74 $ hg status --cwd a/1
75 75 ? a/1/in_a_1
76 76 ? a/in_a
77 77 ? b/1/in_b_1
78 78 ? b/2/in_b_2
79 79 ? b/in_b
80 80 ? in_root
81 81 $ hg status --cwd a/1 .
82 82 ? in_a_1
83 83 $ hg status --cwd a/1 ..
84 84 ? in_a_1
85 85 ? ../in_a
86 86
87 87 $ hg status --cwd b/1
88 88 ? a/1/in_a_1
89 89 ? a/in_a
90 90 ? b/1/in_b_1
91 91 ? b/2/in_b_2
92 92 ? b/in_b
93 93 ? in_root
94 94 $ hg status --cwd b/1 .
95 95 ? in_b_1
96 96 $ hg status --cwd b/1 ..
97 97 ? in_b_1
98 98 ? ../2/in_b_2
99 99 ? ../in_b
100 100
101 101 $ hg status --cwd b/2
102 102 ? a/1/in_a_1
103 103 ? a/in_a
104 104 ? b/1/in_b_1
105 105 ? b/2/in_b_2
106 106 ? b/in_b
107 107 ? in_root
108 108 $ hg status --cwd b/2 .
109 109 ? in_b_2
110 110 $ hg status --cwd b/2 ..
111 111 ? ../1/in_b_1
112 112 ? in_b_2
113 113 ? ../in_b
114 114
115 115 combining patterns with root and patterns without a root works
116 116
117 117 $ hg st a/in_a re:.*b$
118 118 ? a/in_a
119 119 ? b/in_b
120 120
121 121 tweaking defaults works
122 122 $ hg status --cwd a --config ui.tweakdefaults=yes
123 123 ? 1/in_a_1
124 124 ? in_a
125 125 ? ../b/1/in_b_1
126 126 ? ../b/2/in_b_2
127 127 ? ../b/in_b
128 128 ? ../in_root
129 129 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
130 130 ? a/1/in_a_1 (glob)
131 131 ? a/in_a (glob)
132 132 ? b/1/in_b_1 (glob)
133 133 ? b/2/in_b_2 (glob)
134 134 ? b/in_b (glob)
135 135 ? in_root
136 136 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
137 137 ? 1/in_a_1
138 138 ? in_a
139 139 ? ../b/1/in_b_1
140 140 ? ../b/2/in_b_2
141 141 ? ../b/in_b
142 142 ? ../in_root (glob)
143 143
144 144 relative paths can be requested
145 145
146 146 $ hg status --cwd a --config ui.relative-paths=yes
147 147 ? 1/in_a_1
148 148 ? in_a
149 149 ? ../b/1/in_b_1
150 150 ? ../b/2/in_b_2
151 151 ? ../b/in_b
152 152 ? ../in_root
153 153
154 154 $ hg status --cwd a . --config ui.relative-paths=legacy
155 155 ? 1/in_a_1
156 156 ? in_a
157 157 $ hg status --cwd a . --config ui.relative-paths=no
158 158 ? a/1/in_a_1
159 159 ? a/in_a
160 160
161 161 commands.status.relative overrides ui.relative-paths
162 162
163 163 $ cat >> $HGRCPATH <<EOF
164 164 > [ui]
165 165 > relative-paths = False
166 166 > [commands]
167 167 > status.relative = True
168 168 > EOF
169 169 $ hg status --cwd a
170 170 ? 1/in_a_1
171 171 ? in_a
172 172 ? ../b/1/in_b_1
173 173 ? ../b/2/in_b_2
174 174 ? ../b/in_b
175 175 ? ../in_root
176 176 $ HGPLAIN=1 hg status --cwd a
177 177 ? a/1/in_a_1 (glob)
178 178 ? a/in_a (glob)
179 179 ? b/1/in_b_1 (glob)
180 180 ? b/2/in_b_2 (glob)
181 181 ? b/in_b (glob)
182 182 ? in_root
183 183
184 184 if relative paths are explicitly off, tweakdefaults doesn't change it
185 185 $ cat >> $HGRCPATH <<EOF
186 186 > [commands]
187 187 > status.relative = False
188 188 > EOF
189 189 $ hg status --cwd a --config ui.tweakdefaults=yes
190 190 ? a/1/in_a_1
191 191 ? a/in_a
192 192 ? b/1/in_b_1
193 193 ? b/2/in_b_2
194 194 ? b/in_b
195 195 ? in_root
196 196
197 197 $ cd ..
198 198
199 199 $ hg init repo2
200 200 $ cd repo2
201 201 $ touch modified removed deleted ignored
202 202 $ echo "^ignored$" > .hgignore
203 203 $ hg ci -A -m 'initial checkin'
204 204 adding .hgignore
205 205 adding deleted
206 206 adding modified
207 207 adding removed
208 208 $ touch modified added unknown ignored
209 209 $ hg add added
210 210 $ hg remove removed
211 211 $ rm deleted
212 212
213 213 hg status:
214 214
215 215 $ hg status
216 216 A added
217 217 R removed
218 218 ! deleted
219 219 ? unknown
220 220
221 221 hg status -n:
222 222 $ env RHG_ON_UNSUPPORTED=abort hg status -n
223 223 added
224 224 removed
225 225 deleted
226 226 unknown
227 227
228 228 hg status modified added removed deleted unknown never-existed ignored:
229 229
230 230 $ hg status modified added removed deleted unknown never-existed ignored
231 231 never-existed: * (glob)
232 232 A added
233 233 R removed
234 234 ! deleted
235 235 ? unknown
236 236
237 237 $ hg copy modified copied
238 238
239 239 hg status -C:
240 240
241 241 $ hg status -C
242 242 A added
243 243 A copied
244 244 modified
245 245 R removed
246 246 ! deleted
247 247 ? unknown
248 248
249 249 hg status -A:
250 250
251 251 $ hg status -A
252 252 A added
253 253 A copied
254 254 modified
255 255 R removed
256 256 ! deleted
257 257 ? unknown
258 258 I ignored
259 259 C .hgignore
260 260 C modified
261 261
262 262 $ hg status -A -T '{status} {path} {node|shortest}\n'
263 263 A added ffff
264 264 A copied ffff
265 265 R removed ffff
266 266 ! deleted ffff
267 267 ? unknown ffff
268 268 I ignored ffff
269 269 C .hgignore ffff
270 270 C modified ffff
271 271
272 272 $ hg status -A -Tjson
273 273 [
274 274 {
275 275 "itemtype": "file",
276 276 "path": "added",
277 277 "status": "A"
278 278 },
279 279 {
280 280 "itemtype": "file",
281 281 "path": "copied",
282 282 "source": "modified",
283 283 "status": "A"
284 284 },
285 285 {
286 286 "itemtype": "file",
287 287 "path": "removed",
288 288 "status": "R"
289 289 },
290 290 {
291 291 "itemtype": "file",
292 292 "path": "deleted",
293 293 "status": "!"
294 294 },
295 295 {
296 296 "itemtype": "file",
297 297 "path": "unknown",
298 298 "status": "?"
299 299 },
300 300 {
301 301 "itemtype": "file",
302 302 "path": "ignored",
303 303 "status": "I"
304 304 },
305 305 {
306 306 "itemtype": "file",
307 307 "path": ".hgignore",
308 308 "status": "C"
309 309 },
310 310 {
311 311 "itemtype": "file",
312 312 "path": "modified",
313 313 "status": "C"
314 314 }
315 315 ]
316 316
317 317 $ hg status -A -Tpickle > pickle
318 318 >>> import pickle
319 319 >>> from mercurial import util
320 320 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
321 321 >>> for s, p in data: print("%s %s" % (s, p))
322 322 ! deleted
323 323 ? pickle
324 324 ? unknown
325 325 A added
326 326 A copied
327 327 C .hgignore
328 328 C modified
329 329 I ignored
330 330 R removed
331 331 $ rm pickle
332 332
333 333 $ echo "^ignoreddir$" > .hgignore
334 334 $ mkdir ignoreddir
335 335 $ touch ignoreddir/file
336 336
337 337 Test templater support:
338 338
339 339 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
340 340 [M] .hgignore
341 341 [A] added
342 342 [A] modified -> copied
343 343 [R] removed
344 344 [!] deleted
345 345 [?] ignored
346 346 [?] unknown
347 347 [I] ignoreddir/file
348 348 [C] modified
349 349 $ hg status -AT default
350 350 M .hgignore
351 351 A added
352 352 A copied
353 353 modified
354 354 R removed
355 355 ! deleted
356 356 ? ignored
357 357 ? unknown
358 358 I ignoreddir/file
359 359 C modified
360 360 $ hg status -T compact
361 361 abort: "status" not in template map
362 362 [255]
363 363
364 364 hg status ignoreddir/file:
365 365
366 366 $ hg status ignoreddir/file
367 367
368 368 hg status -i ignoreddir/file:
369 369
370 370 $ hg status -i ignoreddir/file
371 371 I ignoreddir/file
372 372 $ cd ..
373 373
374 374 Check 'status -q' and some combinations
375 375
376 376 $ hg init repo3
377 377 $ cd repo3
378 378 $ touch modified removed deleted ignored
379 379 $ echo "^ignored$" > .hgignore
380 380 $ hg commit -A -m 'initial checkin'
381 381 adding .hgignore
382 382 adding deleted
383 383 adding modified
384 384 adding removed
385 385 $ touch added unknown ignored
386 386 $ hg add added
387 387 $ echo "test" >> modified
388 388 $ hg remove removed
389 389 $ rm deleted
390 390 $ hg copy modified copied
391 391
392 392 Specify working directory revision explicitly, that should be the same as
393 393 "hg status"
394 394
395 395 $ hg status --change "wdir()"
396 396 M modified
397 397 A added
398 398 A copied
399 399 R removed
400 400 ! deleted
401 401 ? unknown
402 402
403 403 Run status with 2 different flags.
404 404 Check if result is the same or different.
405 405 If result is not as expected, raise error
406 406
407 407 $ assert() {
408 408 > hg status $1 > ../a
409 409 > hg status $2 > ../b
410 410 > if diff ../a ../b > /dev/null; then
411 411 > out=0
412 412 > else
413 413 > out=1
414 414 > fi
415 415 > if [ $3 -eq 0 ]; then
416 416 > df="same"
417 417 > else
418 418 > df="different"
419 419 > fi
420 420 > if [ $out -ne $3 ]; then
421 421 > echo "Error on $1 and $2, should be $df."
422 422 > fi
423 423 > }
424 424
425 425 Assert flag1 flag2 [0-same | 1-different]
426 426
427 427 $ assert "-q" "-mard" 0
428 428 $ assert "-A" "-marduicC" 0
429 429 $ assert "-qA" "-mardcC" 0
430 430 $ assert "-qAui" "-A" 0
431 431 $ assert "-qAu" "-marducC" 0
432 432 $ assert "-qAi" "-mardicC" 0
433 433 $ assert "-qu" "-u" 0
434 434 $ assert "-q" "-u" 1
435 435 $ assert "-m" "-a" 1
436 436 $ assert "-r" "-d" 1
437 437 $ cd ..
438 438
439 439 $ hg init repo4
440 440 $ cd repo4
441 441 $ touch modified removed deleted
442 442 $ hg ci -q -A -m 'initial checkin'
443 443 $ touch added unknown
444 444 $ hg add added
445 445 $ hg remove removed
446 446 $ rm deleted
447 447 $ echo x > modified
448 448 $ hg copy modified copied
449 449 $ hg ci -m 'test checkin' -d "1000001 0"
450 450 $ rm *
451 451 $ touch unrelated
452 452 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
453 453
454 454 hg status --change 1:
455 455
456 456 $ hg status --change 1
457 457 M modified
458 458 A added
459 459 A copied
460 460 R removed
461 461
462 462 hg status --change 1 unrelated:
463 463
464 464 $ hg status --change 1 unrelated
465 465
466 466 hg status -C --change 1 added modified copied removed deleted:
467 467
468 468 $ hg status -C --change 1 added modified copied removed deleted
469 469 M modified
470 470 A added
471 471 A copied
472 472 modified
473 473 R removed
474 474
475 475 hg status -A --change 1 and revset:
476 476
477 477 $ hg status -A --change '1|1'
478 478 M modified
479 479 A added
480 480 A copied
481 481 modified
482 482 R removed
483 483 C deleted
484 484
485 485 $ cd ..
486 486
487 487 hg status with --rev and reverted changes:
488 488
489 489 $ hg init reverted-changes-repo
490 490 $ cd reverted-changes-repo
491 491 $ echo a > file
492 492 $ hg add file
493 493 $ hg ci -m a
494 494 $ echo b > file
495 495 $ hg ci -m b
496 496
497 497 reverted file should appear clean
498 498
499 499 $ hg revert -r 0 .
500 500 reverting file
501 501 $ hg status -A --rev 0
502 502 C file
503 503
504 504 #if execbit
505 505 reverted file with changed flag should appear modified
506 506
507 507 $ chmod +x file
508 508 $ hg status -A --rev 0
509 509 M file
510 510
511 511 $ hg revert -r 0 .
512 512 reverting file
513 513
514 514 reverted and committed file with changed flag should appear modified
515 515
516 516 $ hg co -C .
517 517 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
518 518 $ chmod +x file
519 519 $ hg ci -m 'change flag'
520 520 $ hg status -A --rev 1 --rev 2
521 521 M file
522 522 $ hg diff -r 1 -r 2
523 523
524 524 #endif
525 525
526 526 $ cd ..
527 527
528 528 hg status of binary file starting with '\1\n', a separator for metadata:
529 529
530 530 $ hg init repo5
531 531 $ cd repo5
532 532 >>> open("010a", r"wb").write(b"\1\nfoo") and None
533 533 $ hg ci -q -A -m 'initial checkin'
534 534 $ hg status -A
535 535 C 010a
536 536
537 537 >>> open("010a", r"wb").write(b"\1\nbar") and None
538 538 $ hg status -A
539 539 M 010a
540 540 $ hg ci -q -m 'modify 010a'
541 541 $ hg status -A --rev 0:1
542 542 M 010a
543 543
544 544 $ touch empty
545 545 $ hg ci -q -A -m 'add another file'
546 546 $ hg status -A --rev 1:2 010a
547 547 C 010a
548 548
549 549 $ cd ..
550 550
551 551 test "hg status" with "directory pattern" which matches against files
552 552 only known on target revision.
553 553
554 554 $ hg init repo6
555 555 $ cd repo6
556 556
557 557 $ echo a > a.txt
558 558 $ hg add a.txt
559 559 $ hg commit -m '#0'
560 560 $ mkdir -p 1/2/3/4/5
561 561 $ echo b > 1/2/3/4/5/b.txt
562 562 $ hg add 1/2/3/4/5/b.txt
563 563 $ hg commit -m '#1'
564 564
565 565 $ hg update -C 0 > /dev/null
566 566 $ hg status -A
567 567 C a.txt
568 568
569 569 the directory matching against specified pattern should be removed,
570 570 because directory existence prevents 'dirstate.walk()' from showing
571 571 warning message about such pattern.
572 572
573 573 $ test ! -d 1
574 574 $ hg status -A --rev 1 1/2/3/4/5/b.txt
575 575 R 1/2/3/4/5/b.txt
576 576 $ hg status -A --rev 1 1/2/3/4/5
577 577 R 1/2/3/4/5/b.txt
578 578 $ hg status -A --rev 1 1/2/3
579 579 R 1/2/3/4/5/b.txt
580 580 $ hg status -A --rev 1 1
581 581 R 1/2/3/4/5/b.txt
582 582
583 583 $ hg status --config ui.formatdebug=True --rev 1 1
584 584 status = [
585 585 {
586 586 'itemtype': 'file',
587 587 'path': '1/2/3/4/5/b.txt',
588 588 'status': 'R'
589 589 },
590 590 ]
591 591
592 592 #if windows
593 593 $ hg --config ui.slash=false status -A --rev 1 1
594 594 R 1\2\3\4\5\b.txt
595 595 #endif
596 596
597 597 $ cd ..
598 598
599 599 Status after move overwriting a file (issue4458)
600 600 =================================================
601 601
602 602
603 603 $ hg init issue4458
604 604 $ cd issue4458
605 605 $ echo a > a
606 606 $ echo b > b
607 607 $ hg commit -Am base
608 608 adding a
609 609 adding b
610 610
611 611
612 612 with --force
613 613
614 614 $ hg mv b --force a
615 615 $ hg st --copies
616 616 M a
617 617 b
618 618 R b
619 619 $ hg revert --all
620 620 reverting a
621 621 undeleting b
622 622 $ rm *.orig
623 623
624 624 without force
625 625
626 626 $ hg rm a
627 627 $ hg st --copies
628 628 R a
629 629 $ hg mv b a
630 630 $ hg st --copies
631 631 M a
632 632 b
633 633 R b
634 634
635 635 using ui.statuscopies setting
636 636 $ hg st --config ui.statuscopies=true
637 637 M a
638 638 b
639 639 R b
640 640 $ hg st --config ui.statuscopies=true --no-copies
641 641 M a
642 642 R b
643 643 $ hg st --config ui.statuscopies=false
644 644 M a
645 645 R b
646 646 $ hg st --config ui.statuscopies=false --copies
647 647 M a
648 648 b
649 649 R b
650 650 $ hg st --config ui.tweakdefaults=yes
651 651 M a
652 652 b
653 653 R b
654 654
655 655 using log status template (issue5155)
656 656 $ hg log -Tstatus -r 'wdir()' -C
657 657 changeset: 2147483647:ffffffffffff
658 658 parent: 0:8c55c58b4c0e
659 659 user: test
660 660 date: * (glob)
661 661 files:
662 662 M a
663 663 b
664 664 R b
665 665
666 666 $ hg log -GTstatus -r 'wdir()' -C
667 667 o changeset: 2147483647:ffffffffffff
668 668 | parent: 0:8c55c58b4c0e
669 669 ~ user: test
670 670 date: * (glob)
671 671 files:
672 672 M a
673 673 b
674 674 R b
675 675
676 676
677 677 Other "bug" highlight, the revision status does not report the copy information.
678 678 This is buggy behavior.
679 679
680 680 $ hg commit -m 'blah'
681 681 $ hg st --copies --change .
682 682 M a
683 683 R b
684 684
685 685 using log status template, the copy information is displayed correctly.
686 686 $ hg log -Tstatus -r. -C
687 687 changeset: 1:6685fde43d21
688 688 tag: tip
689 689 user: test
690 690 date: * (glob)
691 691 summary: blah
692 692 files:
693 693 M a
694 694 b
695 695 R b
696 696
697 697
698 698 $ cd ..
699 699
700 700 Make sure .hg doesn't show up even as a symlink
701 701
702 702 $ hg init repo0
703 703 $ mkdir symlink-repo0
704 704 $ cd symlink-repo0
705 705 $ ln -s ../repo0/.hg
706 706 $ hg status
707 707
708 708 If the size hasnt changed but mtime has, status needs to read the contents
709 709 of the file to check whether it has changed
710 710
711 711 $ echo 1 > a
712 712 $ echo 1 > b
713 713 $ touch -t 200102030000 a b
714 714 $ hg commit -Aqm '#0'
715 715 $ echo 2 > a
716 716 $ touch -t 200102040000 a b
717 717 $ hg status
718 718 M a
719 719
720 720 Asking specifically for the status of a deleted/removed file
721 721
722 722 $ rm a
723 723 $ rm b
724 724 $ hg status a
725 725 ! a
726 726 $ hg rm a
727 727 $ hg rm b
728 728 $ hg status a
729 729 R a
730 730 $ hg commit -qm '#1'
731 731 $ hg status a
732 732 a: $ENOENT$
733 733
734 734 Check using include flag with pattern when status does not need to traverse
735 735 the working directory (issue6483)
736 736
737 737 $ cd ..
738 738 $ hg init issue6483
739 739 $ cd issue6483
740 740 $ touch a.py b.rs
741 741 $ hg add a.py b.rs
742 742 $ hg st -aI "*.py"
743 743 A a.py
744 744
745 745 Also check exclude pattern
746 746
747 747 $ hg st -aX "*.rs"
748 748 A a.py
749 749
750 750 issue6335
751 751 When a directory containing a tracked file gets symlinked, as of 5.8
752 752 `hg st` only gives the correct answer about clean (or deleted) files
753 753 if also listing unknowns.
754 754 The tree-based dirstate and status algorithm fix this:
755 755
756 756 #if symlink no-dirstate-v1 rust
757 757
758 758 $ cd ..
759 759 $ hg init issue6335
760 760 $ cd issue6335
761 761 $ mkdir foo
762 762 $ touch foo/a
763 763 $ hg ci -Ama
764 764 adding foo/a
765 765 $ mv foo bar
766 766 $ ln -s bar foo
767 767 $ hg status
768 768 ! foo/a
769 769 ? bar/a
770 770 ? foo
771 771
772 772 $ hg status -c # incorrect output without the Rust implementation
773 773 $ hg status -cu
774 774 ? bar/a
775 775 ? foo
776 776 $ hg status -d # incorrect output without the Rust implementation
777 777 ! foo/a
778 778 $ hg status -du
779 779 ! foo/a
780 780 ? bar/a
781 781 ? foo
782 782
783 783 #endif
784 784
785 785
786 786 Create a repo with files in each possible status
787 787
788 788 $ cd ..
789 789 $ hg init repo7
790 790 $ cd repo7
791 791 $ mkdir subdir
792 792 $ touch clean modified deleted removed
793 793 $ touch subdir/clean subdir/modified subdir/deleted subdir/removed
794 794 $ echo ignored > .hgignore
795 795 $ hg ci -Aqm '#0'
796 796 $ echo 1 > modified
797 797 $ echo 1 > subdir/modified
798 798 $ rm deleted
799 799 $ rm subdir/deleted
800 800 $ hg rm removed
801 801 $ hg rm subdir/removed
802 802 $ touch unknown ignored
803 803 $ touch subdir/unknown subdir/ignored
804 804
805 805 Check the output
806 806
807 807 $ hg status
808 808 M modified
809 809 M subdir/modified
810 810 R removed
811 811 R subdir/removed
812 812 ! deleted
813 813 ! subdir/deleted
814 814 ? subdir/unknown
815 815 ? unknown
816 816
817 817 $ hg status -mard
818 818 M modified
819 819 M subdir/modified
820 820 R removed
821 821 R subdir/removed
822 822 ! deleted
823 823 ! subdir/deleted
824 824
825 825 $ hg status -A
826 826 M modified
827 827 M subdir/modified
828 828 R removed
829 829 R subdir/removed
830 830 ! deleted
831 831 ! subdir/deleted
832 832 ? subdir/unknown
833 833 ? unknown
834 834 I ignored
835 835 I subdir/ignored
836 836 C .hgignore
837 837 C clean
838 838 C subdir/clean
839 839
840 840 Note: `hg status some-name` creates a patternmatcher which is not supported
841 841 yet by the Rust implementation of status, but includematcher is supported.
842 842 --include is used below for that reason
843 843
844 844 #if unix-permissions
845 845
846 846 Not having permission to read a directory that contains tracked files makes
847 847 status emit a warning then behave as if the directory was empty or removed
848 848 entirely:
849 849
850 850 $ chmod 0 subdir
851 851 $ hg status --include subdir
852 852 subdir: Permission denied
853 853 R subdir/removed
854 854 ! subdir/clean
855 855 ! subdir/deleted
856 856 ! subdir/modified
857 857 $ chmod 755 subdir
858 858
859 859 #endif
860 860
861 861 Remove a directory that contains tracked files
862 862
863 863 $ rm -r subdir
864 864 $ hg status --include subdir
865 865 R subdir/removed
866 866 ! subdir/clean
867 867 ! subdir/deleted
868 868 ! subdir/modified
869 869
870 870 and replace it by a file
871 871
872 872 $ touch subdir
873 873 $ hg status --include subdir
874 874 R subdir/removed
875 875 ! subdir/clean
876 876 ! subdir/deleted
877 877 ! subdir/modified
878 878 ? subdir
879 879
880 880 Replaced a deleted or removed file with a directory
881 881
882 882 $ mkdir deleted removed
883 883 $ touch deleted/1 removed/1
884 884 $ hg status --include deleted --include removed
885 885 R removed
886 886 ! deleted
887 887 ? deleted/1
888 888 ? removed/1
889 889 $ hg add removed/1
890 890 $ hg status --include deleted --include removed
891 891 A removed/1
892 892 R removed
893 893 ! deleted
894 894 ? deleted/1
895 895
896 896 Deeply nested files in an ignored directory are still listed on request
897 897
898 898 $ echo ignored-dir >> .hgignore
899 899 $ mkdir ignored-dir
900 900 $ mkdir ignored-dir/subdir
901 901 $ touch ignored-dir/subdir/1
902 902 $ hg status --ignored
903 903 I ignored
904 904 I ignored-dir/subdir/1
905 905
906 906 Check using include flag while listing ignored composes correctly (issue6514)
907 907
908 908 $ cd ..
909 909 $ hg init issue6514
910 910 $ cd issue6514
911 911 $ mkdir ignored-folder
912 912 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
913 913 $ cat >.hgignore <<EOF
914 914 > A.hs
915 915 > B.hs
916 916 > ignored-folder/
917 917 > EOF
918 918 $ hg st -i -I 're:.*\.hs$'
919 919 I A.hs
920 920 I B.hs
921 921 I ignored-folder/ctest.hs
922 922
923 923 #if rust dirstate-v2
924 924
925 925 Check read_dir caching
926 926
927 927 $ cd ..
928 928 $ hg init repo8
929 929 $ cd repo8
930 930 $ mkdir subdir
931 931 $ touch subdir/a subdir/b
932 932 $ hg ci -Aqm '#0'
933 933
934 934 The cached mtime is initially unset
935 935
936 936 $ hg debugdirstate --all --no-dates | grep '^ '
937 937 0 -1 unset subdir
938 938
939 939 It is still not set when there are unknown files
940 940
941 941 $ touch subdir/unknown
942 942 $ hg status
943 943 ? subdir/unknown
944 944 $ hg debugdirstate --all --no-dates | grep '^ '
945 945 0 -1 unset subdir
946 946
947 947 Now the directory is eligible for caching, so its mtime is saved in the dirstate
948 948
949 949 $ rm subdir/unknown
950 950 $ sleep 0.1 # ensure the kernels internal clock for mtimes has ticked
951 951 $ hg status
952 952 $ hg debugdirstate --all --no-dates | grep '^ '
953 953 0 -1 set subdir
954 954
955 955 This time the command should be ever so slightly faster since it does not need `read_dir("subdir")`
956 956
957 957 $ hg status
958 958
959 959 Creating a new file changes the directorys mtime, invalidating the cache
960 960
961 961 $ touch subdir/unknown
962 962 $ hg status
963 963 ? subdir/unknown
964 964
965 965 $ rm subdir/unknown
966 966 $ hg status
967 967
968 968 Removing a node from the dirstate resets the cache for its parent directory
969 969
970 970 $ hg forget subdir/a
971 971 $ hg debugdirstate --all --no-dates | grep '^ '
972 972 0 -1 set subdir
973 973 $ hg ci -qm '#1'
974 974 $ hg debugdirstate --all --no-dates | grep '^ '
975 975 0 -1 unset subdir
976 976 $ hg status
977 977 ? subdir/a
978 978
979 979 Changing the hgignore rules makes us recompute the status (and rewrite the dirstate).
980 980
981 981 $ rm subdir/a
982 982 $ mkdir another-subdir
983 983 $ touch another-subdir/something-else
984 984
985 985 $ cat > "$TESTDIR"/extra-hgignore <<EOF
986 986 > something-else
987 987 > EOF
988 988
989 989 $ hg status --config ui.ignore.global="$TESTDIR"/extra-hgignore
990 990 $ hg debugdirstate --all --no-dates | grep '^ '
991 991 0 -1 set subdir
992 992
993 993 $ hg status
994 994 ? another-subdir/something-else
995 995
996 $ hg debugdirstate --all --no-dates | grep '^ '
997 0 -1 unset subdir (known-bad-output !)
998
999 For some reason the first [status] is not enough to save the updated
1000 directory mtime into the cache. The second invocation does it.
1001 The first call only clears the directory cache by marking the directories
1002 as "outdated", which seems like a bug.
1003
1004 $ hg status
1005 ? another-subdir/something-else
996 One invocation of status is enough to populate the cache even if it's invalidated
997 in the same run.
1006 998
1007 999 $ hg debugdirstate --all --no-dates | grep '^ '
1008 1000 0 -1 set subdir
1009 1001
1010 1002 #endif
General Comments 0
You need to be logged in to leave comments. Login now