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