##// END OF EJS Templates
rhg: refactor display_status_paths with a struct for common arguments...
Simon Sapin -
r49283:2afaa014 default
parent child Browse files
Show More
@@ -1,498 +1,465 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::relativize_paths;
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::TruncatedTimestamp;
17 17 use hg::dirstate::RANGE_MASK_31BIT;
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_path_from_bytes;
25 25 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
26 26 use hg::{HgPathCow, StatusOptions};
27 27 use log::{info, warn};
28 28 use std::io;
29 29 use std::path::PathBuf;
30 30
31 31 pub const HELP_TEXT: &str = "
32 32 Show changed files in the working directory
33 33
34 34 This is a pure Rust version of `hg status`.
35 35
36 36 Some options might be missing, check the list below.
37 37 ";
38 38
39 39 pub fn args() -> clap::App<'static, 'static> {
40 40 SubCommand::with_name("status")
41 41 .alias("st")
42 42 .about(HELP_TEXT)
43 43 .arg(
44 44 Arg::with_name("all")
45 45 .help("show status of all files")
46 46 .short("-A")
47 47 .long("--all"),
48 48 )
49 49 .arg(
50 50 Arg::with_name("modified")
51 51 .help("show only modified files")
52 52 .short("-m")
53 53 .long("--modified"),
54 54 )
55 55 .arg(
56 56 Arg::with_name("added")
57 57 .help("show only added files")
58 58 .short("-a")
59 59 .long("--added"),
60 60 )
61 61 .arg(
62 62 Arg::with_name("removed")
63 63 .help("show only removed files")
64 64 .short("-r")
65 65 .long("--removed"),
66 66 )
67 67 .arg(
68 68 Arg::with_name("clean")
69 69 .help("show only clean files")
70 70 .short("-c")
71 71 .long("--clean"),
72 72 )
73 73 .arg(
74 74 Arg::with_name("deleted")
75 75 .help("show only deleted files")
76 76 .short("-d")
77 77 .long("--deleted"),
78 78 )
79 79 .arg(
80 80 Arg::with_name("unknown")
81 81 .help("show only unknown (not tracked) files")
82 82 .short("-u")
83 83 .long("--unknown"),
84 84 )
85 85 .arg(
86 86 Arg::with_name("ignored")
87 87 .help("show only ignored files")
88 88 .short("-i")
89 89 .long("--ignored"),
90 90 )
91 91 .arg(
92 92 Arg::with_name("no-status")
93 93 .help("hide status prefix")
94 94 .short("-n")
95 95 .long("--no-status"),
96 96 )
97 97 }
98 98
99 99 /// Pure data type allowing the caller to specify file states to display
100 100 #[derive(Copy, Clone, Debug)]
101 101 pub struct DisplayStates {
102 102 pub modified: bool,
103 103 pub added: bool,
104 104 pub removed: bool,
105 105 pub clean: bool,
106 106 pub deleted: bool,
107 107 pub unknown: bool,
108 108 pub ignored: bool,
109 109 }
110 110
111 111 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
112 112 modified: true,
113 113 added: true,
114 114 removed: true,
115 115 clean: false,
116 116 deleted: true,
117 117 unknown: true,
118 118 ignored: false,
119 119 };
120 120
121 121 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
122 122 modified: true,
123 123 added: true,
124 124 removed: true,
125 125 clean: true,
126 126 deleted: true,
127 127 unknown: true,
128 128 ignored: true,
129 129 };
130 130
131 131 impl DisplayStates {
132 132 pub fn is_empty(&self) -> bool {
133 133 !(self.modified
134 134 || self.added
135 135 || self.removed
136 136 || self.clean
137 137 || self.deleted
138 138 || self.unknown
139 139 || self.ignored)
140 140 }
141 141 }
142 142
143 143 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
144 144 let status_enabled_default = false;
145 145 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
146 146 if !status_enabled.unwrap_or(status_enabled_default) {
147 147 return Err(CommandError::unsupported(
148 148 "status is experimental in rhg (enable it with 'rhg.status = true' \
149 149 or enable fallback with 'rhg.on-unsupported = fallback')"
150 150 ));
151 151 }
152 152
153 153 // TODO: lift these limitations
154 154 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
155 155 return Err(CommandError::unsupported(
156 156 "ui.tweakdefaults is not yet supported with rhg status",
157 157 ));
158 158 }
159 159 if invocation.config.get_bool(b"ui", b"statuscopies")? {
160 160 return Err(CommandError::unsupported(
161 161 "ui.statuscopies is not yet supported with rhg status",
162 162 ));
163 163 }
164 164 if invocation
165 165 .config
166 166 .get(b"commands", b"status.terse")
167 167 .is_some()
168 168 {
169 169 return Err(CommandError::unsupported(
170 170 "status.terse is not yet supported with rhg status",
171 171 ));
172 172 }
173 173
174 174 let ui = invocation.ui;
175 175 let config = invocation.config;
176 176 let args = invocation.subcommand_args;
177 177 let display_states = if args.is_present("all") {
178 178 // TODO when implementing `--quiet`: it excludes clean files
179 179 // from `--all`
180 180 ALL_DISPLAY_STATES
181 181 } else {
182 182 let requested = DisplayStates {
183 183 modified: args.is_present("modified"),
184 184 added: args.is_present("added"),
185 185 removed: args.is_present("removed"),
186 186 clean: args.is_present("clean"),
187 187 deleted: args.is_present("deleted"),
188 188 unknown: args.is_present("unknown"),
189 189 ignored: args.is_present("ignored"),
190 190 };
191 191 if requested.is_empty() {
192 192 DEFAULT_DISPLAY_STATES
193 193 } else {
194 194 requested
195 195 }
196 196 };
197 197 let no_status = args.is_present("no-status");
198 198
199 199 let repo = invocation.repo?;
200 200
201 201 if repo.has_sparse() || repo.has_narrow() {
202 202 return Err(CommandError::unsupported(
203 203 "rhg status is not supported for sparse checkouts or narrow clones yet"
204 204 ));
205 205 }
206 206
207 207 let mut dmap = repo.dirstate_map_mut()?;
208 208
209 209 let options = StatusOptions {
210 210 // we're currently supporting file systems with exec flags only
211 211 // anyway
212 212 check_exec: true,
213 213 list_clean: display_states.clean,
214 214 list_unknown: display_states.unknown,
215 215 list_ignored: display_states.ignored,
216 216 collect_traversed_dirs: false,
217 217 };
218 218 let (mut ds_status, pattern_warnings) = dmap.status(
219 219 &AlwaysMatcher,
220 220 repo.working_directory_path().to_owned(),
221 221 ignore_files(repo, config),
222 222 options,
223 223 )?;
224 224 if !pattern_warnings.is_empty() {
225 225 warn!("Pattern warnings: {:?}", &pattern_warnings);
226 226 }
227 227
228 228 if !ds_status.bad.is_empty() {
229 229 warn!("Bad matches {:?}", &(ds_status.bad))
230 230 }
231 231 if !ds_status.unsure.is_empty() {
232 232 info!(
233 233 "Files to be rechecked by retrieval from filelog: {:?}",
234 234 &ds_status.unsure
235 235 );
236 236 }
237 237 let mut fixup = Vec::new();
238 238 if !ds_status.unsure.is_empty()
239 239 && (display_states.modified || display_states.clean)
240 240 {
241 241 let p1 = repo.dirstate_parents()?.p1;
242 242 let manifest = repo.manifest_for_node(p1).map_err(|e| {
243 243 CommandError::from((e, &*format!("{:x}", p1.short())))
244 244 })?;
245 245 for to_check in ds_status.unsure {
246 246 if unsure_is_modified(repo, &manifest, &to_check)? {
247 247 if display_states.modified {
248 248 ds_status.modified.push(to_check);
249 249 }
250 250 } else {
251 251 if display_states.clean {
252 252 ds_status.clean.push(to_check.clone());
253 253 }
254 254 fixup.push(to_check.into_owned())
255 255 }
256 256 }
257 257 }
258 let relative_paths = (!ui.plain())
259 && config
260 .get_option(b"commands", b"status.relative")?
261 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
262 let output = DisplayStatusPaths {
263 ui,
264 repo,
265 no_status,
266 relative_paths,
267 };
258 268 if display_states.modified {
259 display_status_paths(
260 ui,
261 repo,
262 config,
263 no_status,
264 &mut ds_status.modified,
265 b"M",
266 )?;
269 output.display(b"M", ds_status.modified)?;
267 270 }
268 271 if display_states.added {
269 display_status_paths(
270 ui,
271 repo,
272 config,
273 no_status,
274 &mut ds_status.added,
275 b"A",
276 )?;
272 output.display(b"A", ds_status.added)?;
277 273 }
278 274 if display_states.removed {
279 display_status_paths(
280 ui,
281 repo,
282 config,
283 no_status,
284 &mut ds_status.removed,
285 b"R",
286 )?;
275 output.display(b"R", ds_status.removed)?;
287 276 }
288 277 if display_states.deleted {
289 display_status_paths(
290 ui,
291 repo,
292 config,
293 no_status,
294 &mut ds_status.deleted,
295 b"!",
296 )?;
278 output.display(b"!", ds_status.deleted)?;
297 279 }
298 280 if display_states.unknown {
299 display_status_paths(
300 ui,
301 repo,
302 config,
303 no_status,
304 &mut ds_status.unknown,
305 b"?",
306 )?;
281 output.display(b"?", ds_status.unknown)?;
307 282 }
308 283 if display_states.ignored {
309 display_status_paths(
310 ui,
311 repo,
312 config,
313 no_status,
314 &mut ds_status.ignored,
315 b"I",
316 )?;
284 output.display(b"I", ds_status.ignored)?;
317 285 }
318 286 if display_states.clean {
319 display_status_paths(
320 ui,
321 repo,
322 config,
323 no_status,
324 &mut ds_status.clean,
325 b"C",
326 )?;
287 output.display(b"C", ds_status.clean)?;
327 288 }
328 289
329 290 let mut dirstate_write_needed = ds_status.dirty;
330 291 let filesystem_time_at_status_start = ds_status
331 292 .filesystem_time_at_status_start
332 293 .map(TruncatedTimestamp::from);
333 294
334 295 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
335 296 && !dirstate_write_needed
336 297 {
337 298 // Nothing to update
338 299 return Ok(());
339 300 }
340 301
341 302 // Update the dirstate on disk if we can
342 303 let with_lock_result =
343 304 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
344 305 if let Some(mtime_boundary) = filesystem_time_at_status_start {
345 306 for hg_path in fixup {
346 307 use std::os::unix::fs::MetadataExt;
347 308 let fs_path = hg_path_to_path_buf(&hg_path)
348 309 .expect("HgPath conversion");
349 310 // Specifically do not reuse `fs_metadata` from
350 311 // `unsure_is_clean` which was needed before reading
351 312 // contents. Here we access metadata again after reading
352 313 // content, in case it changed in the meantime.
353 314 let fs_metadata = repo
354 315 .working_directory_vfs()
355 316 .symlink_metadata(&fs_path)?;
356 317 if let Some(mtime) =
357 318 TruncatedTimestamp::for_reliable_mtime_of(
358 319 &fs_metadata,
359 320 &mtime_boundary,
360 321 )
361 322 .when_reading_file(&fs_path)?
362 323 {
363 324 let mode = fs_metadata.mode();
364 325 let size = fs_metadata.len() as u32 & RANGE_MASK_31BIT;
365 326 let mut entry = dmap
366 327 .get(&hg_path)?
367 328 .expect("ambiguous file not in dirstate");
368 329 entry.set_clean(mode, size, mtime);
369 330 dmap.add_file(&hg_path, entry)?;
370 331 dirstate_write_needed = true
371 332 }
372 333 }
373 334 }
374 335 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
375 336 if dirstate_write_needed {
376 337 repo.write_dirstate()?
377 338 }
378 339 Ok(())
379 340 });
380 341 match with_lock_result {
381 342 Ok(closure_result) => closure_result?,
382 343 Err(LockError::AlreadyHeld) => {
383 344 // Not updating the dirstate is not ideal but not critical:
384 345 // don’t keep our caller waiting until some other Mercurial
385 346 // process releases the lock.
386 347 }
387 348 Err(LockError::Other(HgError::IoError { error, .. }))
388 349 if error.kind() == io::ErrorKind::PermissionDenied =>
389 350 {
390 351 // `hg status` on a read-only repository is fine
391 352 }
392 353 Err(LockError::Other(error)) => {
393 354 // Report other I/O errors
394 355 Err(error)?
395 356 }
396 357 }
397 358 Ok(())
398 359 }
399 360
400 361 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
401 362 let mut ignore_files = Vec::new();
402 363 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
403 364 if repo_ignore.exists() {
404 365 ignore_files.push(repo_ignore)
405 366 }
406 367 for (key, value) in config.iter_section(b"ui") {
407 368 if key == b"ignore" || key.starts_with(b"ignore.") {
408 369 let path = get_path_from_bytes(value);
409 370 // TODO:Β expand "~/" and environment variable here, like Python
410 371 // does with `os.path.expanduser` and `os.path.expandvars`
411 372
412 373 let joined = repo.working_directory_path().join(path);
413 374 ignore_files.push(joined);
414 375 }
415 376 }
416 377 ignore_files
417 378 }
418 379
419 // Probably more elegant to use a Deref or Borrow trait rather than
420 // harcode HgPathBuf, but probably not really useful at this point
421 fn display_status_paths(
422 ui: &Ui,
423 repo: &Repo,
424 config: &Config,
380 struct DisplayStatusPaths<'a> {
381 ui: &'a Ui,
382 repo: &'a Repo,
425 383 no_status: bool,
426 paths: &mut [HgPathCow],
427 status_prefix: &[u8],
428 ) -> Result<(), CommandError> {
429 paths.sort_unstable();
430 let mut relative: bool = config.get_bool(b"ui", b"relative-paths")?;
431 relative = config
432 .get_option(b"commands", b"status.relative")?
433 .unwrap_or(relative);
434 let print_path = |path: &[u8]| {
435 // TODO optim, probably lots of unneeded copies here, especially
436 // if out stream is buffered
437 if no_status {
438 ui.write_stdout(&format_bytes!(b"{}\n", path))
384 relative_paths: bool,
385 }
386
387 impl DisplayStatusPaths<'_> {
388 // Probably more elegant to use a Deref or Borrow trait rather than
389 // harcode HgPathBuf, but probably not really useful at this point
390 fn display(
391 &self,
392 status_prefix: &[u8],
393 mut paths: Vec<HgPathCow>,
394 ) -> Result<(), CommandError> {
395 paths.sort_unstable();
396 let print_path = |path: &[u8]| {
397 // TODO optim, probably lots of unneeded copies here, especially
398 // if out stream is buffered
399 if self.no_status {
400 self.ui.write_stdout(&format_bytes!(b"{}\n", path))
401 } else {
402 self.ui.write_stdout(&format_bytes!(
403 b"{} {}\n",
404 status_prefix,
405 path
406 ))
407 }
408 };
409
410 if self.relative_paths {
411 relativize_paths(self.repo, paths.iter().map(Ok), |path| {
412 print_path(&path)
413 })?;
439 414 } else {
440 ui.write_stdout(&format_bytes!(b"{} {}\n", status_prefix, path))
415 for path in paths {
416 print_path(path.as_bytes())?
417 }
441 418 }
442 };
443
444 if relative && !ui.plain() {
445 relativize_paths(repo, paths.iter().map(Ok), |path| {
446 print_path(&path)
447 })?;
448 } else {
449 for path in paths {
450 print_path(path.as_bytes())?
451 }
419 Ok(())
452 420 }
453 Ok(())
454 421 }
455 422
456 423 /// Check if a file is modified by comparing actual repo store and file system.
457 424 ///
458 425 /// This meant to be used for those that the dirstate cannot resolve, due
459 426 /// to time resolution limits.
460 427 fn unsure_is_modified(
461 428 repo: &Repo,
462 429 manifest: &Manifest,
463 430 hg_path: &HgPath,
464 431 ) -> Result<bool, HgError> {
465 432 let vfs = repo.working_directory_vfs();
466 433 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
467 434 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
468 435 let is_symlink = fs_metadata.file_type().is_symlink();
469 436 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
470 437 // dirstate
471 438 let fs_flags = if is_symlink {
472 439 Some(b'l')
473 440 } else if has_exec_bit(&fs_metadata) {
474 441 Some(b'x')
475 442 } else {
476 443 None
477 444 };
478 445
479 446 let entry = manifest
480 447 .find_file(hg_path)?
481 448 .expect("ambgious file not in p1");
482 449 if entry.flags != fs_flags {
483 450 return Ok(true);
484 451 }
485 452 let filelog = repo.filelog(hg_path)?;
486 453 let filelog_entry =
487 454 filelog.data_for_node(entry.node_id()?).map_err(|_| {
488 455 HgError::corrupted("filelog missing node from manifest")
489 456 })?;
490 457 let contents_in_p1 = filelog_entry.data()?;
491 458
492 459 let fs_contents = if is_symlink {
493 460 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
494 461 } else {
495 462 vfs.read(fs_path)?
496 463 };
497 464 Ok(contents_in_p1 != &*fs_contents)
498 465 }
General Comments 0
You need to be logged in to leave comments. Login now