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