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