##// END OF EJS Templates
rhg: support `status --print0`...
Arseniy Alekseyev -
r51289:98fc949b default
parent child Browse files
Show More
@@ -1,657 +1,670 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::{
10 10 format_pattern_file_warning, print_narrow_sparse_warnings, Ui,
11 11 };
12 12 use crate::utils::path_utils::RelativizePaths;
13 13 use clap::Arg;
14 14 use format_bytes::format_bytes;
15 15 use hg::config::Config;
16 16 use hg::dirstate::has_exec_bit;
17 17 use hg::dirstate::status::StatusPath;
18 18 use hg::dirstate::TruncatedTimestamp;
19 19 use hg::errors::{HgError, IoResultExt};
20 20 use hg::lock::LockError;
21 21 use hg::manifest::Manifest;
22 22 use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
23 23 use hg::repo::Repo;
24 24 use hg::utils::debug::debug_wait_for_file;
25 25 use hg::utils::files::get_bytes_from_os_string;
26 26 use hg::utils::files::get_path_from_bytes;
27 27 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
28 28 use hg::DirstateStatus;
29 29 use hg::PatternFileWarning;
30 30 use hg::StatusError;
31 31 use hg::StatusOptions;
32 32 use hg::{self, narrow, sparse};
33 33 use log::info;
34 34 use rayon::prelude::*;
35 35 use std::io;
36 36 use std::path::PathBuf;
37 37
38 38 pub const HELP_TEXT: &str = "
39 39 Show changed files in the working directory
40 40
41 41 This is a pure Rust version of `hg status`.
42 42
43 43 Some options might be missing, check the list below.
44 44 ";
45 45
46 46 pub fn args() -> clap::Command {
47 47 clap::command!("status")
48 48 .alias("st")
49 49 .about(HELP_TEXT)
50 50 .arg(
51 51 Arg::new("all")
52 52 .help("show status of all files")
53 53 .short('A')
54 54 .action(clap::ArgAction::SetTrue)
55 55 .long("all"),
56 56 )
57 57 .arg(
58 58 Arg::new("modified")
59 59 .help("show only modified files")
60 60 .short('m')
61 61 .action(clap::ArgAction::SetTrue)
62 62 .long("modified"),
63 63 )
64 64 .arg(
65 65 Arg::new("added")
66 66 .help("show only added files")
67 67 .short('a')
68 68 .action(clap::ArgAction::SetTrue)
69 69 .long("added"),
70 70 )
71 71 .arg(
72 72 Arg::new("removed")
73 73 .help("show only removed files")
74 74 .short('r')
75 75 .action(clap::ArgAction::SetTrue)
76 76 .long("removed"),
77 77 )
78 78 .arg(
79 79 Arg::new("clean")
80 80 .help("show only clean files")
81 81 .short('c')
82 82 .action(clap::ArgAction::SetTrue)
83 83 .long("clean"),
84 84 )
85 85 .arg(
86 86 Arg::new("deleted")
87 87 .help("show only deleted files")
88 88 .short('d')
89 89 .action(clap::ArgAction::SetTrue)
90 90 .long("deleted"),
91 91 )
92 92 .arg(
93 93 Arg::new("unknown")
94 94 .help("show only unknown (not tracked) files")
95 95 .short('u')
96 96 .action(clap::ArgAction::SetTrue)
97 97 .long("unknown"),
98 98 )
99 99 .arg(
100 100 Arg::new("ignored")
101 101 .help("show only ignored files")
102 102 .short('i')
103 103 .action(clap::ArgAction::SetTrue)
104 104 .long("ignored"),
105 105 )
106 106 .arg(
107 107 Arg::new("copies")
108 108 .help("show source of copied files (DEFAULT: ui.statuscopies)")
109 109 .short('C')
110 110 .action(clap::ArgAction::SetTrue)
111 111 .long("copies"),
112 112 )
113 113 .arg(
114 Arg::new("print0")
115 .help("end filenames with NUL, for use with xargs")
116 .short('0')
117 .action(clap::ArgAction::SetTrue)
118 .long("print0"),
119 )
120 .arg(
114 121 Arg::new("no-status")
115 122 .help("hide status prefix")
116 123 .short('n')
117 124 .action(clap::ArgAction::SetTrue)
118 125 .long("no-status"),
119 126 )
120 127 .arg(
121 128 Arg::new("verbose")
122 129 .help("enable additional output")
123 130 .short('v')
124 131 .action(clap::ArgAction::SetTrue)
125 132 .long("verbose"),
126 133 )
127 134 }
128 135
129 136 /// Pure data type allowing the caller to specify file states to display
130 137 #[derive(Copy, Clone, Debug)]
131 138 pub struct DisplayStates {
132 139 pub modified: bool,
133 140 pub added: bool,
134 141 pub removed: bool,
135 142 pub clean: bool,
136 143 pub deleted: bool,
137 144 pub unknown: bool,
138 145 pub ignored: bool,
139 146 }
140 147
141 148 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
142 149 modified: true,
143 150 added: true,
144 151 removed: true,
145 152 clean: false,
146 153 deleted: true,
147 154 unknown: true,
148 155 ignored: false,
149 156 };
150 157
151 158 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
152 159 modified: true,
153 160 added: true,
154 161 removed: true,
155 162 clean: true,
156 163 deleted: true,
157 164 unknown: true,
158 165 ignored: true,
159 166 };
160 167
161 168 impl DisplayStates {
162 169 pub fn is_empty(&self) -> bool {
163 170 !(self.modified
164 171 || self.added
165 172 || self.removed
166 173 || self.clean
167 174 || self.deleted
168 175 || self.unknown
169 176 || self.ignored)
170 177 }
171 178 }
172 179
173 180 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
174 181 Ok(repo.dirstate_parents()?.is_merge())
175 182 }
176 183
177 184 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
178 185 // These are all the known values for the [fname] argument of
179 186 // [addunfinished] function in [state.py]
180 187 let known_state_files: &[&str] = &[
181 188 "bisect.state",
182 189 "graftstate",
183 190 "histedit-state",
184 191 "rebasestate",
185 192 "shelvedstate",
186 193 "transplant/journal",
187 194 "updatestate",
188 195 ];
189 196 if has_unfinished_merge(repo)? {
190 197 return Ok(true);
191 198 };
192 199 for f in known_state_files {
193 200 if repo.hg_vfs().join(f).exists() {
194 201 return Ok(true);
195 202 }
196 203 }
197 204 Ok(false)
198 205 }
199 206
200 207 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
201 208 // TODO: lift these limitations
202 209 if invocation
203 210 .config
204 211 .get(b"commands", b"status.terse")
205 212 .is_some()
206 213 {
207 214 return Err(CommandError::unsupported(
208 215 "status.terse is not yet supported with rhg status",
209 216 ));
210 217 }
211 218
212 219 let ui = invocation.ui;
213 220 let config = invocation.config;
214 221 let args = invocation.subcommand_args;
215 222
216 // TODO add `!args.get_flag("print0") &&` when we support `print0`
223 let print0 = args.get_flag("print0");
217 224 let verbose = args.get_flag("verbose")
218 225 || config.get_bool(b"ui", b"verbose")?
219 226 || config.get_bool(b"commands", b"status.verbose")?;
227 let verbose = verbose && !print0;
220 228
221 229 let all = args.get_flag("all");
222 230 let display_states = if all {
223 231 // TODO when implementing `--quiet`: it excludes clean files
224 232 // from `--all`
225 233 ALL_DISPLAY_STATES
226 234 } else {
227 235 let requested = DisplayStates {
228 236 modified: args.get_flag("modified"),
229 237 added: args.get_flag("added"),
230 238 removed: args.get_flag("removed"),
231 239 clean: args.get_flag("clean"),
232 240 deleted: args.get_flag("deleted"),
233 241 unknown: args.get_flag("unknown"),
234 242 ignored: args.get_flag("ignored"),
235 243 };
236 244 if requested.is_empty() {
237 245 DEFAULT_DISPLAY_STATES
238 246 } else {
239 247 requested
240 248 }
241 249 };
242 250 let no_status = args.get_flag("no-status");
243 251 let list_copies = all
244 252 || args.get_flag("copies")
245 253 || config.get_bool(b"ui", b"statuscopies")?;
246 254
247 255 let repo = invocation.repo?;
248 256
249 257 if verbose && has_unfinished_state(repo)? {
250 258 return Err(CommandError::unsupported(
251 259 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
252 260 ));
253 261 }
254 262
255 263 let mut dmap = repo.dirstate_map_mut()?;
256 264
257 265 let check_exec = hg::checkexec::check_exec(repo.working_directory_path());
258 266
259 267 let options = StatusOptions {
260 268 check_exec,
261 269 list_clean: display_states.clean,
262 270 list_unknown: display_states.unknown,
263 271 list_ignored: display_states.ignored,
264 272 list_copies,
265 273 collect_traversed_dirs: false,
266 274 };
267 275
268 276 type StatusResult<'a> =
269 277 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
270 278
271 279 let after_status = |res: StatusResult| -> Result<_, CommandError> {
272 280 let (mut ds_status, pattern_warnings) = res?;
273 281 for warning in pattern_warnings {
274 282 ui.write_stderr(&format_pattern_file_warning(&warning, repo))?;
275 283 }
276 284
277 285 for (path, error) in ds_status.bad {
278 286 let error = match error {
279 287 hg::BadMatch::OsError(code) => {
280 288 std::io::Error::from_raw_os_error(code).to_string()
281 289 }
282 290 hg::BadMatch::BadType(ty) => {
283 291 format!("unsupported file type (type is {})", ty)
284 292 }
285 293 };
286 294 ui.write_stderr(&format_bytes!(
287 295 b"{}: {}\n",
288 296 path.as_bytes(),
289 297 error.as_bytes()
290 298 ))?
291 299 }
292 300 if !ds_status.unsure.is_empty() {
293 301 info!(
294 302 "Files to be rechecked by retrieval from filelog: {:?}",
295 303 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
296 304 );
297 305 }
298 306 let mut fixup = Vec::new();
299 307 if !ds_status.unsure.is_empty()
300 308 && (display_states.modified || display_states.clean)
301 309 {
302 310 let p1 = repo.dirstate_parents()?.p1;
303 311 let manifest = repo.manifest_for_node(p1).map_err(|e| {
304 312 CommandError::from((e, &*format!("{:x}", p1.short())))
305 313 })?;
306 314 let working_directory_vfs = repo.working_directory_vfs();
307 315 let store_vfs = repo.store_vfs();
308 316 let res: Vec<_> = ds_status
309 317 .unsure
310 318 .into_par_iter()
311 319 .map(|to_check| {
312 320 // The compiler seems to get a bit confused with complex
313 321 // inference when using a parallel iterator + map
314 322 // + map_err + collect, so let's just inline some of the
315 323 // logic.
316 324 match unsure_is_modified(
317 325 working_directory_vfs,
318 326 store_vfs,
319 327 check_exec,
320 328 &manifest,
321 329 &to_check.path,
322 330 ) {
323 331 Err(HgError::IoError { .. }) => {
324 332 // IO errors most likely stem from the file being
325 333 // deleted even though we know it's in the
326 334 // dirstate.
327 335 Ok((to_check, UnsureOutcome::Deleted))
328 336 }
329 337 Ok(outcome) => Ok((to_check, outcome)),
330 338 Err(e) => Err(e),
331 339 }
332 340 })
333 341 .collect::<Result<_, _>>()?;
334 342 for (status_path, outcome) in res.into_iter() {
335 343 match outcome {
336 344 UnsureOutcome::Clean => {
337 345 if display_states.clean {
338 346 ds_status.clean.push(status_path.clone());
339 347 }
340 348 fixup.push(status_path.path.into_owned())
341 349 }
342 350 UnsureOutcome::Modified => {
343 351 if display_states.modified {
344 352 ds_status.modified.push(status_path);
345 353 }
346 354 }
347 355 UnsureOutcome::Deleted => {
348 356 if display_states.deleted {
349 357 ds_status.deleted.push(status_path);
350 358 }
351 359 }
352 360 }
353 361 }
354 362 }
355 363 let relative_paths = config
356 364 .get_option(b"commands", b"status.relative")?
357 365 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
358 366 let output = DisplayStatusPaths {
359 367 ui,
360 368 no_status,
361 369 relativize: if relative_paths {
362 370 Some(RelativizePaths::new(repo)?)
363 371 } else {
364 372 None
365 373 },
374 print0,
366 375 };
367 376 if display_states.modified {
368 377 output.display(b"M ", "status.modified", ds_status.modified)?;
369 378 }
370 379 if display_states.added {
371 380 output.display(b"A ", "status.added", ds_status.added)?;
372 381 }
373 382 if display_states.removed {
374 383 output.display(b"R ", "status.removed", ds_status.removed)?;
375 384 }
376 385 if display_states.deleted {
377 386 output.display(b"! ", "status.deleted", ds_status.deleted)?;
378 387 }
379 388 if display_states.unknown {
380 389 output.display(b"? ", "status.unknown", ds_status.unknown)?;
381 390 }
382 391 if display_states.ignored {
383 392 output.display(b"I ", "status.ignored", ds_status.ignored)?;
384 393 }
385 394 if display_states.clean {
386 395 output.display(b"C ", "status.clean", ds_status.clean)?;
387 396 }
388 397
389 398 let dirstate_write_needed = ds_status.dirty;
390 399 let filesystem_time_at_status_start =
391 400 ds_status.filesystem_time_at_status_start;
392 401
393 402 Ok((
394 403 fixup,
395 404 dirstate_write_needed,
396 405 filesystem_time_at_status_start,
397 406 ))
398 407 };
399 408 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
400 409 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
401 410 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
402 411 (true, true) => {
403 412 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
404 413 }
405 414 (true, false) => narrow_matcher,
406 415 (false, true) => sparse_matcher,
407 416 (false, false) => Box::new(AlwaysMatcher),
408 417 };
409 418
410 419 print_narrow_sparse_warnings(
411 420 &narrow_warnings,
412 421 &sparse_warnings,
413 422 ui,
414 423 repo,
415 424 )?;
416 425 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
417 426 dmap.with_status(
418 427 matcher.as_ref(),
419 428 repo.working_directory_path().to_owned(),
420 429 ignore_files(repo, config),
421 430 options,
422 431 after_status,
423 432 )?;
424 433
425 434 // Development config option to test write races
426 435 if let Err(e) =
427 436 debug_wait_for_file(config, "status.pre-dirstate-write-file")
428 437 {
429 438 ui.write_stderr(e.as_bytes()).ok();
430 439 }
431 440
432 441 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
433 442 && !dirstate_write_needed
434 443 {
435 444 // Nothing to update
436 445 return Ok(());
437 446 }
438 447
439 448 // Update the dirstate on disk if we can
440 449 let with_lock_result =
441 450 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
442 451 if let Some(mtime_boundary) = filesystem_time_at_status_start {
443 452 for hg_path in fixup {
444 453 use std::os::unix::fs::MetadataExt;
445 454 let fs_path = hg_path_to_path_buf(&hg_path)
446 455 .expect("HgPath conversion");
447 456 // Specifically do not reuse `fs_metadata` from
448 457 // `unsure_is_clean` which was needed before reading
449 458 // contents. Here we access metadata again after reading
450 459 // content, in case it changed in the meantime.
451 460 let metadata_res = repo
452 461 .working_directory_vfs()
453 462 .symlink_metadata(&fs_path);
454 463 let fs_metadata = match metadata_res {
455 464 Ok(meta) => meta,
456 465 Err(err) => match err {
457 466 HgError::IoError { .. } => {
458 467 // The file has probably been deleted. In any
459 468 // case, it was in the dirstate before, so
460 469 // let's ignore the error.
461 470 continue;
462 471 }
463 472 _ => return Err(err.into()),
464 473 },
465 474 };
466 475 if let Some(mtime) =
467 476 TruncatedTimestamp::for_reliable_mtime_of(
468 477 &fs_metadata,
469 478 &mtime_boundary,
470 479 )
471 480 .when_reading_file(&fs_path)?
472 481 {
473 482 let mode = fs_metadata.mode();
474 483 let size = fs_metadata.len();
475 484 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
476 485 dirstate_write_needed = true
477 486 }
478 487 }
479 488 }
480 489 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
481 490 if dirstate_write_needed {
482 491 repo.write_dirstate()?
483 492 }
484 493 Ok(())
485 494 });
486 495 match with_lock_result {
487 496 Ok(closure_result) => closure_result?,
488 497 Err(LockError::AlreadyHeld) => {
489 498 // Not updating the dirstate is not ideal but not critical:
490 499 // don’t keep our caller waiting until some other Mercurial
491 500 // process releases the lock.
492 501 log::info!("not writing dirstate from `status`: lock is held")
493 502 }
494 503 Err(LockError::Other(HgError::IoError { error, .. }))
495 504 if error.kind() == io::ErrorKind::PermissionDenied =>
496 505 {
497 506 // `hg status` on a read-only repository is fine
498 507 }
499 508 Err(LockError::Other(error)) => {
500 509 // Report other I/O errors
501 510 Err(error)?
502 511 }
503 512 }
504 513 Ok(())
505 514 }
506 515
507 516 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
508 517 let mut ignore_files = Vec::new();
509 518 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
510 519 if repo_ignore.exists() {
511 520 ignore_files.push(repo_ignore)
512 521 }
513 522 for (key, value) in config.iter_section(b"ui") {
514 523 if key == b"ignore" || key.starts_with(b"ignore.") {
515 524 let path = get_path_from_bytes(value);
516 525 // TODO:Β expand "~/" and environment variable here, like Python
517 526 // does with `os.path.expanduser` and `os.path.expandvars`
518 527
519 528 let joined = repo.working_directory_path().join(path);
520 529 ignore_files.push(joined);
521 530 }
522 531 }
523 532 ignore_files
524 533 }
525 534
526 535 struct DisplayStatusPaths<'a> {
527 536 ui: &'a Ui,
528 537 no_status: bool,
529 538 relativize: Option<RelativizePaths>,
539 print0: bool,
530 540 }
531 541
532 542 impl DisplayStatusPaths<'_> {
533 543 // Probably more elegant to use a Deref or Borrow trait rather than
534 544 // harcode HgPathBuf, but probably not really useful at this point
535 545 fn display(
536 546 &self,
537 547 status_prefix: &[u8],
538 548 label: &'static str,
539 549 mut paths: Vec<StatusPath<'_>>,
540 550 ) -> Result<(), CommandError> {
541 551 paths.sort_unstable();
542 552 // TODO: get the stdout lock once for the whole loop
543 553 // instead of in each write
544 554 for StatusPath { path, copy_source } in paths {
545 555 let relative;
546 556 let path = if let Some(relativize) = &self.relativize {
547 557 relative = relativize.relativize(&path);
548 558 &*relative
549 559 } else {
550 560 path.as_bytes()
551 561 };
552 562 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
553 563 // in order to stream to stdout instead of allocating an
554 564 // itermediate `Vec<u8>`.
555 565 if !self.no_status {
556 566 self.ui.write_stdout_labelled(status_prefix, label)?
557 567 }
558 self.ui
559 .write_stdout_labelled(&format_bytes!(b"{}\n", path), label)?;
568 let linebreak = if self.print0 { b"\x00" } else { b"\n" };
569 self.ui.write_stdout_labelled(
570 &format_bytes!(b"{}{}", path, linebreak),
571 label,
572 )?;
560 573 if let Some(source) = copy_source {
561 574 let label = "status.copied";
562 575 self.ui.write_stdout_labelled(
563 &format_bytes!(b" {}\n", source.as_bytes()),
576 &format_bytes!(b" {}{}", source.as_bytes(), linebreak),
564 577 label,
565 578 )?
566 579 }
567 580 }
568 581 Ok(())
569 582 }
570 583 }
571 584
572 585 /// Outcome of the additional check for an ambiguous tracked file
573 586 enum UnsureOutcome {
574 587 /// The file is actually clean
575 588 Clean,
576 589 /// The file has been modified
577 590 Modified,
578 591 /// The file was deleted on disk (or became another type of fs entry)
579 592 Deleted,
580 593 }
581 594
582 595 /// Check if a file is modified by comparing actual repo store and file system.
583 596 ///
584 597 /// This meant to be used for those that the dirstate cannot resolve, due
585 598 /// to time resolution limits.
586 599 fn unsure_is_modified(
587 600 working_directory_vfs: hg::vfs::Vfs,
588 601 store_vfs: hg::vfs::Vfs,
589 602 check_exec: bool,
590 603 manifest: &Manifest,
591 604 hg_path: &HgPath,
592 605 ) -> Result<UnsureOutcome, HgError> {
593 606 let vfs = working_directory_vfs;
594 607 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
595 608 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
596 609 let is_symlink = fs_metadata.file_type().is_symlink();
597 610
598 611 let entry = manifest
599 612 .find_by_path(hg_path)?
600 613 .expect("ambgious file not in p1");
601 614
602 615 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
603 616 // dirstate
604 617 let fs_flags = if is_symlink {
605 618 Some(b'l')
606 619 } else if check_exec && has_exec_bit(&fs_metadata) {
607 620 Some(b'x')
608 621 } else {
609 622 None
610 623 };
611 624
612 625 let entry_flags = if check_exec {
613 626 entry.flags
614 627 } else if entry.flags == Some(b'x') {
615 628 None
616 629 } else {
617 630 entry.flags
618 631 };
619 632
620 633 if entry_flags != fs_flags {
621 634 return Ok(UnsureOutcome::Modified);
622 635 }
623 636 let filelog = hg::filelog::Filelog::open_vfs(&store_vfs, hg_path)?;
624 637 let fs_len = fs_metadata.len();
625 638 let file_node = entry.node_id()?;
626 639 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
627 640 HgError::corrupted(format!(
628 641 "filelog {:?} missing node {:?} from manifest",
629 642 hg_path, file_node
630 643 ))
631 644 })?;
632 645 if filelog_entry.file_data_len_not_equal_to(fs_len) {
633 646 // No need to read file contents:
634 647 // it cannot be equal if it has a different length.
635 648 return Ok(UnsureOutcome::Modified);
636 649 }
637 650
638 651 let p1_filelog_data = filelog_entry.data()?;
639 652 let p1_contents = p1_filelog_data.file_data()?;
640 653 if p1_contents.len() as u64 != fs_len {
641 654 // No need to read file contents:
642 655 // it cannot be equal if it has a different length.
643 656 return Ok(UnsureOutcome::Modified);
644 657 }
645 658
646 659 let fs_contents = if is_symlink {
647 660 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
648 661 } else {
649 662 vfs.read(fs_path)?
650 663 };
651 664
652 665 Ok(if p1_contents != &*fs_contents {
653 666 UnsureOutcome::Modified
654 667 } else {
655 668 UnsureOutcome::Clean
656 669 })
657 670 }
@@ -1,1002 +1,1007 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 hg status -0:
250
251 $ hg status -0 --config rhg.on-unsupported=abort
252 A added\x00A copied\x00R removed\x00! deleted\x00? unknown\x00 (no-eol) (esc)
253
249 254 hg status -A:
250 255
251 256 $ hg status -A
252 257 A added
253 258 A copied
254 259 modified
255 260 R removed
256 261 ! deleted
257 262 ? unknown
258 263 I ignored
259 264 C .hgignore
260 265 C modified
261 266
262 267 $ hg status -A -T '{status} {path} {node|shortest}\n'
263 268 A added ffff
264 269 A copied ffff
265 270 R removed ffff
266 271 ! deleted ffff
267 272 ? unknown ffff
268 273 I ignored ffff
269 274 C .hgignore ffff
270 275 C modified ffff
271 276
272 277 $ hg status -A -Tjson
273 278 [
274 279 {
275 280 "itemtype": "file",
276 281 "path": "added",
277 282 "status": "A"
278 283 },
279 284 {
280 285 "itemtype": "file",
281 286 "path": "copied",
282 287 "source": "modified",
283 288 "status": "A"
284 289 },
285 290 {
286 291 "itemtype": "file",
287 292 "path": "removed",
288 293 "status": "R"
289 294 },
290 295 {
291 296 "itemtype": "file",
292 297 "path": "deleted",
293 298 "status": "!"
294 299 },
295 300 {
296 301 "itemtype": "file",
297 302 "path": "unknown",
298 303 "status": "?"
299 304 },
300 305 {
301 306 "itemtype": "file",
302 307 "path": "ignored",
303 308 "status": "I"
304 309 },
305 310 {
306 311 "itemtype": "file",
307 312 "path": ".hgignore",
308 313 "status": "C"
309 314 },
310 315 {
311 316 "itemtype": "file",
312 317 "path": "modified",
313 318 "status": "C"
314 319 }
315 320 ]
316 321
317 322 $ hg status -A -Tpickle > pickle
318 323 >>> import pickle
319 324 >>> from mercurial import util
320 325 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
321 326 >>> for s, p in data: print("%s %s" % (s, p))
322 327 ! deleted
323 328 ? pickle
324 329 ? unknown
325 330 A added
326 331 A copied
327 332 C .hgignore
328 333 C modified
329 334 I ignored
330 335 R removed
331 336 $ rm pickle
332 337
333 338 $ echo "^ignoreddir$" > .hgignore
334 339 $ mkdir ignoreddir
335 340 $ touch ignoreddir/file
336 341
337 342 Test templater support:
338 343
339 344 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
340 345 [M] .hgignore
341 346 [A] added
342 347 [A] modified -> copied
343 348 [R] removed
344 349 [!] deleted
345 350 [?] ignored
346 351 [?] unknown
347 352 [I] ignoreddir/file
348 353 [C] modified
349 354 $ hg status -AT default
350 355 M .hgignore
351 356 A added
352 357 A copied
353 358 modified
354 359 R removed
355 360 ! deleted
356 361 ? ignored
357 362 ? unknown
358 363 I ignoreddir/file
359 364 C modified
360 365 $ hg status -T compact
361 366 abort: "status" not in template map
362 367 [255]
363 368
364 369 hg status ignoreddir/file:
365 370
366 371 $ hg status ignoreddir/file
367 372
368 373 hg status -i ignoreddir/file:
369 374
370 375 $ hg status -i ignoreddir/file
371 376 I ignoreddir/file
372 377 $ cd ..
373 378
374 379 Check 'status -q' and some combinations
375 380
376 381 $ hg init repo3
377 382 $ cd repo3
378 383 $ touch modified removed deleted ignored
379 384 $ echo "^ignored$" > .hgignore
380 385 $ hg commit -A -m 'initial checkin'
381 386 adding .hgignore
382 387 adding deleted
383 388 adding modified
384 389 adding removed
385 390 $ touch added unknown ignored
386 391 $ hg add added
387 392 $ echo "test" >> modified
388 393 $ hg remove removed
389 394 $ rm deleted
390 395 $ hg copy modified copied
391 396
392 397 Specify working directory revision explicitly, that should be the same as
393 398 "hg status"
394 399
395 400 $ hg status --change "wdir()"
396 401 M modified
397 402 A added
398 403 A copied
399 404 R removed
400 405 ! deleted
401 406 ? unknown
402 407
403 408 Run status with 2 different flags.
404 409 Check if result is the same or different.
405 410 If result is not as expected, raise error
406 411
407 412 $ assert() {
408 413 > hg status $1 > ../a
409 414 > hg status $2 > ../b
410 415 > if diff ../a ../b > /dev/null; then
411 416 > out=0
412 417 > else
413 418 > out=1
414 419 > fi
415 420 > if [ $3 -eq 0 ]; then
416 421 > df="same"
417 422 > else
418 423 > df="different"
419 424 > fi
420 425 > if [ $out -ne $3 ]; then
421 426 > echo "Error on $1 and $2, should be $df."
422 427 > fi
423 428 > }
424 429
425 430 Assert flag1 flag2 [0-same | 1-different]
426 431
427 432 $ assert "-q" "-mard" 0
428 433 $ assert "-A" "-marduicC" 0
429 434 $ assert "-qA" "-mardcC" 0
430 435 $ assert "-qAui" "-A" 0
431 436 $ assert "-qAu" "-marducC" 0
432 437 $ assert "-qAi" "-mardicC" 0
433 438 $ assert "-qu" "-u" 0
434 439 $ assert "-q" "-u" 1
435 440 $ assert "-m" "-a" 1
436 441 $ assert "-r" "-d" 1
437 442 $ cd ..
438 443
439 444 $ hg init repo4
440 445 $ cd repo4
441 446 $ touch modified removed deleted
442 447 $ hg ci -q -A -m 'initial checkin'
443 448 $ touch added unknown
444 449 $ hg add added
445 450 $ hg remove removed
446 451 $ rm deleted
447 452 $ echo x > modified
448 453 $ hg copy modified copied
449 454 $ hg ci -m 'test checkin' -d "1000001 0"
450 455 $ rm *
451 456 $ touch unrelated
452 457 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
453 458
454 459 hg status --change 1:
455 460
456 461 $ hg status --change 1
457 462 M modified
458 463 A added
459 464 A copied
460 465 R removed
461 466
462 467 hg status --change 1 unrelated:
463 468
464 469 $ hg status --change 1 unrelated
465 470
466 471 hg status -C --change 1 added modified copied removed deleted:
467 472
468 473 $ hg status -C --change 1 added modified copied removed deleted
469 474 M modified
470 475 A added
471 476 A copied
472 477 modified
473 478 R removed
474 479
475 480 hg status -A --change 1 and revset:
476 481
477 482 $ hg status -A --change '1|1'
478 483 M modified
479 484 A added
480 485 A copied
481 486 modified
482 487 R removed
483 488 C deleted
484 489
485 490 $ cd ..
486 491
487 492 hg status with --rev and reverted changes:
488 493
489 494 $ hg init reverted-changes-repo
490 495 $ cd reverted-changes-repo
491 496 $ echo a > file
492 497 $ hg add file
493 498 $ hg ci -m a
494 499 $ echo b > file
495 500 $ hg ci -m b
496 501
497 502 reverted file should appear clean
498 503
499 504 $ hg revert -r 0 .
500 505 reverting file
501 506 $ hg status -A --rev 0
502 507 C file
503 508
504 509 #if execbit
505 510 reverted file with changed flag should appear modified
506 511
507 512 $ chmod +x file
508 513 $ hg status -A --rev 0
509 514 M file
510 515
511 516 $ hg revert -r 0 .
512 517 reverting file
513 518
514 519 reverted and committed file with changed flag should appear modified
515 520
516 521 $ hg co -C .
517 522 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
518 523 $ chmod +x file
519 524 $ hg ci -m 'change flag'
520 525 $ hg status -A --rev 1 --rev 2
521 526 M file
522 527 $ hg diff -r 1 -r 2
523 528
524 529 #endif
525 530
526 531 $ cd ..
527 532
528 533 hg status of binary file starting with '\1\n', a separator for metadata:
529 534
530 535 $ hg init repo5
531 536 $ cd repo5
532 537 >>> open("010a", r"wb").write(b"\1\nfoo") and None
533 538 $ hg ci -q -A -m 'initial checkin'
534 539 $ hg status -A
535 540 C 010a
536 541
537 542 >>> open("010a", r"wb").write(b"\1\nbar") and None
538 543 $ hg status -A
539 544 M 010a
540 545 $ hg ci -q -m 'modify 010a'
541 546 $ hg status -A --rev 0:1
542 547 M 010a
543 548
544 549 $ touch empty
545 550 $ hg ci -q -A -m 'add another file'
546 551 $ hg status -A --rev 1:2 010a
547 552 C 010a
548 553
549 554 $ cd ..
550 555
551 556 test "hg status" with "directory pattern" which matches against files
552 557 only known on target revision.
553 558
554 559 $ hg init repo6
555 560 $ cd repo6
556 561
557 562 $ echo a > a.txt
558 563 $ hg add a.txt
559 564 $ hg commit -m '#0'
560 565 $ mkdir -p 1/2/3/4/5
561 566 $ echo b > 1/2/3/4/5/b.txt
562 567 $ hg add 1/2/3/4/5/b.txt
563 568 $ hg commit -m '#1'
564 569
565 570 $ hg update -C 0 > /dev/null
566 571 $ hg status -A
567 572 C a.txt
568 573
569 574 the directory matching against specified pattern should be removed,
570 575 because directory existence prevents 'dirstate.walk()' from showing
571 576 warning message about such pattern.
572 577
573 578 $ test ! -d 1
574 579 $ hg status -A --rev 1 1/2/3/4/5/b.txt
575 580 R 1/2/3/4/5/b.txt
576 581 $ hg status -A --rev 1 1/2/3/4/5
577 582 R 1/2/3/4/5/b.txt
578 583 $ hg status -A --rev 1 1/2/3
579 584 R 1/2/3/4/5/b.txt
580 585 $ hg status -A --rev 1 1
581 586 R 1/2/3/4/5/b.txt
582 587
583 588 $ hg status --config ui.formatdebug=True --rev 1 1
584 589 status = [
585 590 {
586 591 'itemtype': 'file',
587 592 'path': '1/2/3/4/5/b.txt',
588 593 'status': 'R'
589 594 },
590 595 ]
591 596
592 597 #if windows
593 598 $ hg --config ui.slash=false status -A --rev 1 1
594 599 R 1\2\3\4\5\b.txt
595 600 #endif
596 601
597 602 $ cd ..
598 603
599 604 Status after move overwriting a file (issue4458)
600 605 =================================================
601 606
602 607
603 608 $ hg init issue4458
604 609 $ cd issue4458
605 610 $ echo a > a
606 611 $ echo b > b
607 612 $ hg commit -Am base
608 613 adding a
609 614 adding b
610 615
611 616
612 617 with --force
613 618
614 619 $ hg mv b --force a
615 620 $ hg st --copies
616 621 M a
617 622 b
618 623 R b
619 624 $ hg revert --all
620 625 reverting a
621 626 undeleting b
622 627 $ rm *.orig
623 628
624 629 without force
625 630
626 631 $ hg rm a
627 632 $ hg st --copies
628 633 R a
629 634 $ hg mv b a
630 635 $ hg st --copies
631 636 M a
632 637 b
633 638 R b
634 639
635 640 using ui.statuscopies setting
636 641 $ hg st --config ui.statuscopies=true
637 642 M a
638 643 b
639 644 R b
640 645 $ hg st --config ui.statuscopies=true --no-copies
641 646 M a
642 647 R b
643 648 $ hg st --config ui.statuscopies=false
644 649 M a
645 650 R b
646 651 $ hg st --config ui.statuscopies=false --copies
647 652 M a
648 653 b
649 654 R b
650 655 $ hg st --config ui.tweakdefaults=yes
651 656 M a
652 657 b
653 658 R b
654 659
655 660 using log status template (issue5155)
656 661 $ hg log -Tstatus -r 'wdir()' -C
657 662 changeset: 2147483647:ffffffffffff
658 663 parent: 0:8c55c58b4c0e
659 664 user: test
660 665 date: * (glob)
661 666 files:
662 667 M a
663 668 b
664 669 R b
665 670
666 671 $ hg log -GTstatus -r 'wdir()' -C
667 672 o changeset: 2147483647:ffffffffffff
668 673 | parent: 0:8c55c58b4c0e
669 674 ~ user: test
670 675 date: * (glob)
671 676 files:
672 677 M a
673 678 b
674 679 R b
675 680
676 681
677 682 Other "bug" highlight, the revision status does not report the copy information.
678 683 This is buggy behavior.
679 684
680 685 $ hg commit -m 'blah'
681 686 $ hg st --copies --change .
682 687 M a
683 688 R b
684 689
685 690 using log status template, the copy information is displayed correctly.
686 691 $ hg log -Tstatus -r. -C
687 692 changeset: 1:6685fde43d21
688 693 tag: tip
689 694 user: test
690 695 date: * (glob)
691 696 summary: blah
692 697 files:
693 698 M a
694 699 b
695 700 R b
696 701
697 702
698 703 $ cd ..
699 704
700 705 Make sure .hg doesn't show up even as a symlink
701 706
702 707 $ hg init repo0
703 708 $ mkdir symlink-repo0
704 709 $ cd symlink-repo0
705 710 $ ln -s ../repo0/.hg
706 711 $ hg status
707 712
708 713 If the size hasn’t changed but mtime has, status needs to read the contents
709 714 of the file to check whether it has changed
710 715
711 716 $ echo 1 > a
712 717 $ echo 1 > b
713 718 $ touch -t 200102030000 a b
714 719 $ hg commit -Aqm '#0'
715 720 $ echo 2 > a
716 721 $ touch -t 200102040000 a b
717 722 $ hg status
718 723 M a
719 724
720 725 Asking specifically for the status of a deleted/removed file
721 726
722 727 $ rm a
723 728 $ rm b
724 729 $ hg status a
725 730 ! a
726 731 $ hg rm a
727 732 $ hg rm b
728 733 $ hg status a
729 734 R a
730 735 $ hg commit -qm '#1'
731 736 $ hg status a
732 737 a: $ENOENT$
733 738
734 739 Check using include flag with pattern when status does not need to traverse
735 740 the working directory (issue6483)
736 741
737 742 $ cd ..
738 743 $ hg init issue6483
739 744 $ cd issue6483
740 745 $ touch a.py b.rs
741 746 $ hg add a.py b.rs
742 747 $ hg st -aI "*.py"
743 748 A a.py
744 749
745 750 Also check exclude pattern
746 751
747 752 $ hg st -aX "*.rs"
748 753 A a.py
749 754
750 755 issue6335
751 756 When a directory containing a tracked file gets symlinked, as of 5.8
752 757 `hg st` only gives the correct answer about clean (or deleted) files
753 758 if also listing unknowns.
754 759 The tree-based dirstate and status algorithm fix this:
755 760
756 761 #if symlink no-dirstate-v1 rust
757 762
758 763 $ cd ..
759 764 $ hg init issue6335
760 765 $ cd issue6335
761 766 $ mkdir foo
762 767 $ touch foo/a
763 768 $ hg ci -Ama
764 769 adding foo/a
765 770 $ mv foo bar
766 771 $ ln -s bar foo
767 772 $ hg status
768 773 ! foo/a
769 774 ? bar/a
770 775 ? foo
771 776
772 777 $ hg status -c # incorrect output without the Rust implementation
773 778 $ hg status -cu
774 779 ? bar/a
775 780 ? foo
776 781 $ hg status -d # incorrect output without the Rust implementation
777 782 ! foo/a
778 783 $ hg status -du
779 784 ! foo/a
780 785 ? bar/a
781 786 ? foo
782 787
783 788 #endif
784 789
785 790
786 791 Create a repo with files in each possible status
787 792
788 793 $ cd ..
789 794 $ hg init repo7
790 795 $ cd repo7
791 796 $ mkdir subdir
792 797 $ touch clean modified deleted removed
793 798 $ touch subdir/clean subdir/modified subdir/deleted subdir/removed
794 799 $ echo ignored > .hgignore
795 800 $ hg ci -Aqm '#0'
796 801 $ echo 1 > modified
797 802 $ echo 1 > subdir/modified
798 803 $ rm deleted
799 804 $ rm subdir/deleted
800 805 $ hg rm removed
801 806 $ hg rm subdir/removed
802 807 $ touch unknown ignored
803 808 $ touch subdir/unknown subdir/ignored
804 809
805 810 Check the output
806 811
807 812 $ hg status
808 813 M modified
809 814 M subdir/modified
810 815 R removed
811 816 R subdir/removed
812 817 ! deleted
813 818 ! subdir/deleted
814 819 ? subdir/unknown
815 820 ? unknown
816 821
817 822 $ hg status -mard
818 823 M modified
819 824 M subdir/modified
820 825 R removed
821 826 R subdir/removed
822 827 ! deleted
823 828 ! subdir/deleted
824 829
825 830 $ hg status -A
826 831 M modified
827 832 M subdir/modified
828 833 R removed
829 834 R subdir/removed
830 835 ! deleted
831 836 ! subdir/deleted
832 837 ? subdir/unknown
833 838 ? unknown
834 839 I ignored
835 840 I subdir/ignored
836 841 C .hgignore
837 842 C clean
838 843 C subdir/clean
839 844
840 845 Note: `hg status some-name` creates a patternmatcher which is not supported
841 846 yet by the Rust implementation of status, but includematcher is supported.
842 847 --include is used below for that reason
843 848
844 849 #if unix-permissions
845 850
846 851 Not having permission to read a directory that contains tracked files makes
847 852 status emit a warning then behave as if the directory was empty or removed
848 853 entirely:
849 854
850 855 $ chmod 0 subdir
851 856 $ hg status --include subdir
852 857 subdir: $EACCES$
853 858 R subdir/removed
854 859 ! subdir/clean
855 860 ! subdir/deleted
856 861 ! subdir/modified
857 862 $ chmod 755 subdir
858 863
859 864 #endif
860 865
861 866 Remove a directory that contains tracked files
862 867
863 868 $ rm -r subdir
864 869 $ hg status --include subdir
865 870 R subdir/removed
866 871 ! subdir/clean
867 872 ! subdir/deleted
868 873 ! subdir/modified
869 874
870 875 … and replace it by a file
871 876
872 877 $ touch subdir
873 878 $ hg status --include subdir
874 879 R subdir/removed
875 880 ! subdir/clean
876 881 ! subdir/deleted
877 882 ! subdir/modified
878 883 ? subdir
879 884
880 885 Replaced a deleted or removed file with a directory
881 886
882 887 $ mkdir deleted removed
883 888 $ touch deleted/1 removed/1
884 889 $ hg status --include deleted --include removed
885 890 R removed
886 891 ! deleted
887 892 ? deleted/1
888 893 ? removed/1
889 894 $ hg add removed/1
890 895 $ hg status --include deleted --include removed
891 896 A removed/1
892 897 R removed
893 898 ! deleted
894 899 ? deleted/1
895 900
896 901 Deeply nested files in an ignored directory are still listed on request
897 902
898 903 $ echo ignored-dir >> .hgignore
899 904 $ mkdir ignored-dir
900 905 $ mkdir ignored-dir/subdir
901 906 $ touch ignored-dir/subdir/1
902 907 $ hg status --ignored
903 908 I ignored
904 909 I ignored-dir/subdir/1
905 910
906 911 Check using include flag while listing ignored composes correctly (issue6514)
907 912
908 913 $ cd ..
909 914 $ hg init issue6514
910 915 $ cd issue6514
911 916 $ mkdir ignored-folder
912 917 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
913 918 $ cat >.hgignore <<EOF
914 919 > A.hs
915 920 > B.hs
916 921 > ignored-folder/
917 922 > EOF
918 923 $ hg st -i -I 're:.*\.hs$'
919 924 I A.hs
920 925 I B.hs
921 926 I ignored-folder/ctest.hs
922 927
923 928 #if rust dirstate-v2
924 929
925 930 Check read_dir caching
926 931
927 932 $ cd ..
928 933 $ hg init repo8
929 934 $ cd repo8
930 935 $ mkdir subdir
931 936 $ touch subdir/a subdir/b
932 937 $ hg ci -Aqm '#0'
933 938
934 939 The cached mtime is initially unset
935 940
936 941 $ hg debugdirstate --all --no-dates | grep '^ '
937 942 0 -1 unset subdir
938 943
939 944 It is still not set when there are unknown files
940 945
941 946 $ touch subdir/unknown
942 947 $ hg status
943 948 ? subdir/unknown
944 949 $ hg debugdirstate --all --no-dates | grep '^ '
945 950 0 -1 unset subdir
946 951
947 952 Now the directory is eligible for caching, so its mtime is saved in the dirstate
948 953
949 954 $ rm subdir/unknown
950 955 $ sleep 0.1 # ensure the kernel’s internal clock for mtimes has ticked
951 956 $ hg status
952 957 $ hg debugdirstate --all --no-dates | grep '^ '
953 958 0 -1 set subdir
954 959
955 960 This time the command should be ever so slightly faster since it does not need `read_dir("subdir")`
956 961
957 962 $ hg status
958 963
959 964 Creating a new file changes the directory’s mtime, invalidating the cache
960 965
961 966 $ touch subdir/unknown
962 967 $ hg status
963 968 ? subdir/unknown
964 969
965 970 $ rm subdir/unknown
966 971 $ hg status
967 972
968 973 Removing a node from the dirstate resets the cache for its parent directory
969 974
970 975 $ hg forget subdir/a
971 976 $ hg debugdirstate --all --no-dates | grep '^ '
972 977 0 -1 set subdir
973 978 $ hg ci -qm '#1'
974 979 $ hg debugdirstate --all --no-dates | grep '^ '
975 980 0 -1 unset subdir
976 981 $ hg status
977 982 ? subdir/a
978 983
979 984 Changing the hgignore rules makes us recompute the status (and rewrite the dirstate).
980 985
981 986 $ rm subdir/a
982 987 $ mkdir another-subdir
983 988 $ touch another-subdir/something-else
984 989
985 990 $ cat > "$TESTTMP"/extra-hgignore <<EOF
986 991 > something-else
987 992 > EOF
988 993
989 994 $ hg status --config ui.ignore.global="$TESTTMP"/extra-hgignore
990 995 $ hg debugdirstate --all --no-dates | grep '^ '
991 996 0 -1 set subdir
992 997
993 998 $ hg status
994 999 ? another-subdir/something-else
995 1000
996 1001 One invocation of status is enough to populate the cache even if it's invalidated
997 1002 in the same run.
998 1003
999 1004 $ hg debugdirstate --all --no-dates | grep '^ '
1000 1005 0 -1 set subdir
1001 1006
1002 1007 #endif
General Comments 0
You need to be logged in to leave comments. Login now