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