##// END OF EJS Templates
hg-core: refactor rev parsing in operations::status_rev_rev...
Mitchell Kember -
r53294:42bd36bb default
parent child Browse files
Show More
@@ -1,832 +1,827
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, relative_paths,
11 11 RelativePaths, Ui,
12 12 };
13 13 use crate::utils::path_utils::RelativizePaths;
14 14 use clap::Arg;
15 15 use format_bytes::format_bytes;
16 16 use hg::config::Config;
17 17 use hg::dirstate::entry::{has_exec_bit, TruncatedTimestamp};
18 18 use hg::dirstate::status::{
19 19 BadMatch, DirstateStatus, StatusError, StatusOptions, StatusPath,
20 20 };
21 21 use hg::errors::{HgError, IoResultExt};
22 22 use hg::filepatterns::{parse_pattern_args, PatternFileWarning};
23 23 use hg::lock::LockError;
24 24 use hg::matchers::{AlwaysMatcher, IntersectionMatcher};
25 25 use hg::repo::Repo;
26 26 use hg::revlog::manifest::Manifest;
27 27 use hg::revlog::options::{default_revlog_options, RevlogOpenOptions};
28 28 use hg::revlog::RevlogType;
29 29 use hg::utils::debug::debug_wait_for_file;
30 30 use hg::utils::files::{
31 31 get_bytes_from_os_str, get_bytes_from_os_string, get_path_from_bytes,
32 32 };
33 33 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
34 34 use hg::Revision;
35 35 use hg::{self, narrow, sparse};
36 36 use log::info;
37 37 use rayon::prelude::*;
38 38 use std::borrow::Cow;
39 39 use std::io;
40 40 use std::mem::take;
41 41 use std::path::PathBuf;
42 42
43 43 pub const HELP_TEXT: &str = "
44 44 Show changed files in the working directory
45 45
46 46 This is a pure Rust version of `hg status`.
47 47
48 48 Some options might be missing, check the list below.
49 49 ";
50 50
51 51 pub fn args() -> clap::Command {
52 52 clap::command!("status")
53 53 .alias("st")
54 54 .about(HELP_TEXT)
55 55 .arg(
56 56 Arg::new("file")
57 57 .value_parser(clap::value_parser!(std::ffi::OsString))
58 58 .help("show only these files")
59 59 .action(clap::ArgAction::Append),
60 60 )
61 61 .arg(
62 62 Arg::new("all")
63 63 .help("show status of all files")
64 64 .short('A')
65 65 .action(clap::ArgAction::SetTrue)
66 66 .long("all"),
67 67 )
68 68 .arg(
69 69 Arg::new("modified")
70 70 .help("show only modified files")
71 71 .short('m')
72 72 .action(clap::ArgAction::SetTrue)
73 73 .long("modified"),
74 74 )
75 75 .arg(
76 76 Arg::new("added")
77 77 .help("show only added files")
78 78 .short('a')
79 79 .action(clap::ArgAction::SetTrue)
80 80 .long("added"),
81 81 )
82 82 .arg(
83 83 Arg::new("removed")
84 84 .help("show only removed files")
85 85 .short('r')
86 86 .action(clap::ArgAction::SetTrue)
87 87 .long("removed"),
88 88 )
89 89 .arg(
90 90 Arg::new("clean")
91 91 .help("show only clean files")
92 92 .short('c')
93 93 .action(clap::ArgAction::SetTrue)
94 94 .long("clean"),
95 95 )
96 96 .arg(
97 97 Arg::new("deleted")
98 98 .help("show only deleted files")
99 99 .short('d')
100 100 .action(clap::ArgAction::SetTrue)
101 101 .long("deleted"),
102 102 )
103 103 .arg(
104 104 Arg::new("unknown")
105 105 .help("show only unknown (not tracked) files")
106 106 .short('u')
107 107 .action(clap::ArgAction::SetTrue)
108 108 .long("unknown"),
109 109 )
110 110 .arg(
111 111 Arg::new("ignored")
112 112 .help("show only ignored files")
113 113 .short('i')
114 114 .action(clap::ArgAction::SetTrue)
115 115 .long("ignored"),
116 116 )
117 117 .arg(
118 118 Arg::new("copies")
119 119 .help("show source of copied files (DEFAULT: ui.statuscopies)")
120 120 .short('C')
121 121 .action(clap::ArgAction::SetTrue)
122 122 .long("copies"),
123 123 )
124 124 .arg(
125 125 Arg::new("no-copies")
126 126 .action(clap::ArgAction::SetTrue)
127 127 .long("no-copies")
128 128 .overrides_with("copies"),
129 129 )
130 130 .arg(
131 131 Arg::new("print0")
132 132 .help("end filenames with NUL, for use with xargs")
133 133 .short('0')
134 134 .action(clap::ArgAction::SetTrue)
135 135 .long("print0"),
136 136 )
137 137 .arg(
138 138 Arg::new("no-status")
139 139 .help("hide status prefix")
140 140 .short('n')
141 141 .action(clap::ArgAction::SetTrue)
142 142 .long("no-status"),
143 143 )
144 144 .arg(
145 145 Arg::new("verbose")
146 146 .help("enable additional output")
147 147 .short('v')
148 148 .action(clap::ArgAction::SetTrue)
149 149 .long("verbose"),
150 150 )
151 151 .arg(
152 152 Arg::new("rev")
153 153 .help("show difference from/to revision")
154 154 .long("rev")
155 155 .num_args(1)
156 156 .action(clap::ArgAction::Append)
157 157 .value_name("REV"),
158 158 )
159 159 }
160 160
161 161 fn parse_revpair(
162 162 repo: &Repo,
163 163 revs: Option<Vec<String>>,
164 164 ) -> Result<Option<(Revision, Revision)>, CommandError> {
165 let revs = match revs {
166 None => return Ok(None),
167 Some(revs) => revs,
165 let Some(revs) = revs else {
166 return Ok(None);
168 167 };
169 if revs.is_empty() {
170 return Ok(None);
168 match revs.as_slice() {
169 [] => Ok(None),
170 [rev1, rev2] => Ok(Some((
171 hg::revset::resolve_single(rev1, repo)?,
172 hg::revset::resolve_single(rev2, repo)?,
173 ))),
174 _ => Err(CommandError::unsupported("expected 0 or 2 --rev flags")),
171 175 }
172 if revs.len() != 2 {
173 return Err(CommandError::unsupported("expected 0 or 2 --rev flags"));
174 }
175
176 let rev1 = &revs[0];
177 let rev2 = &revs[1];
178 let rev1 = hg::revset::resolve_single(rev1, repo)?;
179 let rev2 = hg::revset::resolve_single(rev2, repo)?;
180 Ok(Some((rev1, rev2)))
181 176 }
182 177
183 178 /// Pure data type allowing the caller to specify file states to display
184 179 #[derive(Copy, Clone, Debug)]
185 180 pub struct DisplayStates {
186 181 pub modified: bool,
187 182 pub added: bool,
188 183 pub removed: bool,
189 184 pub clean: bool,
190 185 pub deleted: bool,
191 186 pub unknown: bool,
192 187 pub ignored: bool,
193 188 }
194 189
195 190 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
196 191 modified: true,
197 192 added: true,
198 193 removed: true,
199 194 clean: false,
200 195 deleted: true,
201 196 unknown: true,
202 197 ignored: false,
203 198 };
204 199
205 200 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
206 201 modified: true,
207 202 added: true,
208 203 removed: true,
209 204 clean: true,
210 205 deleted: true,
211 206 unknown: true,
212 207 ignored: true,
213 208 };
214 209
215 210 impl DisplayStates {
216 211 pub fn is_empty(&self) -> bool {
217 212 !(self.modified
218 213 || self.added
219 214 || self.removed
220 215 || self.clean
221 216 || self.deleted
222 217 || self.unknown
223 218 || self.ignored)
224 219 }
225 220 }
226 221
227 222 fn has_unfinished_merge(repo: &Repo) -> Result<bool, CommandError> {
228 223 Ok(repo.dirstate_parents()?.is_merge())
229 224 }
230 225
231 226 fn has_unfinished_state(repo: &Repo) -> Result<bool, CommandError> {
232 227 // These are all the known values for the [fname] argument of
233 228 // [addunfinished] function in [state.py]
234 229 let known_state_files: &[&str] = &[
235 230 "bisect.state",
236 231 "graftstate",
237 232 "histedit-state",
238 233 "rebasestate",
239 234 "shelvedstate",
240 235 "transplant/journal",
241 236 "updatestate",
242 237 ];
243 238 if has_unfinished_merge(repo)? {
244 239 return Ok(true);
245 240 };
246 241 for f in known_state_files {
247 242 if repo.hg_vfs().join(f).exists() {
248 243 return Ok(true);
249 244 }
250 245 }
251 246 Ok(false)
252 247 }
253 248
254 249 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
255 250 // TODO: lift these limitations
256 251 if invocation
257 252 .config
258 253 .get(b"commands", b"status.terse")
259 254 .is_some()
260 255 {
261 256 return Err(CommandError::unsupported(
262 257 "status.terse is not yet supported with rhg status",
263 258 ));
264 259 }
265 260
266 261 let ui = invocation.ui;
267 262 let config = invocation.config;
268 263 let args = invocation.subcommand_args;
269 264
270 265 let revs = args.get_many::<String>("rev");
271 266 let print0 = args.get_flag("print0");
272 267 let verbose = args.get_flag("verbose")
273 268 || config.get_bool(b"ui", b"verbose")?
274 269 || config.get_bool(b"commands", b"status.verbose")?;
275 270 let verbose = verbose && !print0;
276 271
277 272 let all = args.get_flag("all");
278 273 let display_states = if all {
279 274 // TODO when implementing `--quiet`: it excludes clean files
280 275 // from `--all`
281 276 ALL_DISPLAY_STATES
282 277 } else {
283 278 let requested = DisplayStates {
284 279 modified: args.get_flag("modified"),
285 280 added: args.get_flag("added"),
286 281 removed: args.get_flag("removed"),
287 282 clean: args.get_flag("clean"),
288 283 deleted: args.get_flag("deleted"),
289 284 unknown: args.get_flag("unknown"),
290 285 ignored: args.get_flag("ignored"),
291 286 };
292 287 if requested.is_empty() {
293 288 DEFAULT_DISPLAY_STATES
294 289 } else {
295 290 requested
296 291 }
297 292 };
298 293 let no_status = args.get_flag("no-status");
299 294 let list_copies = if args.get_flag("copies") {
300 295 true
301 296 } else if args.get_flag("no-copies") {
302 297 false
303 298 } else {
304 299 config.get_bool(b"ui", b"statuscopies")?
305 300 };
306 301 let list_copies = (list_copies || all) && !no_status;
307 302
308 303 let repo = invocation.repo?;
309 304 let revpair = parse_revpair(repo, revs.map(|i| i.cloned().collect()))?;
310 305
311 306 if verbose && has_unfinished_state(repo)? {
312 307 return Err(CommandError::unsupported(
313 308 "verbose status output is not supported by rhg (and is needed because we're in an unfinished operation)",
314 309 ));
315 310 }
316 311
317 312 let mut dmap = repo.dirstate_map_mut()?;
318 313
319 314 let check_exec = hg::checkexec::check_exec(repo.working_directory_path());
320 315
321 316 let options = StatusOptions {
322 317 check_exec,
323 318 list_clean: display_states.clean,
324 319 list_unknown: display_states.unknown,
325 320 list_ignored: display_states.ignored,
326 321 list_copies,
327 322 collect_traversed_dirs: false,
328 323 };
329 324
330 325 type StatusResult<'a> =
331 326 Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
332 327
333 328 let relative_status = config
334 329 .get_option(b"commands", b"status.relative")?
335 330 .expect("commands.status.relative should have a default value");
336 331
337 332 let relativize_paths = relative_status || {
338 333 // See in Python code with `getuipathfn` usage in `commands.py`.
339 334 let legacy_relative_behavior = args.contains_id("file");
340 335 match relative_paths(invocation.config)? {
341 336 RelativePaths::Legacy => legacy_relative_behavior,
342 337 RelativePaths::Bool(v) => v,
343 338 }
344 339 };
345 340
346 341 let mut output = DisplayStatusPaths {
347 342 ui,
348 343 no_status,
349 344 relativize: if relativize_paths {
350 345 Some(RelativizePaths::new(repo)?)
351 346 } else {
352 347 None
353 348 },
354 349 print0,
355 350 };
356 351
357 352 let after_status = |res: StatusResult| -> Result<_, CommandError> {
358 353 let (mut ds_status, pattern_warnings) = res?;
359 354 for warning in pattern_warnings {
360 355 ui.write_stderr(&format_pattern_file_warning(&warning, repo))?;
361 356 }
362 357
363 358 for (path, error) in take(&mut ds_status.bad) {
364 359 let error = match error {
365 360 BadMatch::OsError(code) => {
366 361 std::io::Error::from_raw_os_error(code).to_string()
367 362 }
368 363 BadMatch::BadType(ty) => {
369 364 format!("unsupported file type (type is {})", ty)
370 365 }
371 366 };
372 367 ui.write_stderr(&format_bytes!(
373 368 b"{}: {}\n",
374 369 path.as_bytes(),
375 370 error.as_bytes()
376 371 ))?
377 372 }
378 373 if !ds_status.unsure.is_empty() {
379 374 info!(
380 375 "Files to be rechecked by retrieval from filelog: {:?}",
381 376 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
382 377 );
383 378 }
384 379 let mut fixup = Vec::new();
385 380 if !ds_status.unsure.is_empty()
386 381 && (display_states.modified || display_states.clean)
387 382 {
388 383 let p1 = repo.dirstate_parents()?.p1;
389 384 let manifest = repo.manifest_for_node(p1)?;
390 385 let working_directory_vfs = repo.working_directory_vfs();
391 386 let store_vfs = repo.store_vfs();
392 387 let filelog_open_options = default_revlog_options(
393 388 repo.config(),
394 389 repo.requirements(),
395 390 RevlogType::Filelog,
396 391 )?;
397 392 let res: Vec<_> = take(&mut ds_status.unsure)
398 393 .into_par_iter()
399 394 .map(|to_check| {
400 395 // The compiler seems to get a bit confused with complex
401 396 // inference when using a parallel iterator + map
402 397 // + map_err + collect, so let's just inline some of the
403 398 // logic.
404 399 match unsure_is_modified(
405 400 &working_directory_vfs,
406 401 &store_vfs,
407 402 check_exec,
408 403 &manifest,
409 404 &to_check.path,
410 405 filelog_open_options,
411 406 ) {
412 407 Err(HgError::IoError { .. }) => {
413 408 // IO errors most likely stem from the file being
414 409 // deleted even though we know it's in the
415 410 // dirstate.
416 411 Ok((to_check, UnsureOutcome::Deleted))
417 412 }
418 413 Ok(outcome) => Ok((to_check, outcome)),
419 414 Err(e) => Err(e),
420 415 }
421 416 })
422 417 .collect::<Result<_, _>>()?;
423 418 for (status_path, outcome) in res.into_iter() {
424 419 match outcome {
425 420 UnsureOutcome::Clean => {
426 421 if display_states.clean {
427 422 ds_status.clean.push(status_path.clone());
428 423 }
429 424 fixup.push(status_path.path.into_owned())
430 425 }
431 426 UnsureOutcome::Modified => {
432 427 if display_states.modified {
433 428 ds_status.modified.push(status_path);
434 429 }
435 430 }
436 431 UnsureOutcome::Deleted => {
437 432 if display_states.deleted {
438 433 ds_status.deleted.push(status_path);
439 434 }
440 435 }
441 436 }
442 437 }
443 438 }
444 439
445 440 let dirstate_write_needed = ds_status.dirty;
446 441 let filesystem_time_at_status_start =
447 442 ds_status.filesystem_time_at_status_start;
448 443
449 444 output.output(display_states, ds_status)?;
450 445
451 446 Ok((
452 447 fixup,
453 448 dirstate_write_needed,
454 449 filesystem_time_at_status_start,
455 450 ))
456 451 };
457 452
458 453 let (narrow_matcher, narrow_warnings) = narrow::matcher(repo)?;
459 454 let (sparse_matcher, sparse_warnings) = sparse::matcher(repo)?;
460 455 let matcher = match (repo.has_narrow(), repo.has_sparse()) {
461 456 (true, true) => {
462 457 Box::new(IntersectionMatcher::new(narrow_matcher, sparse_matcher))
463 458 }
464 459 (true, false) => narrow_matcher,
465 460 (false, true) => sparse_matcher,
466 461 (false, false) => Box::new(AlwaysMatcher),
467 462 };
468 463 let matcher = match args.get_many::<std::ffi::OsString>("file") {
469 464 None => matcher,
470 465 Some(files) => {
471 466 let patterns: Vec<Vec<u8>> = files
472 467 .filter(|s| !s.is_empty())
473 468 .map(get_bytes_from_os_str)
474 469 .collect();
475 470 for file in &patterns {
476 471 if file.starts_with(b"set:") {
477 472 return Err(CommandError::unsupported("fileset"));
478 473 }
479 474 }
480 475 let cwd = hg::utils::current_dir()?;
481 476 let root = repo.working_directory_path();
482 477 let ignore_patterns = parse_pattern_args(patterns, &cwd, root)?;
483 478 let files_matcher =
484 479 hg::matchers::PatternMatcher::new(ignore_patterns)?;
485 480 Box::new(IntersectionMatcher::new(
486 481 Box::new(files_matcher),
487 482 matcher,
488 483 ))
489 484 }
490 485 };
491 486 print_narrow_sparse_warnings(
492 487 &narrow_warnings,
493 488 &sparse_warnings,
494 489 ui,
495 490 repo,
496 491 )?;
497 492
498 493 if let Some((rev1, rev2)) = revpair {
499 494 let mut ds_status = DirstateStatus::default();
500 495 if list_copies {
501 496 return Err(CommandError::unsupported(
502 497 "status --rev --rev with copy information is not implemented yet",
503 498 ));
504 499 }
505 500
506 501 let stat = hg::operations::status_rev_rev_no_copies(
507 502 repo, rev1, rev2, matcher,
508 503 )?;
509 504 for entry in stat.iter() {
510 505 let (path, status) = entry?;
511 506 let path = StatusPath {
512 507 path: Cow::Borrowed(path),
513 508 copy_source: None,
514 509 };
515 510 match status {
516 511 hg::operations::DiffStatus::Removed => {
517 512 if display_states.removed {
518 513 ds_status.removed.push(path)
519 514 }
520 515 }
521 516 hg::operations::DiffStatus::Added => {
522 517 if display_states.added {
523 518 ds_status.added.push(path)
524 519 }
525 520 }
526 521 hg::operations::DiffStatus::Modified => {
527 522 if display_states.modified {
528 523 ds_status.modified.push(path)
529 524 }
530 525 }
531 526 hg::operations::DiffStatus::Matching => {
532 527 if display_states.clean {
533 528 ds_status.clean.push(path)
534 529 }
535 530 }
536 531 }
537 532 }
538 533 output.output(display_states, ds_status)?;
539 534 return Ok(());
540 535 }
541 536
542 537 let (fixup, mut dirstate_write_needed, filesystem_time_at_status_start) =
543 538 dmap.with_status(
544 539 matcher.as_ref(),
545 540 repo.working_directory_path().to_owned(),
546 541 ignore_files(repo, config),
547 542 options,
548 543 after_status,
549 544 )?;
550 545
551 546 // Development config option to test write races
552 547 if let Err(e) =
553 548 debug_wait_for_file(config, "status.pre-dirstate-write-file")
554 549 {
555 550 ui.write_stderr(e.as_bytes()).ok();
556 551 }
557 552
558 553 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
559 554 && !dirstate_write_needed
560 555 {
561 556 // Nothing to update
562 557 return Ok(());
563 558 }
564 559
565 560 // Update the dirstate on disk if we can
566 561 let with_lock_result =
567 562 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
568 563 if let Some(mtime_boundary) = filesystem_time_at_status_start {
569 564 for hg_path in fixup {
570 565 use std::os::unix::fs::MetadataExt;
571 566 let fs_path = hg_path_to_path_buf(&hg_path)
572 567 .expect("HgPath conversion");
573 568 // Specifically do not reuse `fs_metadata` from
574 569 // `unsure_is_clean` which was needed before reading
575 570 // contents. Here we access metadata again after reading
576 571 // content, in case it changed in the meantime.
577 572 let metadata_res = repo
578 573 .working_directory_vfs()
579 574 .symlink_metadata(&fs_path);
580 575 let fs_metadata = match metadata_res {
581 576 Ok(meta) => meta,
582 577 Err(err) => match err {
583 578 HgError::IoError { .. } => {
584 579 // The file has probably been deleted. In any
585 580 // case, it was in the dirstate before, so
586 581 // let's ignore the error.
587 582 continue;
588 583 }
589 584 _ => return Err(err.into()),
590 585 },
591 586 };
592 587 if let Some(mtime) =
593 588 TruncatedTimestamp::for_reliable_mtime_of(
594 589 &fs_metadata,
595 590 &mtime_boundary,
596 591 )
597 592 .when_reading_file(&fs_path)?
598 593 {
599 594 let mode = fs_metadata.mode();
600 595 let size = fs_metadata.len();
601 596 dmap.set_clean(&hg_path, mode, size as u32, mtime)?;
602 597 dirstate_write_needed = true
603 598 }
604 599 }
605 600 }
606 601 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
607 602 if dirstate_write_needed {
608 603 repo.write_dirstate()?
609 604 }
610 605 Ok(())
611 606 });
612 607 match with_lock_result {
613 608 Ok(closure_result) => closure_result?,
614 609 Err(LockError::AlreadyHeld) => {
615 610 // Not updating the dirstate is not ideal but not critical:
616 611 // don’t keep our caller waiting until some other Mercurial
617 612 // process releases the lock.
618 613 log::info!("not writing dirstate from `status`: lock is held")
619 614 }
620 615 Err(LockError::Other(HgError::IoError { error, .. }))
621 616 if error.kind() == io::ErrorKind::PermissionDenied
622 617 || match error.raw_os_error() {
623 618 None => false,
624 619 Some(errno) => libc::EROFS == errno,
625 620 } =>
626 621 {
627 622 // `hg status` on a read-only repository is fine
628 623 }
629 624 Err(LockError::Other(error)) => {
630 625 // Report other I/O errors
631 626 Err(error)?
632 627 }
633 628 }
634 629 Ok(())
635 630 }
636 631
637 632 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
638 633 let mut ignore_files = Vec::new();
639 634 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
640 635 if repo_ignore.exists() {
641 636 ignore_files.push(repo_ignore)
642 637 }
643 638 for (key, value) in config.iter_section(b"ui") {
644 639 if key == b"ignore" || key.starts_with(b"ignore.") {
645 640 let path = get_path_from_bytes(value);
646 641 let path = shellexpand::path::full_with_context_no_errors(
647 642 path,
648 643 home::home_dir,
649 644 |s| std::env::var(s).ok(),
650 645 );
651 646 let joined = repo.working_directory_path().join(path);
652 647 ignore_files.push(joined);
653 648 }
654 649 }
655 650 ignore_files
656 651 }
657 652
658 653 struct DisplayStatusPaths<'a> {
659 654 ui: &'a Ui,
660 655 no_status: bool,
661 656 relativize: Option<RelativizePaths>,
662 657 print0: bool,
663 658 }
664 659
665 660 impl DisplayStatusPaths<'_> {
666 661 // Probably more elegant to use a Deref or Borrow trait rather than
667 662 // harcode HgPathBuf, but probably not really useful at this point
668 663 fn display(
669 664 &self,
670 665 status_prefix: &[u8],
671 666 label: &'static str,
672 667 mut paths: Vec<StatusPath<'_>>,
673 668 ) -> Result<(), CommandError> {
674 669 paths.sort_unstable();
675 670 // TODO: get the stdout lock once for the whole loop
676 671 // instead of in each write
677 672 for StatusPath { path, copy_source } in paths {
678 673 let relative_path;
679 674 let relative_source;
680 675 let (path, copy_source) = if let Some(relativize) =
681 676 &self.relativize
682 677 {
683 678 relative_path = relativize.relativize(&path);
684 679 relative_source =
685 680 copy_source.as_ref().map(|s| relativize.relativize(s));
686 681 (&*relative_path, relative_source.as_deref())
687 682 } else {
688 683 (path.as_bytes(), copy_source.as_ref().map(|s| s.as_bytes()))
689 684 };
690 685 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
691 686 // in order to stream to stdout instead of allocating an
692 687 // itermediate `Vec<u8>`.
693 688 if !self.no_status {
694 689 self.ui.write_stdout_labelled(status_prefix, label)?
695 690 }
696 691 let linebreak = if self.print0 { b"\x00" } else { b"\n" };
697 692 self.ui.write_stdout_labelled(
698 693 &format_bytes!(b"{}{}", path, linebreak),
699 694 label,
700 695 )?;
701 696 if let Some(source) = copy_source {
702 697 let label = "status.copied";
703 698 self.ui.write_stdout_labelled(
704 699 &format_bytes!(b" {}{}", source, linebreak),
705 700 label,
706 701 )?
707 702 }
708 703 }
709 704 Ok(())
710 705 }
711 706
712 707 fn output(
713 708 &mut self,
714 709 display_states: DisplayStates,
715 710 ds_status: DirstateStatus,
716 711 ) -> Result<(), CommandError> {
717 712 if display_states.modified {
718 713 self.display(b"M ", "status.modified", ds_status.modified)?;
719 714 }
720 715 if display_states.added {
721 716 self.display(b"A ", "status.added", ds_status.added)?;
722 717 }
723 718 if display_states.removed {
724 719 self.display(b"R ", "status.removed", ds_status.removed)?;
725 720 }
726 721 if display_states.deleted {
727 722 self.display(b"! ", "status.deleted", ds_status.deleted)?;
728 723 }
729 724 if display_states.unknown {
730 725 self.display(b"? ", "status.unknown", ds_status.unknown)?;
731 726 }
732 727 if display_states.ignored {
733 728 self.display(b"I ", "status.ignored", ds_status.ignored)?;
734 729 }
735 730 if display_states.clean {
736 731 self.display(b"C ", "status.clean", ds_status.clean)?;
737 732 }
738 733 Ok(())
739 734 }
740 735 }
741 736
742 737 /// Outcome of the additional check for an ambiguous tracked file
743 738 enum UnsureOutcome {
744 739 /// The file is actually clean
745 740 Clean,
746 741 /// The file has been modified
747 742 Modified,
748 743 /// The file was deleted on disk (or became another type of fs entry)
749 744 Deleted,
750 745 }
751 746
752 747 /// Check if a file is modified by comparing actual repo store and file system.
753 748 ///
754 749 /// This meant to be used for those that the dirstate cannot resolve, due
755 750 /// to time resolution limits.
756 751 fn unsure_is_modified(
757 752 working_directory_vfs: &hg::vfs::VfsImpl,
758 753 store_vfs: &hg::vfs::VfsImpl,
759 754 check_exec: bool,
760 755 manifest: &Manifest,
761 756 hg_path: &HgPath,
762 757 revlog_open_options: RevlogOpenOptions,
763 758 ) -> Result<UnsureOutcome, HgError> {
764 759 let vfs = working_directory_vfs;
765 760 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
766 761 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
767 762 let is_symlink = fs_metadata.file_type().is_symlink();
768 763
769 764 let entry = manifest
770 765 .find_by_path(hg_path)?
771 766 .expect("ambgious file not in p1");
772 767
773 768 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
774 769 // dirstate
775 770 let fs_flags = if is_symlink {
776 771 Some(b'l')
777 772 } else if check_exec && has_exec_bit(&fs_metadata) {
778 773 Some(b'x')
779 774 } else {
780 775 None
781 776 };
782 777
783 778 let entry_flags = if check_exec {
784 779 entry.flags
785 780 } else if entry.flags.map(|f| f.into()) == Some(b'x') {
786 781 None
787 782 } else {
788 783 entry.flags
789 784 };
790 785
791 786 if entry_flags.map(|f| f.into()) != fs_flags {
792 787 return Ok(UnsureOutcome::Modified);
793 788 }
794 789 let filelog = hg::revlog::filelog::Filelog::open_vfs(
795 790 store_vfs,
796 791 hg_path,
797 792 revlog_open_options,
798 793 )?;
799 794 let fs_len = fs_metadata.len();
800 795 let file_node = entry.node_id()?;
801 796 let filelog_entry = filelog.entry_for_node(file_node).map_err(|_| {
802 797 HgError::corrupted(format!(
803 798 "filelog {:?} missing node {:?} from manifest",
804 799 hg_path, file_node
805 800 ))
806 801 })?;
807 802 if filelog_entry.file_data_len_not_equal_to(fs_len) {
808 803 // No need to read file contents:
809 804 // it cannot be equal if it has a different length.
810 805 return Ok(UnsureOutcome::Modified);
811 806 }
812 807
813 808 let p1_filelog_data = filelog_entry.data()?;
814 809 let p1_contents = p1_filelog_data.file_data()?;
815 810 if p1_contents.len() as u64 != fs_len {
816 811 // No need to read file contents:
817 812 // it cannot be equal if it has a different length.
818 813 return Ok(UnsureOutcome::Modified);
819 814 }
820 815
821 816 let fs_contents = if is_symlink {
822 817 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
823 818 } else {
824 819 vfs.read(fs_path)?
825 820 };
826 821
827 822 Ok(if p1_contents != &*fs_contents {
828 823 UnsureOutcome::Modified
829 824 } else {
830 825 UnsureOutcome::Clean
831 826 })
832 827 }
General Comments 0
You need to be logged in to leave comments. Login now