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