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