##// END OF EJS Templates
rhg: fix race when a fixup file is deleted on disk...
Raphaël Gomès -
r51120:53ca3e3b stable
parent child Browse files
Show More
@@ -1,634 +1,646 b''
1 1 // status.rs
2 2 //
3 3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 use crate::error::CommandError;
9 9 use crate::ui::Ui;
10 10 use crate::utils::path_utils::RelativizePaths;
11 11 use clap::{Arg, SubCommand};
12 12 use format_bytes::format_bytes;
13 13 use hg::config::Config;
14 14 use hg::dirstate::has_exec_bit;
15 15 use hg::dirstate::status::StatusPath;
16 16 use hg::dirstate::TruncatedTimestamp;
17 17 use hg::errors::{HgError, IoResultExt};
18 18 use hg::lock::LockError;
19 19 use hg::manifest::Manifest;
20 20 use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
21 21 use hg::repo::Repo;
22 22 use hg::utils::debug::debug_wait_for_file;
23 23 use hg::utils::files::get_bytes_from_os_string;
24 24 use hg::utils::files::get_bytes_from_path;
25 25 use hg::utils::files::get_path_from_bytes;
26 26 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
27 27 use hg::DirstateStatus;
28 28 use hg::PatternFileWarning;
29 29 use hg::StatusError;
30 30 use hg::StatusOptions;
31 31 use hg::{self, narrow, sparse};
32 32 use log::info;
33 33 use rayon::prelude::*;
34 34 use std::io;
35 35 use std::path::PathBuf;
36 36
37 37 pub const HELP_TEXT: &str = "
38 38 Show changed files in the working directory
39 39
40 40 This is a pure Rust version of `hg status`.
41 41
42 42 Some options might be missing, check the list below.
43 43 ";
44 44
45 45 pub fn args() -> clap::App<'static, 'static> {
46 46 SubCommand::with_name("status")
47 47 .alias("st")
48 48 .about(HELP_TEXT)
49 49 .arg(
50 50 Arg::with_name("all")
51 51 .help("show status of all files")
52 52 .short("-A")
53 53 .long("--all"),
54 54 )
55 55 .arg(
56 56 Arg::with_name("modified")
57 57 .help("show only modified files")
58 58 .short("-m")
59 59 .long("--modified"),
60 60 )
61 61 .arg(
62 62 Arg::with_name("added")
63 63 .help("show only added files")
64 64 .short("-a")
65 65 .long("--added"),
66 66 )
67 67 .arg(
68 68 Arg::with_name("removed")
69 69 .help("show only removed files")
70 70 .short("-r")
71 71 .long("--removed"),
72 72 )
73 73 .arg(
74 74 Arg::with_name("clean")
75 75 .help("show only clean files")
76 76 .short("-c")
77 77 .long("--clean"),
78 78 )
79 79 .arg(
80 80 Arg::with_name("deleted")
81 81 .help("show only deleted files")
82 82 .short("-d")
83 83 .long("--deleted"),
84 84 )
85 85 .arg(
86 86 Arg::with_name("unknown")
87 87 .help("show only unknown (not tracked) files")
88 88 .short("-u")
89 89 .long("--unknown"),
90 90 )
91 91 .arg(
92 92 Arg::with_name("ignored")
93 93 .help("show only ignored files")
94 94 .short("-i")
95 95 .long("--ignored"),
96 96 )
97 97 .arg(
98 98 Arg::with_name("copies")
99 99 .help("show source of copied files (DEFAULT: ui.statuscopies)")
100 100 .short("-C")
101 101 .long("--copies"),
102 102 )
103 103 .arg(
104 104 Arg::with_name("no-status")
105 105 .help("hide status prefix")
106 106 .short("-n")
107 107 .long("--no-status"),
108 108 )
109 109 .arg(
110 110 Arg::with_name("verbose")
111 111 .help("enable additional output")
112 112 .short("-v")
113 113 .long("--verbose"),
114 114 )
115 115 }
116 116
117 117 /// Pure data type allowing the caller to specify file states to display
118 118 #[derive(Copy, Clone, Debug)]
119 119 pub struct DisplayStates {
120 120 pub modified: bool,
121 121 pub added: bool,
122 122 pub removed: bool,
123 123 pub clean: bool,
124 124 pub deleted: bool,
125 125 pub unknown: bool,
126 126 pub ignored: bool,
127 127 }
128 128
129 129 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
130 130 modified: true,
131 131 added: true,
132 132 removed: true,
133 133 clean: false,
134 134 deleted: true,
135 135 unknown: true,
136 136 ignored: false,
137 137 };
138 138
139 139 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
140 140 modified: true,
141 141 added: true,
142 142 removed: true,
143 143 clean: true,
144 144 deleted: true,
145 145 unknown: true,
146 146 ignored: true,
147 147 };
148 148
149 149 impl DisplayStates {
150 150 pub fn is_empty(&self) -> bool {
151 151 !(self.modified
152 152 || self.added
153 153 || self.removed
154 154 || self.clean
155 155 || self.deleted
156 156 || self.unknown
157 157 || self.ignored)
158 158 }
159 159 }
160 160
161 161 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
162 162 return Ok(repo.dirstate_parents()?.is_merge());
163 163 }
164 164
165 165 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
166 166 // These are all the known values for the [fname] argument of
167 167 // [addunfinished] function in [state.py]
168 168 let known_state_files: &[&str] = &[
169 169 "bisect.state",
170 170 "graftstate",
171 171 "histedit-state",
172 172 "rebasestate",
173 173 "shelvedstate",
174 174 "transplant/journal",
175 175 "updatestate",
176 176 ];
177 177 if has_unfinished_merge(repo)? {
178 178 return Ok(true);
179 179 };
180 180 for f in known_state_files {
181 181 if repo.hg_vfs().join(f).exists() {
182 182 return Ok(true);
183 183 }
184 184 }
185 185 return Ok(false);
186 186 }
187 187
188 188 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
189 189 // TODO: lift these limitations
190 190 if invocation
191 191 .config
192 192 .get(b"commands", b"status.terse")
193 193 .is_some()
194 194 {
195 195 return Err(CommandError::unsupported(
196 196 "status.terse is not yet supported with rhg status",
197 197 ));
198 198 }
199 199
200 200 let ui = invocation.ui;
201 201 let config = invocation.config;
202 202 let args = invocation.subcommand_args;
203 203
204 204 let verbose = !args.is_present("print0")
205 205 && (args.is_present("verbose")
206 206 || config.get_bool(b"ui", b"verbose")?
207 207 || config.get_bool(b"commands", b"status.verbose")?);
208 208
209 209 let all = args.is_present("all");
210 210 let display_states = if all {
211 211 // TODO when implementing `--quiet`: it excludes clean files
212 212 // from `--all`
213 213 ALL_DISPLAY_STATES
214 214 } else {
215 215 let requested = DisplayStates {
216 216 modified: args.is_present("modified"),
217 217 added: args.is_present("added"),
218 218 removed: args.is_present("removed"),
219 219 clean: args.is_present("clean"),
220 220 deleted: args.is_present("deleted"),
221 221 unknown: args.is_present("unknown"),
222 222 ignored: args.is_present("ignored"),
223 223 };
224 224 if requested.is_empty() {
225 225 DEFAULT_DISPLAY_STATES
226 226 } else {
227 227 requested
228 228 }
229 229 };
230 230 let no_status = args.is_present("no-status");
231 231 let list_copies = all
232 232 || args.is_present("copies")
233 233 || config.get_bool(b"ui", b"statuscopies")?;
234 234
235 235 let repo = invocation.repo?;
236 236
237 237 if verbose {
238 238 if has_unfinished_state(repo)? {
239 239 return Err(CommandError::unsupported(
240 240 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
241 241 ));
242 242 };
243 243 }
244 244
245 245 let mut dmap = repo.dirstate_map_mut()?;
246 246
247 247 let options = StatusOptions {
248 248 // we're currently supporting file systems with exec flags only
249 249 // anyway
250 250 check_exec: true,
251 251 list_clean: display_states.clean,
252 252 list_unknown: display_states.unknown,
253 253 list_ignored: display_states.ignored,
254 254 list_copies,
255 255 collect_traversed_dirs: false,
256 256 };
257 257
258 258 type StatusResult<'a> =
259 259 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
260 260
261 261 let after_status = |res: StatusResult| -> Result<_, CommandError> {
262 262 let (mut ds_status, pattern_warnings) = res?;
263 263 for warning in pattern_warnings {
264 264 ui.write_stderr(&print_pattern_file_warning(&warning, &repo))?;
265 265 }
266 266
267 267 for (path, error) in ds_status.bad {
268 268 let error = match error {
269 269 hg::BadMatch::OsError(code) => {
270 270 std::io::Error::from_raw_os_error(code).to_string()
271 271 }
272 272 hg::BadMatch::BadType(ty) => {
273 273 format!("unsupported file type (type is {})", ty)
274 274 }
275 275 };
276 276 ui.write_stderr(&format_bytes!(
277 277 b"{}: {}\n",
278 278 path.as_bytes(),
279 279 error.as_bytes()
280 280 ))?
281 281 }
282 282 if !ds_status.unsure.is_empty() {
283 283 info!(
284 284 "Files to be rechecked by retrieval from filelog: {:?}",
285 285 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
286 286 );
287 287 }
288 288 let mut fixup = Vec::new();
289 289 if !ds_status.unsure.is_empty()
290 290 && (display_states.modified || display_states.clean)
291 291 {
292 292 let p1 = repo.dirstate_parents()?.p1;
293 293 let manifest = repo.manifest_for_node(p1).map_err(|e| {
294 294 CommandError::from((e, &*format!("{:x}", p1.short())))
295 295 })?;
296 296 let working_directory_vfs = repo.working_directory_vfs();
297 297 let store_vfs = repo.store_vfs();
298 298 let res: Vec<_> = ds_status
299 299 .unsure
300 300 .into_par_iter()
301 301 .map(|to_check| {
302 302 unsure_is_modified(
303 303 working_directory_vfs,
304 304 store_vfs,
305 305 &manifest,
306 306 &to_check.path,
307 307 )
308 308 .map(|modified| (to_check, modified))
309 309 })
310 310 .collect::<Result<_, _>>()?;
311 311 for (status_path, is_modified) in res.into_iter() {
312 312 if is_modified {
313 313 if display_states.modified {
314 314 ds_status.modified.push(status_path);
315 315 }
316 316 } else {
317 317 if display_states.clean {
318 318 ds_status.clean.push(status_path.clone());
319 319 }
320 320 fixup.push(status_path.path.into_owned())
321 321 }
322 322 }
323 323 }
324 324 let relative_paths = config
325 325 .get_option(b"commands", b"status.relative")?
326 326 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
327 327 let output = DisplayStatusPaths {
328 328 ui,
329 329 no_status,
330 330 relativize: if relative_paths {
331 331 Some(RelativizePaths::new(repo)?)
332 332 } else {
333 333 None
334 334 },
335 335 };
336 336 if display_states.modified {
337 337 output.display(b"M ", "status.modified", ds_status.modified)?;
338 338 }
339 339 if display_states.added {
340 340 output.display(b"A ", "status.added", ds_status.added)?;
341 341 }
342 342 if display_states.removed {
343 343 output.display(b"R ", "status.removed", ds_status.removed)?;
344 344 }
345 345 if display_states.deleted {
346 346 output.display(b"! ", "status.deleted", ds_status.deleted)?;
347 347 }
348 348 if display_states.unknown {
349 349 output.display(b"? ", "status.unknown", ds_status.unknown)?;
350 350 }
351 351 if display_states.ignored {
352 352 output.display(b"I ", "status.ignored", ds_status.ignored)?;
353 353 }
354 354 if display_states.clean {
355 355 output.display(b"C ", "status.clean", ds_status.clean)?;
356 356 }
357 357
358 358 let dirstate_write_needed = ds_status.dirty;
359 359 let filesystem_time_at_status_start =
360 360 ds_status.filesystem_time_at_status_start;
361 361
362 362 Ok((
363 363 fixup,
364 364 dirstate_write_needed,
365 365 filesystem_time_at_status_start,
366 366 ))
367 367 };
368 368 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
369 369 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
370 370 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
371 371 (true, true) => {
372 372 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
373 373 }
374 374 (true, false) => narrow_matcher,
375 375 (false, true) => sparse_matcher,
376 376 (false, false) => Box::new(AlwaysMatcher),
377 377 };
378 378
379 379 for warning in narrow_warnings.into_iter().chain(sparse_warnings) {
380 380 match &warning {
381 381 sparse::SparseWarning::RootWarning { context, line } => {
382 382 let msg = format_bytes!(
383 383 b"warning: {} profile cannot use paths \"
384 384 starting with /, ignoring {}\n",
385 385 context,
386 386 line
387 387 );
388 388 ui.write_stderr(&msg)?;
389 389 }
390 390 sparse::SparseWarning::ProfileNotFound { profile, rev } => {
391 391 let msg = format_bytes!(
392 392 b"warning: sparse profile '{}' not found \"
393 393 in rev {} - ignoring it\n",
394 394 profile,
395 395 rev
396 396 );
397 397 ui.write_stderr(&msg)?;
398 398 }
399 399 sparse::SparseWarning::Pattern(e) => {
400 400 ui.write_stderr(&print_pattern_file_warning(e, &repo))?;
401 401 }
402 402 }
403 403 }
404 404 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
405 405 dmap.with_status(
406 406 matcher.as_ref(),
407 407 repo.working_directory_path().to_owned(),
408 408 ignore_files(repo, config),
409 409 options,
410 410 after_status,
411 411 )?;
412 412
413 413 // Development config option to test write races
414 414 if let Err(e) =
415 415 debug_wait_for_file(&config, "status.pre-dirstate-write-file")
416 416 {
417 417 ui.write_stderr(e.as_bytes()).ok();
418 418 }
419 419
420 420 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
421 421 && !dirstate_write_needed
422 422 {
423 423 // Nothing to update
424 424 return Ok(());
425 425 }
426 426
427 427 // Update the dirstate on disk if we can
428 428 let with_lock_result =
429 429 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
430 430 if let Some(mtime_boundary) = filesystem_time_at_status_start {
431 431 for hg_path in fixup {
432 432 use std::os::unix::fs::MetadataExt;
433 433 let fs_path = hg_path_to_path_buf(&hg_path)
434 434 .expect("HgPath conversion");
435 435 // Specifically do not reuse `fs_metadata` from
436 436 // `unsure_is_clean` which was needed before reading
437 437 // contents. Here we access metadata again after reading
438 438 // content, in case it changed in the meantime.
439 let fs_metadata = repo
439 let metadata_res = repo
440 440 .working_directory_vfs()
441 .symlink_metadata(&fs_path)?;
441 .symlink_metadata(&fs_path);
442 let fs_metadata = match metadata_res {
443 Ok(meta) => meta,
444 Err(err) => match err {
445 HgError::IoError { .. } => {
446 // The file has probably been deleted. In any
447 // case, it was in the dirstate before, so
448 // let's ignore the error.
449 continue;
450 }
451 _ => return Err(err.into()),
452 },
453 };
442 454 if let Some(mtime) =
443 455 TruncatedTimestamp::for_reliable_mtime_of(
444 456 &fs_metadata,
445 457 &mtime_boundary,
446 458 )
447 459 .when_reading_file(&fs_path)?
448 460 {
449 461 let mode = fs_metadata.mode();
450 462 let size = fs_metadata.len();
451 463 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
452 464 dirstate_write_needed = true
453 465 }
454 466 }
455 467 }
456 468 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
457 469 if dirstate_write_needed {
458 470 repo.write_dirstate()?
459 471 }
460 472 Ok(())
461 473 });
462 474 match with_lock_result {
463 475 Ok(closure_result) => closure_result?,
464 476 Err(LockError::AlreadyHeld) => {
465 477 // Not updating the dirstate is not ideal but not critical:
466 478 // don’t keep our caller waiting until some other Mercurial
467 479 // process releases the lock.
468 480 log::info!("not writing dirstate from `status`: lock is held")
469 481 }
470 482 Err(LockError::Other(HgError::IoError { error, .. }))
471 483 if error.kind() == io::ErrorKind::PermissionDenied =>
472 484 {
473 485 // `hg status` on a read-only repository is fine
474 486 }
475 487 Err(LockError::Other(error)) => {
476 488 // Report other I/O errors
477 489 Err(error)?
478 490 }
479 491 }
480 492 Ok(())
481 493 }
482 494
483 495 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
484 496 let mut ignore_files = Vec::new();
485 497 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
486 498 if repo_ignore.exists() {
487 499 ignore_files.push(repo_ignore)
488 500 }
489 501 for (key, value) in config.iter_section(b"ui") {
490 502 if key == b"ignore" || key.starts_with(b"ignore.") {
491 503 let path = get_path_from_bytes(value);
492 504 // TODO: expand "~/" and environment variable here, like Python
493 505 // does with `os.path.expanduser` and `os.path.expandvars`
494 506
495 507 let joined = repo.working_directory_path().join(path);
496 508 ignore_files.push(joined);
497 509 }
498 510 }
499 511 ignore_files
500 512 }
501 513
502 514 struct DisplayStatusPaths<'a> {
503 515 ui: &'a Ui,
504 516 no_status: bool,
505 517 relativize: Option<RelativizePaths>,
506 518 }
507 519
508 520 impl DisplayStatusPaths<'_> {
509 521 // Probably more elegant to use a Deref or Borrow trait rather than
510 522 // harcode HgPathBuf, but probably not really useful at this point
511 523 fn display(
512 524 &self,
513 525 status_prefix: &[u8],
514 526 label: &'static str,
515 527 mut paths: Vec<StatusPath<'_>>,
516 528 ) -> Result<(), CommandError> {
517 529 paths.sort_unstable();
518 530 // TODO: get the stdout lock once for the whole loop
519 531 // instead of in each write
520 532 for StatusPath { path, copy_source } in paths {
521 533 let relative;
522 534 let path = if let Some(relativize) = &self.relativize {
523 535 relative = relativize.relativize(&path);
524 536 &*relative
525 537 } else {
526 538 path.as_bytes()
527 539 };
528 540 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
529 541 // in order to stream to stdout instead of allocating an
530 542 // itermediate `Vec<u8>`.
531 543 if !self.no_status {
532 544 self.ui.write_stdout_labelled(status_prefix, label)?
533 545 }
534 546 self.ui
535 547 .write_stdout_labelled(&format_bytes!(b"{}\n", path), label)?;
536 548 if let Some(source) = copy_source {
537 549 let label = "status.copied";
538 550 self.ui.write_stdout_labelled(
539 551 &format_bytes!(b" {}\n", source.as_bytes()),
540 552 label,
541 553 )?
542 554 }
543 555 }
544 556 Ok(())
545 557 }
546 558 }
547 559
548 560 /// Check if a file is modified by comparing actual repo store and file system.
549 561 ///
550 562 /// This meant to be used for those that the dirstate cannot resolve, due
551 563 /// to time resolution limits.
552 564 fn unsure_is_modified(
553 565 working_directory_vfs: hg::vfs::Vfs,
554 566 store_vfs: hg::vfs::Vfs,
555 567 manifest: &Manifest,
556 568 hg_path: &HgPath,
557 569 ) -> Result<bool, HgError> {
558 570 let vfs = working_directory_vfs;
559 571 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
560 572 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
561 573 let is_symlink = fs_metadata.file_type().is_symlink();
562 574 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
563 575 // dirstate
564 576 let fs_flags = if is_symlink {
565 577 Some(b'l')
566 578 } else if has_exec_bit(&fs_metadata) {
567 579 Some(b'x')
568 580 } else {
569 581 None
570 582 };
571 583
572 584 let entry = manifest
573 585 .find_by_path(hg_path)?
574 586 .expect("ambgious file not in p1");
575 587 if entry.flags != fs_flags {
576 588 return Ok(true);
577 589 }
578 590 let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;
579 591 let fs_len = fs_metadata.len();
580 592 let file_node = entry.node_id()?;
581 593 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
582 594 HgError::corrupted(format!(
583 595 "filelog missing node {:?} from manifest",
584 596 file_node
585 597 ))
586 598 })?;
587 599 if filelog_entry.file_data_len_not_equal_to(fs_len) {
588 600 // No need to read file contents:
589 601 // it cannot be equal if it has a different length.
590 602 return Ok(true);
591 603 }
592 604
593 605 let p1_filelog_data = filelog_entry.data()?;
594 606 let p1_contents = p1_filelog_data.file_data()?;
595 607 if p1_contents.len() as u64 != fs_len {
596 608 // No need to read file contents:
597 609 // it cannot be equal if it has a different length.
598 610 return Ok(true);
599 611 }
600 612
601 613 let fs_contents = if is_symlink {
602 614 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
603 615 } else {
604 616 vfs.read(fs_path)?
605 617 };
606 618 Ok(p1_contents != &*fs_contents)
607 619 }
608 620
609 621 fn print_pattern_file_warning(
610 622 warning: &PatternFileWarning,
611 623 repo: &Repo,
612 624 ) -> Vec<u8> {
613 625 match warning {
614 626 PatternFileWarning::InvalidSyntax(path, syntax) => format_bytes!(
615 627 b"{}: ignoring invalid syntax '{}'\n",
616 628 get_bytes_from_path(path),
617 629 &*syntax
618 630 ),
619 631 PatternFileWarning::NoSuchFile(path) => {
620 632 let path = if let Ok(relative) =
621 633 path.strip_prefix(repo.working_directory_path())
622 634 {
623 635 relative
624 636 } else {
625 637 &*path
626 638 };
627 639 format_bytes!(
628 640 b"skipping unreadable pattern file '{}': \
629 641 No such file or directory\n",
630 642 get_bytes_from_path(path),
631 643 )
632 644 }
633 645 }
634 646 }
@@ -1,487 +1,485 b''
1 1 =====================================================================
2 2 Check potential race conditions between a status and other operations
3 3 =====================================================================
4 4
5 5 #testcases dirstate-v1 dirstate-v2-append dirstate-v2-rewrite
6 6
7 7 The `hg status` command can run without the wlock, however it might end up
8 8 having to update the on-disk dirstate files, for example to mark ambiguous
9 9 files as clean, or to update directory caches information with dirstate-v2.
10 10
11 11
12 12 If another process updates the dirstate in the meantime we might run into
13 13 trouble. Especially, commands doing semantic changes like `hg add` or
14 14 `hg commit` should not see their update erased by a concurrent status.
15 15
16 16 Unlike commands like `add` or `commit`, `status` only writes the dirstate
17 17 to update caches, no actual information is lost if we fail to write to disk.
18 18
19 19
20 20 This test file is meant to test various cases where such parallel operations
21 21 between a status with reasons to update the dirstate and another semantic
22 22 changes happen.
23 23
24 24
25 25 Setup
26 26 =====
27 27
28 28 $ cat >> $HGRCPATH << EOF
29 29 > [storage]
30 30 > dirstate-v2.slow-path=allow
31 31 > EOF
32 32
33 33 #if no-dirstate-v1
34 34 $ cat >> $HGRCPATH << EOF
35 35 > [format]
36 36 > use-dirstate-v2=yes
37 37 > EOF
38 38 #else
39 39 $ cat >> $HGRCPATH << EOF
40 40 > [format]
41 41 > use-dirstate-v2=no
42 42 > EOF
43 43 #endif
44 44
45 45 #if dirstate-v2-rewrite
46 46 $ d2args="--config devel.dirstate.v2.data_update_mode=force-new"
47 47 #endif
48 48 #if dirstate-v2-append
49 49 $ d2args="--config devel.dirstate.v2.data_update_mode=force-append"
50 50 #endif
51 51
52 52 $ directories="dir dir/nested dir2"
53 53 $ first_files="dir/nested/a dir/b dir/c dir/d dir2/e f"
54 54 $ second_files="g dir/nested/h dir/i dir/j dir2/k dir2/l dir/nested/m"
55 55 $ extra_files="dir/n dir/o p q"
56 56
57 57 $ hg init reference-repo
58 58 $ cd reference-repo
59 59 $ mkdir -p dir/nested dir2
60 60 $ touch -t 200001010000 $first_files $directories
61 61 $ hg commit -Aqm "recreate a bunch of files to facilitate dirstate-v2 append"
62 62 $ touch -t 200001010010 $second_files $directories
63 63 $ hg commit -Aqm "more files to have two commits"
64 64 $ hg log -G -v
65 65 @ changeset: 1:c349430a1631
66 66 | tag: tip
67 67 | user: test
68 68 | date: Thu Jan 01 00:00:00 1970 +0000
69 69 | files: dir/i dir/j dir/nested/h dir/nested/m dir2/k dir2/l g
70 70 | description:
71 71 | more files to have two commits
72 72 |
73 73 |
74 74 o changeset: 0:4f23db756b09
75 75 user: test
76 76 date: Thu Jan 01 00:00:00 1970 +0000
77 77 files: dir/b dir/c dir/d dir/nested/a dir2/e f
78 78 description:
79 79 recreate a bunch of files to facilitate dirstate-v2 append
80 80
81 81
82 82 $ hg manifest
83 83 dir/b
84 84 dir/c
85 85 dir/d
86 86 dir/i
87 87 dir/j
88 88 dir/nested/a
89 89 dir/nested/h
90 90 dir/nested/m
91 91 dir2/e
92 92 dir2/k
93 93 dir2/l
94 94 f
95 95 g
96 96
97 97 Add some unknown files and refresh the dirstate
98 98
99 99 $ touch -t 200001010020 $extra_files
100 100 $ hg add dir/o
101 101 $ hg remove dir/nested/m
102 102
103 103 $ hg st --config devel.dirstate.v2.data_update_mode=force-new
104 104 A dir/o
105 105 R dir/nested/m
106 106 ? dir/n
107 107 ? p
108 108 ? q
109 109 $ hg debugstate
110 110 n 644 0 2000-01-01 00:00:00 dir/b
111 111 n 644 0 2000-01-01 00:00:00 dir/c
112 112 n 644 0 2000-01-01 00:00:00 dir/d
113 113 n 644 0 2000-01-01 00:10:00 dir/i
114 114 n 644 0 2000-01-01 00:10:00 dir/j
115 115 n 644 0 2000-01-01 00:00:00 dir/nested/a
116 116 n 644 0 2000-01-01 00:10:00 dir/nested/h
117 117 r ?????????????????????????????????? dir/nested/m (glob)
118 118 a ?????????????????????????????????? dir/o (glob)
119 119 n 644 0 2000-01-01 00:00:00 dir2/e
120 120 n 644 0 2000-01-01 00:10:00 dir2/k
121 121 n 644 0 2000-01-01 00:10:00 dir2/l
122 122 n 644 0 2000-01-01 00:00:00 f
123 123 n 644 0 2000-01-01 00:10:00 g
124 124 $ hg debugstate > ../reference
125 125 $ cd ..
126 126
127 127 Explain / verify the test principles
128 128 ------------------------------------
129 129
130 130 First, we can properly copy the reference
131 131
132 132 $ cp -a reference-repo sanity-check
133 133 $ cd sanity-check
134 134 $ hg debugstate
135 135 n 644 0 2000-01-01 00:00:00 dir/b
136 136 n 644 0 2000-01-01 00:00:00 dir/c
137 137 n 644 0 2000-01-01 00:00:00 dir/d
138 138 n 644 0 2000-01-01 00:10:00 dir/i
139 139 n 644 0 2000-01-01 00:10:00 dir/j
140 140 n 644 0 2000-01-01 00:00:00 dir/nested/a
141 141 n 644 0 2000-01-01 00:10:00 dir/nested/h
142 142 r ?????????????????????????????????? dir/nested/m (glob)
143 143 a ?????????????????????????????????? dir/o (glob)
144 144 n 644 0 2000-01-01 00:00:00 dir2/e
145 145 n 644 0 2000-01-01 00:10:00 dir2/k
146 146 n 644 0 2000-01-01 00:10:00 dir2/l
147 147 n 644 0 2000-01-01 00:00:00 f
148 148 n 644 0 2000-01-01 00:10:00 g
149 149 $ hg debugstate > ../post-copy
150 150 $ diff ../reference ../post-copy
151 151
152 152 And status thinks the cache is in a proper state
153 153
154 154 $ hg st
155 155 A dir/o
156 156 R dir/nested/m
157 157 ? dir/n
158 158 ? p
159 159 ? q
160 160 $ hg debugstate
161 161 n 644 0 2000-01-01 00:00:00 dir/b
162 162 n 644 0 2000-01-01 00:00:00 dir/c
163 163 n 644 0 2000-01-01 00:00:00 dir/d
164 164 n 644 0 2000-01-01 00:10:00 dir/i
165 165 n 644 0 2000-01-01 00:10:00 dir/j
166 166 n 644 0 2000-01-01 00:00:00 dir/nested/a
167 167 n 644 0 2000-01-01 00:10:00 dir/nested/h
168 168 r ?????????????????????????????????? dir/nested/m (glob)
169 169 a ?????????????????????????????????? dir/o (glob)
170 170 n 644 0 2000-01-01 00:00:00 dir2/e
171 171 n 644 0 2000-01-01 00:10:00 dir2/k
172 172 n 644 0 2000-01-01 00:10:00 dir2/l
173 173 n 644 0 2000-01-01 00:00:00 f
174 174 n 644 0 2000-01-01 00:10:00 g
175 175 $ hg debugstate > ../post-status
176 176 $ diff ../reference ../post-status
177 177
178 178 Then we can start a status that:
179 179 - has some update to do (the touch call)
180 180 - will wait AFTER running status, but before updating the cache on disk
181 181
182 182 $ touch -t 200001010001 dir/c
183 183 $ hg st >$TESTTMP/status-race-lock.out 2>$TESTTMP/status-race-lock.log \
184 184 > --config rhg.on-unsupported=abort \
185 185 > --config devel.sync.status.pre-dirstate-write-file=$TESTTMP/status-race-lock \
186 186 > &
187 187 $ $RUNTESTDIR/testlib/wait-on-file 5 $TESTTMP/status-race-lock.waiting
188 188
189 189 We check it runs the status first by modifying a file and updating another timestamp
190 190
191 191 $ touch -t 200001010003 dir/i
192 192 $ echo babar > dir/j
193 193 $ touch $TESTTMP/status-race-lock
194 194 $ wait
195 195
196 196 The test process should have reported a status before the change we made,
197 197 and should have missed the timestamp update
198 198
199 199 $ cat $TESTTMP/status-race-lock.out
200 200 A dir/o
201 201 R dir/nested/m
202 202 ? dir/n
203 203 ? p
204 204 ? q
205 205 $ cat $TESTTMP/status-race-lock.log
206 206 $ hg debugstate | grep dir/c
207 207 n 644 0 2000-01-01 00:01:00 dir/c
208 208 $ hg debugstate | grep dir/i
209 209 n 644 0 2000-01-01 00:10:00 dir/i
210 210 $ hg debugstate | grep dir/j
211 211 n 644 0 2000-01-01 00:10:00 dir/j
212 212
213 213 final cleanup
214 214
215 215 $ rm $TESTTMP/status-race-lock $TESTTMP/status-race-lock.waiting
216 216 $ cd ..
217 217
218 218 Actual Testing
219 219 ==============
220 220
221 221 Race with a `hg add`
222 222 -------------------
223 223
224 224 $ cp -a reference-repo race-with-add
225 225 $ cd race-with-add
226 226
227 227 spin a `hg status` with some caches to update
228 228
229 229 $ touch -t 200001020001 f
230 230 $ hg st >$TESTTMP/status-race-lock.out 2>$TESTTMP/status-race-lock.log \
231 231 > --config rhg.on-unsupported=abort \
232 232 > --config devel.sync.status.pre-dirstate-write-file=$TESTTMP/status-race-lock \
233 233 > &
234 234 $ $RUNTESTDIR/testlib/wait-on-file 5 $TESTTMP/status-race-lock.waiting
235 235
236 236 Add a file
237 237
238 238 $ hg $d2args add dir/n
239 239 $ touch $TESTTMP/status-race-lock
240 240 $ wait
241 241
242 242 The file should in a "added" state
243 243
244 244 $ hg status
245 245 A dir/n (no-rhg !)
246 246 A dir/n (rhg dirstate-v2-rewrite !)
247 247 A dir/n (missing-correct-output rhg dirstate-v1 !)
248 248 A dir/o
249 249 R dir/nested/m
250 250 ? dir/n (known-bad-output rhg no-dirstate-v2-rewrite !)
251 251 ? p
252 252 ? q
253 253
254 254 The status process should return a consistent result and not crash.
255 255
256 256 $ cat $TESTTMP/status-race-lock.out
257 257 A dir/o
258 258 R dir/nested/m
259 259 ? dir/n
260 260 ? p
261 261 ? q
262 262 $ cat $TESTTMP/status-race-lock.log
263 263 abort: when writing $TESTTMP/race-with-add/.hg/dirstate.*: $ENOENT$ (glob) (known-bad-output rhg dirstate-v2-rewrite !)
264 264
265 265 final cleanup
266 266
267 267 $ rm $TESTTMP/status-race-lock $TESTTMP/status-race-lock.waiting
268 268 $ cd ..
269 269
270 270 Race with a `hg commit`
271 271 ----------------------
272 272
273 273 $ cp -a reference-repo race-with-commit
274 274 $ cd race-with-commit
275 275
276 276 spin a `hg status` with some caches to update
277 277
278 278 $ touch -t 200001020001 dir/j
279 279 $ hg st >$TESTTMP/status-race-lock.out 2>$TESTTMP/status-race-lock.log \
280 280 > --config rhg.on-unsupported=abort \
281 281 > --config devel.sync.status.pre-dirstate-write-file=$TESTTMP/status-race-lock \
282 282 > &
283 283 $ $RUNTESTDIR/testlib/wait-on-file 5 $TESTTMP/status-race-lock.waiting
284 284
285 285 Add a file and force the data file rewrite
286 286
287 287 $ hg $d2args commit -m created-during-status dir/o
288 288 $ touch $TESTTMP/status-race-lock
289 289 $ wait
290 290
291 291 The parent must change and the status should be clean
292 292
293 293 # XXX rhg misbehaves here
294 294 #if no-rhg
295 295 $ hg summary
296 296 parent: 2:2e3b442a2fd4 tip
297 297 created-during-status
298 298 branch: default
299 299 commit: 1 removed, 3 unknown
300 300 update: (current)
301 301 phases: 3 draft
302 302 $ hg status
303 303 R dir/nested/m
304 304 ? dir/n
305 305 ? p
306 306 ? q
307 307 #else
308 308 $ hg summary
309 309 parent: 1:c349430a1631
310 310 more files to have two commits
311 311 branch: default
312 312 commit: 1 added, 1 removed, 3 unknown (new branch head)
313 313 update: 1 new changesets (update)
314 314 phases: 3 draft
315 315 $ hg status
316 316 A dir/o
317 317 R dir/nested/m
318 318 ? dir/n
319 319 ? p
320 320 ? q
321 321 #endif
322 322
323 323 The status process should return a consistent result and not crash.
324 324
325 325 $ cat $TESTTMP/status-race-lock.out
326 326 A dir/o
327 327 R dir/nested/m
328 328 ? dir/n
329 329 ? p
330 330 ? q
331 331 $ cat $TESTTMP/status-race-lock.log
332 332 abort: when removing $TESTTMP/race-with-commit/.hg/dirstate.*: $ENOENT$ (glob) (known-bad-output rhg dirstate-v2-rewrite !)
333 333
334 334 final cleanup
335 335
336 336 $ rm $TESTTMP/status-race-lock $TESTTMP/status-race-lock.waiting
337 337 $ cd ..
338 338
339 339 Race with a `hg update`
340 340 ----------------------
341 341
342 342 $ cp -a reference-repo race-with-update
343 343 $ cd race-with-update
344 344
345 345 spin a `hg status` with some caches to update
346 346
347 347 $ touch -t 200001020001 dir2/k
348 348 $ hg st >$TESTTMP/status-race-lock.out 2>$TESTTMP/status-race-lock.log \
349 349 > --config rhg.on-unsupported=abort \
350 350 > --config devel.sync.status.pre-dirstate-write-file=$TESTTMP/status-race-lock \
351 351 > &
352 352 $ $RUNTESTDIR/testlib/wait-on-file 5 $TESTTMP/status-race-lock.waiting
353 353
354 354 Add a file and force the data file rewrite
355 355
356 356 $ hg $d2args update ".~1"
357 357 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
358 358 $ touch $TESTTMP/status-race-lock
359 359 $ wait
360 360
361 361 The parent must change and the status should be clean
362 362
363 363 $ hg summary
364 364 parent: 0:4f23db756b09
365 365 recreate a bunch of files to facilitate dirstate-v2 append
366 366 branch: default
367 367 commit: 1 added, 3 unknown (new branch head)
368 368 update: 1 new changesets (update)
369 369 phases: 2 draft
370 370 $ hg status
371 371 A dir/o
372 372 ? dir/n
373 373 ? p
374 374 ? q
375 375
376 376 The status process should return a consistent result and not crash.
377 377
378 378 $ cat $TESTTMP/status-race-lock.out
379 379 A dir/o
380 380 R dir/nested/m
381 381 ? dir/n
382 382 ? p
383 383 ? q
384 384 $ cat $TESTTMP/status-race-lock.log
385 abort: when reading $TESTTMP/race-with-update/dir2/k: $ENOENT$ (known-bad-output rhg !)
386 385
387 386 final cleanup
388 387
389 388 $ rm $TESTTMP/status-race-lock $TESTTMP/status-race-lock.waiting
390 389 $ cd ..
391 390
392 391 Race with another status
393 392 ------------------------
394 393
395 394 $ cp -a reference-repo race-with-status
396 395 $ cd race-with-status
397 396
398 397 spin a `hg status` with some caches to update
399 398
400 399 $ touch -t 200001010030 dir/nested/h
401 400 $ hg st >$TESTTMP/status-race-lock.out 2>$TESTTMP/status-race-lock.log \
402 401 > --config rhg.on-unsupported=abort \
403 402 > --config devel.sync.status.pre-dirstate-write-file=$TESTTMP/status-race-lock \
404 403 > &
405 404 $ $RUNTESTDIR/testlib/wait-on-file 5 $TESTTMP/status-race-lock.waiting
406 405
407 406 touch g
408 407
409 408 $ touch -t 200001010025 g
410 409 $ hg $d2args status
411 410 A dir/o
412 411 R dir/nested/m
413 412 ? dir/n
414 413 ? p
415 414 ? q
416 415 $ touch $TESTTMP/status-race-lock
417 416 $ wait
418 417
419 418 the first update should be on disk
420 419
421 420 $ hg debugstate --all | grep "g"
422 421 n 644 0 2000-01-01 00:25:00 g (no-rhg !)
423 422 n 644 0 2000-01-01 00:25:00 g (missing-correct-output rhg !)
424 423 n 644 0 2000-01-01 00:10:00 g (known-bad-output rhg !)
425 424
426 425 The status process should return a consistent result and not crash.
427 426
428 427 $ cat $TESTTMP/status-race-lock.out
429 428 A dir/o
430 429 R dir/nested/m
431 430 ? dir/n
432 431 ? p
433 432 ? q
434 433 $ cat $TESTTMP/status-race-lock.log
435 434 abort: when removing $TESTTMP/race-with-status/.hg/dirstate.*: $ENOENT$ (glob) (known-bad-output rhg dirstate-v2-rewrite !)
436 435
437 436 final cleanup
438 437
439 438 $ rm $TESTTMP/status-race-lock $TESTTMP/status-race-lock.waiting
440 439 $ cd ..
441 440
442 441 Race with the removal of an ambiguous file
443 442 ----------------------è-------------------
444 443
445 444 $ cp -a reference-repo race-with-remove
446 445 $ cd race-with-remove
447 446
448 447 spin a `hg status` with some caches to update
449 448
450 449 $ touch -t 200001010035 dir2/l
451 450 $ hg st >$TESTTMP/status-race-lock.out 2>$TESTTMP/status-race-lock.log \
452 451 > --config rhg.on-unsupported=abort \
453 452 > --config devel.sync.status.pre-dirstate-write-file=$TESTTMP/status-race-lock \
454 453 > &
455 454 $ $RUNTESTDIR/testlib/wait-on-file 5 $TESTTMP/status-race-lock.waiting
456 455
457 456 remove that same file
458 457
459 458 $ hg $d2args remove dir2/l
460 459 $ touch $TESTTMP/status-race-lock
461 460 $ wait
462 461
463 462 file should be marked as removed
464 463
465 464 $ hg status
466 465 A dir/o
467 466 R dir/nested/m
468 467 R dir2/l
469 468 ? dir/n
470 469 ? p
471 470 ? q
472 471
473 472 The status process should return a consistent result and not crash.
474 473
475 474 $ cat $TESTTMP/status-race-lock.out
476 475 A dir/o
477 476 R dir/nested/m
478 477 ? dir/n
479 478 ? p
480 479 ? q
481 480 $ cat $TESTTMP/status-race-lock.log
482 abort: when reading $TESTTMP/race-with-remove/dir2/l: $ENOENT$ (known-bad-output rhg !)
483 481
484 482 final cleanup
485 483
486 484 $ rm $TESTTMP/status-race-lock $TESTTMP/status-race-lock.waiting
487 485 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now