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