##// END OF EJS Templates
rhg: Colorize `rhg status` output when appropriate...
Simon Sapin -
r49585:3e2b4bb2 default
parent child Browse files
Show More
@@ -1,539 +1,539 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_bytes_from_path;
26 26 use hg::utils::files::get_path_from_bytes;
27 27 use hg::utils::hg_path::{hg_path_to_path_buf, HgPath};
28 28 use hg::StatusOptions;
29 29 use log::info;
30 30 use std::io;
31 31 use std::path::PathBuf;
32 32
33 33 pub const HELP_TEXT: &str = "
34 34 Show changed files in the working directory
35 35
36 36 This is a pure Rust version of `hg status`.
37 37
38 38 Some options might be missing, check the list below.
39 39 ";
40 40
41 41 pub fn args() -> clap::App<'static, 'static> {
42 42 SubCommand::with_name("status")
43 43 .alias("st")
44 44 .about(HELP_TEXT)
45 45 .arg(
46 46 Arg::with_name("all")
47 47 .help("show status of all files")
48 48 .short("-A")
49 49 .long("--all"),
50 50 )
51 51 .arg(
52 52 Arg::with_name("modified")
53 53 .help("show only modified files")
54 54 .short("-m")
55 55 .long("--modified"),
56 56 )
57 57 .arg(
58 58 Arg::with_name("added")
59 59 .help("show only added files")
60 60 .short("-a")
61 61 .long("--added"),
62 62 )
63 63 .arg(
64 64 Arg::with_name("removed")
65 65 .help("show only removed files")
66 66 .short("-r")
67 67 .long("--removed"),
68 68 )
69 69 .arg(
70 70 Arg::with_name("clean")
71 71 .help("show only clean files")
72 72 .short("-c")
73 73 .long("--clean"),
74 74 )
75 75 .arg(
76 76 Arg::with_name("deleted")
77 77 .help("show only deleted files")
78 78 .short("-d")
79 79 .long("--deleted"),
80 80 )
81 81 .arg(
82 82 Arg::with_name("unknown")
83 83 .help("show only unknown (not tracked) files")
84 84 .short("-u")
85 85 .long("--unknown"),
86 86 )
87 87 .arg(
88 88 Arg::with_name("ignored")
89 89 .help("show only ignored files")
90 90 .short("-i")
91 91 .long("--ignored"),
92 92 )
93 93 .arg(
94 94 Arg::with_name("copies")
95 95 .help("show source of copied files (DEFAULT: ui.statuscopies)")
96 96 .short("-C")
97 97 .long("--copies"),
98 98 )
99 99 .arg(
100 100 Arg::with_name("no-status")
101 101 .help("hide status prefix")
102 102 .short("-n")
103 103 .long("--no-status"),
104 104 )
105 105 }
106 106
107 107 /// Pure data type allowing the caller to specify file states to display
108 108 #[derive(Copy, Clone, Debug)]
109 109 pub struct DisplayStates {
110 110 pub modified: bool,
111 111 pub added: bool,
112 112 pub removed: bool,
113 113 pub clean: bool,
114 114 pub deleted: bool,
115 115 pub unknown: bool,
116 116 pub ignored: bool,
117 117 }
118 118
119 119 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
120 120 modified: true,
121 121 added: true,
122 122 removed: true,
123 123 clean: false,
124 124 deleted: true,
125 125 unknown: true,
126 126 ignored: false,
127 127 };
128 128
129 129 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
130 130 modified: true,
131 131 added: true,
132 132 removed: true,
133 133 clean: true,
134 134 deleted: true,
135 135 unknown: true,
136 136 ignored: true,
137 137 };
138 138
139 139 impl DisplayStates {
140 140 pub fn is_empty(&self) -> bool {
141 141 !(self.modified
142 142 || self.added
143 143 || self.removed
144 144 || self.clean
145 145 || self.deleted
146 146 || self.unknown
147 147 || self.ignored)
148 148 }
149 149 }
150 150
151 151 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
152 152 let status_enabled_default = false;
153 153 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
154 154 if !status_enabled.unwrap_or(status_enabled_default) {
155 155 return Err(CommandError::unsupported(
156 156 "status is experimental in rhg (enable it with 'rhg.status = true' \
157 157 or enable fallback with 'rhg.on-unsupported = fallback')"
158 158 ));
159 159 }
160 160
161 161 // TODO: lift these limitations
162 162 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
163 163 return Err(CommandError::unsupported(
164 164 "ui.tweakdefaults is not yet supported with rhg status",
165 165 ));
166 166 }
167 167 if invocation.config.get_bool(b"ui", b"statuscopies")? {
168 168 return Err(CommandError::unsupported(
169 169 "ui.statuscopies is not yet supported with rhg status",
170 170 ));
171 171 }
172 172 if invocation
173 173 .config
174 174 .get(b"commands", b"status.terse")
175 175 .is_some()
176 176 {
177 177 return Err(CommandError::unsupported(
178 178 "status.terse is not yet supported with rhg status",
179 179 ));
180 180 }
181 181
182 182 let ui = invocation.ui;
183 183 let config = invocation.config;
184 184 let args = invocation.subcommand_args;
185 185
186 186 let verbose = !ui.plain(None)
187 187 && !args.is_present("print0")
188 188 && (config.get_bool(b"ui", b"verbose")?
189 189 || config.get_bool(b"commands", b"status.verbose")?);
190 190 if verbose {
191 191 return Err(CommandError::unsupported(
192 192 "verbose status is not supported yet",
193 193 ));
194 194 }
195 195
196 196 let all = args.is_present("all");
197 197 let display_states = if all {
198 198 // TODO when implementing `--quiet`: it excludes clean files
199 199 // from `--all`
200 200 ALL_DISPLAY_STATES
201 201 } else {
202 202 let requested = DisplayStates {
203 203 modified: args.is_present("modified"),
204 204 added: args.is_present("added"),
205 205 removed: args.is_present("removed"),
206 206 clean: args.is_present("clean"),
207 207 deleted: args.is_present("deleted"),
208 208 unknown: args.is_present("unknown"),
209 209 ignored: args.is_present("ignored"),
210 210 };
211 211 if requested.is_empty() {
212 212 DEFAULT_DISPLAY_STATES
213 213 } else {
214 214 requested
215 215 }
216 216 };
217 217 let no_status = args.is_present("no-status");
218 218 let list_copies = all
219 219 || args.is_present("copies")
220 220 || config.get_bool(b"ui", b"statuscopies")?;
221 221
222 222 let repo = invocation.repo?;
223 223
224 224 if repo.has_sparse() || repo.has_narrow() {
225 225 return Err(CommandError::unsupported(
226 226 "rhg status is not supported for sparse checkouts or narrow clones yet"
227 227 ));
228 228 }
229 229
230 230 let mut dmap = repo.dirstate_map_mut()?;
231 231
232 232 let options = StatusOptions {
233 233 // we're currently supporting file systems with exec flags only
234 234 // anyway
235 235 check_exec: true,
236 236 list_clean: display_states.clean,
237 237 list_unknown: display_states.unknown,
238 238 list_ignored: display_states.ignored,
239 239 list_copies,
240 240 collect_traversed_dirs: false,
241 241 };
242 242 let (mut ds_status, pattern_warnings) = dmap.status(
243 243 &AlwaysMatcher,
244 244 repo.working_directory_path().to_owned(),
245 245 ignore_files(repo, config),
246 246 options,
247 247 )?;
248 248 for warning in pattern_warnings {
249 249 match warning {
250 250 hg::PatternFileWarning::InvalidSyntax(path, syntax) => ui
251 251 .write_stderr(&format_bytes!(
252 252 b"{}: ignoring invalid syntax '{}'\n",
253 253 get_bytes_from_path(path),
254 254 &*syntax
255 255 ))?,
256 256 hg::PatternFileWarning::NoSuchFile(path) => {
257 257 let path = if let Ok(relative) =
258 258 path.strip_prefix(repo.working_directory_path())
259 259 {
260 260 relative
261 261 } else {
262 262 &*path
263 263 };
264 264 ui.write_stderr(&format_bytes!(
265 265 b"skipping unreadable pattern file '{}': \
266 266 No such file or directory\n",
267 267 get_bytes_from_path(path),
268 268 ))?
269 269 }
270 270 }
271 271 }
272 272
273 273 for (path, error) in ds_status.bad {
274 274 let error = match error {
275 275 hg::BadMatch::OsError(code) => {
276 276 std::io::Error::from_raw_os_error(code).to_string()
277 277 }
278 278 hg::BadMatch::BadType(ty) => {
279 279 format!("unsupported file type (type is {})", ty)
280 280 }
281 281 };
282 282 ui.write_stderr(&format_bytes!(
283 283 b"{}: {}\n",
284 284 path.as_bytes(),
285 285 error.as_bytes()
286 286 ))?
287 287 }
288 288 if !ds_status.unsure.is_empty() {
289 289 info!(
290 290 "Files to be rechecked by retrieval from filelog: {:?}",
291 291 ds_status.unsure.iter().map(|s| &s.path).collect::<Vec<_>>()
292 292 );
293 293 }
294 294 let mut fixup = Vec::new();
295 295 if !ds_status.unsure.is_empty()
296 296 && (display_states.modified || display_states.clean)
297 297 {
298 298 let p1 = repo.dirstate_parents()?.p1;
299 299 let manifest = repo.manifest_for_node(p1).map_err(|e| {
300 300 CommandError::from((e, &*format!("{:x}", p1.short())))
301 301 })?;
302 302 for to_check in ds_status.unsure {
303 303 if unsure_is_modified(repo, &manifest, &to_check.path)? {
304 304 if display_states.modified {
305 305 ds_status.modified.push(to_check);
306 306 }
307 307 } else {
308 308 if display_states.clean {
309 309 ds_status.clean.push(to_check.clone());
310 310 }
311 311 fixup.push(to_check.path.into_owned())
312 312 }
313 313 }
314 314 }
315 315 let relative_paths = (!ui.plain(None))
316 316 && config
317 317 .get_option(b"commands", b"status.relative")?
318 318 .unwrap_or(config.get_bool(b"ui", b"relative-paths")?);
319 319 let output = DisplayStatusPaths {
320 320 ui,
321 321 no_status,
322 322 relativize: if relative_paths {
323 323 Some(RelativizePaths::new(repo)?)
324 324 } else {
325 325 None
326 326 },
327 327 };
328 328 if display_states.modified {
329 output.display(b"M", ds_status.modified)?;
329 output.display(b"M ", "status.modified", ds_status.modified)?;
330 330 }
331 331 if display_states.added {
332 output.display(b"A", ds_status.added)?;
332 output.display(b"A ", "status.added", ds_status.added)?;
333 333 }
334 334 if display_states.removed {
335 output.display(b"R", ds_status.removed)?;
335 output.display(b"R ", "status.removed", ds_status.removed)?;
336 336 }
337 337 if display_states.deleted {
338 output.display(b"!", ds_status.deleted)?;
338 output.display(b"! ", "status.deleted", ds_status.deleted)?;
339 339 }
340 340 if display_states.unknown {
341 output.display(b"?", ds_status.unknown)?;
341 output.display(b"? ", "status.unknown", ds_status.unknown)?;
342 342 }
343 343 if display_states.ignored {
344 output.display(b"I", ds_status.ignored)?;
344 output.display(b"I ", "status.ignored", ds_status.ignored)?;
345 345 }
346 346 if display_states.clean {
347 output.display(b"C", ds_status.clean)?;
347 output.display(b"C ", "status.clean", ds_status.clean)?;
348 348 }
349 349
350 350 let mut dirstate_write_needed = ds_status.dirty;
351 351 let filesystem_time_at_status_start =
352 352 ds_status.filesystem_time_at_status_start;
353 353
354 354 if (fixup.is_empty() || filesystem_time_at_status_start.is_none())
355 355 && !dirstate_write_needed
356 356 {
357 357 // Nothing to update
358 358 return Ok(());
359 359 }
360 360
361 361 // Update the dirstate on disk if we can
362 362 let with_lock_result =
363 363 repo.try_with_wlock_no_wait(|| -> Result<(), CommandError> {
364 364 if let Some(mtime_boundary) = filesystem_time_at_status_start {
365 365 for hg_path in fixup {
366 366 use std::os::unix::fs::MetadataExt;
367 367 let fs_path = hg_path_to_path_buf(&hg_path)
368 368 .expect("HgPath conversion");
369 369 // Specifically do not reuse `fs_metadata` from
370 370 // `unsure_is_clean` which was needed before reading
371 371 // contents. Here we access metadata again after reading
372 372 // content, in case it changed in the meantime.
373 373 let fs_metadata = repo
374 374 .working_directory_vfs()
375 375 .symlink_metadata(&fs_path)?;
376 376 if let Some(mtime) =
377 377 TruncatedTimestamp::for_reliable_mtime_of(
378 378 &fs_metadata,
379 379 &mtime_boundary,
380 380 )
381 381 .when_reading_file(&fs_path)?
382 382 {
383 383 let mode = fs_metadata.mode();
384 384 let size = fs_metadata.len() as u32 & RANGE_MASK_31BIT;
385 385 let mut entry = dmap
386 386 .get(&hg_path)?
387 387 .expect("ambiguous file not in dirstate");
388 388 entry.set_clean(mode, size, mtime);
389 389 dmap.add_file(&hg_path, entry)?;
390 390 dirstate_write_needed = true
391 391 }
392 392 }
393 393 }
394 394 drop(dmap); // Avoid "already mutably borrowed" RefCell panics
395 395 if dirstate_write_needed {
396 396 repo.write_dirstate()?
397 397 }
398 398 Ok(())
399 399 });
400 400 match with_lock_result {
401 401 Ok(closure_result) => closure_result?,
402 402 Err(LockError::AlreadyHeld) => {
403 403 // Not updating the dirstate is not ideal but not critical:
404 404 // don’t keep our caller waiting until some other Mercurial
405 405 // process releases the lock.
406 406 }
407 407 Err(LockError::Other(HgError::IoError { error, .. }))
408 408 if error.kind() == io::ErrorKind::PermissionDenied =>
409 409 {
410 410 // `hg status` on a read-only repository is fine
411 411 }
412 412 Err(LockError::Other(error)) => {
413 413 // Report other I/O errors
414 414 Err(error)?
415 415 }
416 416 }
417 417 Ok(())
418 418 }
419 419
420 420 fn ignore_files(repo: &Repo, config: &Config) -> Vec<PathBuf> {
421 421 let mut ignore_files = Vec::new();
422 422 let repo_ignore = repo.working_directory_vfs().join(".hgignore");
423 423 if repo_ignore.exists() {
424 424 ignore_files.push(repo_ignore)
425 425 }
426 426 for (key, value) in config.iter_section(b"ui") {
427 427 if key == b"ignore" || key.starts_with(b"ignore.") {
428 428 let path = get_path_from_bytes(value);
429 429 // TODO:Β expand "~/" and environment variable here, like Python
430 430 // does with `os.path.expanduser` and `os.path.expandvars`
431 431
432 432 let joined = repo.working_directory_path().join(path);
433 433 ignore_files.push(joined);
434 434 }
435 435 }
436 436 ignore_files
437 437 }
438 438
439 439 struct DisplayStatusPaths<'a> {
440 440 ui: &'a Ui,
441 441 no_status: bool,
442 442 relativize: Option<RelativizePaths>,
443 443 }
444 444
445 445 impl DisplayStatusPaths<'_> {
446 446 // Probably more elegant to use a Deref or Borrow trait rather than
447 447 // harcode HgPathBuf, but probably not really useful at this point
448 448 fn display(
449 449 &self,
450 450 status_prefix: &[u8],
451 label: &'static str,
451 452 mut paths: Vec<StatusPath<'_>>,
452 453 ) -> Result<(), CommandError> {
453 454 paths.sort_unstable();
455 // TODO:Β get the stdout lock once for the whole loop instead of in each write
454 456 for StatusPath { path, copy_source } in paths {
455 457 let relative;
456 458 let path = if let Some(relativize) = &self.relativize {
457 459 relative = relativize.relativize(&path);
458 460 &*relative
459 461 } else {
460 462 path.as_bytes()
461 463 };
462 // TODO optim, probably lots of unneeded copies here, especially
463 // if out stream is buffered
464 if self.no_status {
465 self.ui.write_stdout(&format_bytes!(b"{}\n", path))?
466 } else {
467 self.ui.write_stdout(&format_bytes!(
468 b"{} {}\n",
469 status_prefix,
470 path
471 ))?
464 // TODO: Add a way to use `write_bytes!` instead of `format_bytes!`
465 // in order to stream to stdout instead of allocating an
466 // itermediate `Vec<u8>`.
467 if !self.no_status {
468 self.ui.write_stdout_labelled(status_prefix, label)?
472 469 }
470 self.ui
471 .write_stdout_labelled(&format_bytes!(b"{}\n", path), label)?;
473 472 if let Some(source) = copy_source {
474 self.ui.write_stdout(&format_bytes!(
475 b" {}\n",
476 source.as_bytes()
477 ))?
473 let label = "status.copied";
474 self.ui.write_stdout_labelled(
475 &format_bytes!(b" {}\n", source.as_bytes()),
476 label,
477 )?
478 478 }
479 479 }
480 480 Ok(())
481 481 }
482 482 }
483 483
484 484 /// Check if a file is modified by comparing actual repo store and file system.
485 485 ///
486 486 /// This meant to be used for those that the dirstate cannot resolve, due
487 487 /// to time resolution limits.
488 488 fn unsure_is_modified(
489 489 repo: &Repo,
490 490 manifest: &Manifest,
491 491 hg_path: &HgPath,
492 492 ) -> Result<bool, HgError> {
493 493 let vfs = repo.working_directory_vfs();
494 494 let fs_path = hg_path_to_path_buf(hg_path).expect("HgPath conversion");
495 495 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
496 496 let is_symlink = fs_metadata.file_type().is_symlink();
497 497 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
498 498 // dirstate
499 499 let fs_flags = if is_symlink {
500 500 Some(b'l')
501 501 } else if has_exec_bit(&fs_metadata) {
502 502 Some(b'x')
503 503 } else {
504 504 None
505 505 };
506 506
507 507 let entry = manifest
508 508 .find_by_path(hg_path)?
509 509 .expect("ambgious file not in p1");
510 510 if entry.flags != fs_flags {
511 511 return Ok(true);
512 512 }
513 513 let filelog = repo.filelog(hg_path)?;
514 514 let fs_len = fs_metadata.len();
515 515 let filelog_entry =
516 516 filelog.entry_for_node(entry.node_id()?).map_err(|_| {
517 517 HgError::corrupted("filelog missing node from manifest")
518 518 })?;
519 519 if filelog_entry.file_data_len_not_equal_to(fs_len) {
520 520 // No need to read file contents:
521 521 // it cannot be equal if it has a different length.
522 522 return Ok(true);
523 523 }
524 524
525 525 let p1_filelog_data = filelog_entry.data()?;
526 526 let p1_contents = p1_filelog_data.file_data()?;
527 527 if p1_contents.len() as u64 != fs_len {
528 528 // No need to read file contents:
529 529 // it cannot be equal if it has a different length.
530 530 return Ok(true);
531 531 }
532 532
533 533 let fs_contents = if is_symlink {
534 534 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
535 535 } else {
536 536 vfs.read(fs_path)?
537 537 };
538 538 Ok(p1_contents != &*fs_contents)
539 539 }
@@ -1,715 +1,706 b''
1 1 extern crate log;
2 2 use crate::error::CommandError;
3 3 use crate::ui::Ui;
4 4 use clap::App;
5 5 use clap::AppSettings;
6 6 use clap::Arg;
7 7 use clap::ArgMatches;
8 8 use format_bytes::{format_bytes, join};
9 9 use hg::config::{Config, ConfigSource};
10 10 use hg::exit_codes;
11 11 use hg::repo::{Repo, RepoError};
12 12 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
13 13 use hg::utils::SliceExt;
14 14 use std::collections::HashSet;
15 15 use std::ffi::OsString;
16 16 use std::path::PathBuf;
17 17 use std::process::Command;
18 18
19 19 mod blackbox;
20 20 mod color;
21 21 mod error;
22 22 mod ui;
23 23 pub mod utils {
24 24 pub mod path_utils;
25 25 }
26 26
27 27 fn main_with_result(
28 28 process_start_time: &blackbox::ProcessStartTime,
29 29 ui: &ui::Ui,
30 30 repo: Result<&Repo, &NoRepoInCwdError>,
31 31 config: &Config,
32 32 ) -> Result<(), CommandError> {
33 check_unsupported(config, repo, ui)?;
33 check_unsupported(config, repo)?;
34 34
35 35 let app = App::new("rhg")
36 36 .global_setting(AppSettings::AllowInvalidUtf8)
37 37 .global_setting(AppSettings::DisableVersion)
38 38 .setting(AppSettings::SubcommandRequired)
39 39 .setting(AppSettings::VersionlessSubcommands)
40 40 .arg(
41 41 Arg::with_name("repository")
42 42 .help("repository root directory")
43 43 .short("-R")
44 44 .long("--repository")
45 45 .value_name("REPO")
46 46 .takes_value(true)
47 47 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
48 48 .global(true),
49 49 )
50 50 .arg(
51 51 Arg::with_name("config")
52 52 .help("set/override config option (use 'section.name=value')")
53 53 .long("--config")
54 54 .value_name("CONFIG")
55 55 .takes_value(true)
56 56 .global(true)
57 57 // Ok: `--config section.key1=val --config section.key2=val2`
58 58 .multiple(true)
59 59 // Not ok: `--config section.key1=val section.key2=val2`
60 60 .number_of_values(1),
61 61 )
62 62 .arg(
63 63 Arg::with_name("cwd")
64 64 .help("change working directory")
65 65 .long("--cwd")
66 66 .value_name("DIR")
67 67 .takes_value(true)
68 68 .global(true),
69 69 )
70 70 .arg(
71 71 Arg::with_name("color")
72 72 .help("when to colorize (boolean, always, auto, never, or debug)")
73 73 .long("--color")
74 74 .value_name("TYPE")
75 75 .takes_value(true)
76 76 .global(true),
77 77 )
78 78 .version("0.0.1");
79 79 let app = add_subcommand_args(app);
80 80
81 81 let matches = app.clone().get_matches_safe()?;
82 82
83 83 let (subcommand_name, subcommand_matches) = matches.subcommand();
84 84
85 85 // Mercurial allows users to define "defaults" for commands, fallback
86 86 // if a default is detected for the current command
87 87 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
88 88 if defaults?.is_some() {
89 89 let msg = "`defaults` config set";
90 90 return Err(CommandError::unsupported(msg));
91 91 }
92 92
93 93 for prefix in ["pre", "post", "fail"].iter() {
94 94 // Mercurial allows users to define generic hooks for commands,
95 95 // fallback if any are detected
96 96 let item = format!("{}-{}", prefix, subcommand_name);
97 97 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
98 98 if hook_for_command.is_some() {
99 99 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
100 100 return Err(CommandError::unsupported(msg));
101 101 }
102 102 }
103 103 let run = subcommand_run_fn(subcommand_name)
104 104 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
105 105 let subcommand_args = subcommand_matches
106 106 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
107 107
108 108 let invocation = CliInvocation {
109 109 ui,
110 110 subcommand_args,
111 111 config,
112 112 repo,
113 113 };
114 114
115 115 if let Ok(repo) = repo {
116 116 // We don't support subrepos, fallback if the subrepos file is present
117 117 if repo.working_directory_vfs().join(".hgsub").exists() {
118 118 let msg = "subrepos (.hgsub is present)";
119 119 return Err(CommandError::unsupported(msg));
120 120 }
121 121 }
122 122
123 123 if config.is_extension_enabled(b"blackbox") {
124 124 let blackbox =
125 125 blackbox::Blackbox::new(&invocation, process_start_time)?;
126 126 blackbox.log_command_start();
127 127 let result = run(&invocation);
128 128 blackbox.log_command_end(exit_code(
129 129 &result,
130 130 // TODO: show a warning or combine with original error if
131 131 // `get_bool` returns an error
132 132 config
133 133 .get_bool(b"ui", b"detailed-exit-code")
134 134 .unwrap_or(false),
135 135 ));
136 136 result
137 137 } else {
138 138 run(&invocation)
139 139 }
140 140 }
141 141
142 142 fn main() {
143 143 // Run this first, before we find out if the blackbox extension is even
144 144 // enabled, in order to include everything in-between in the duration
145 145 // measurements. Reading config files can be slow if they’re on NFS.
146 146 let process_start_time = blackbox::ProcessStartTime::now();
147 147
148 148 env_logger::init();
149 149
150 150 let early_args = EarlyArgs::parse(std::env::args_os());
151 151
152 152 let initial_current_dir = early_args.cwd.map(|cwd| {
153 153 let cwd = get_path_from_bytes(&cwd);
154 154 std::env::current_dir()
155 155 .and_then(|initial| {
156 156 std::env::set_current_dir(cwd)?;
157 157 Ok(initial)
158 158 })
159 159 .unwrap_or_else(|error| {
160 160 exit(
161 161 &None,
162 162 &Ui::new_infallible(&Config::empty()),
163 163 OnUnsupported::Abort,
164 164 Err(CommandError::abort(format!(
165 165 "abort: {}: '{}'",
166 166 error,
167 167 cwd.display()
168 168 ))),
169 169 false,
170 170 )
171 171 })
172 172 });
173 173
174 174 let mut non_repo_config =
175 175 Config::load_non_repo().unwrap_or_else(|error| {
176 176 // Normally this is decided based on config, but we don’t have that
177 177 // available. As of this writing config loading never returns an
178 178 // "unsupported" error but that is not enforced by the type system.
179 179 let on_unsupported = OnUnsupported::Abort;
180 180
181 181 exit(
182 182 &initial_current_dir,
183 183 &Ui::new_infallible(&Config::empty()),
184 184 on_unsupported,
185 185 Err(error.into()),
186 186 false,
187 187 )
188 188 });
189 189
190 190 non_repo_config
191 191 .load_cli_args(early_args.config, early_args.color)
192 192 .unwrap_or_else(|error| {
193 193 exit(
194 194 &initial_current_dir,
195 195 &Ui::new_infallible(&non_repo_config),
196 196 OnUnsupported::from_config(&non_repo_config),
197 197 Err(error.into()),
198 198 non_repo_config
199 199 .get_bool(b"ui", b"detailed-exit-code")
200 200 .unwrap_or(false),
201 201 )
202 202 });
203 203
204 204 if let Some(repo_path_bytes) = &early_args.repo {
205 205 lazy_static::lazy_static! {
206 206 static ref SCHEME_RE: regex::bytes::Regex =
207 207 // Same as `_matchscheme` in `mercurial/util.py`
208 208 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
209 209 }
210 210 if SCHEME_RE.is_match(&repo_path_bytes) {
211 211 exit(
212 212 &initial_current_dir,
213 213 &Ui::new_infallible(&non_repo_config),
214 214 OnUnsupported::from_config(&non_repo_config),
215 215 Err(CommandError::UnsupportedFeature {
216 216 message: format_bytes!(
217 217 b"URL-like --repository {}",
218 218 repo_path_bytes
219 219 ),
220 220 }),
221 221 // TODO: show a warning or combine with original error if
222 222 // `get_bool` returns an error
223 223 non_repo_config
224 224 .get_bool(b"ui", b"detailed-exit-code")
225 225 .unwrap_or(false),
226 226 )
227 227 }
228 228 }
229 229 let repo_arg = early_args.repo.unwrap_or(Vec::new());
230 230 let repo_path: Option<PathBuf> = {
231 231 if repo_arg.is_empty() {
232 232 None
233 233 } else {
234 234 let local_config = {
235 235 if std::env::var_os("HGRCSKIPREPO").is_none() {
236 236 // TODO: handle errors from find_repo_root
237 237 if let Ok(current_dir_path) = Repo::find_repo_root() {
238 238 let config_files = vec![
239 239 ConfigSource::AbsPath(
240 240 current_dir_path.join(".hg/hgrc"),
241 241 ),
242 242 ConfigSource::AbsPath(
243 243 current_dir_path.join(".hg/hgrc-not-shared"),
244 244 ),
245 245 ];
246 246 // TODO: handle errors from
247 247 // `load_from_explicit_sources`
248 248 Config::load_from_explicit_sources(config_files).ok()
249 249 } else {
250 250 None
251 251 }
252 252 } else {
253 253 None
254 254 }
255 255 };
256 256
257 257 let non_repo_config_val = {
258 258 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
259 259 match &non_repo_val {
260 260 Some(val) if val.len() > 0 => home::home_dir()
261 261 .unwrap_or_else(|| PathBuf::from("~"))
262 262 .join(get_path_from_bytes(val))
263 263 .canonicalize()
264 264 // TODO: handle error and make it similar to python
265 265 // implementation maybe?
266 266 .ok(),
267 267 _ => None,
268 268 }
269 269 };
270 270
271 271 let config_val = match &local_config {
272 272 None => non_repo_config_val,
273 273 Some(val) => {
274 274 let local_config_val = val.get(b"paths", &repo_arg);
275 275 match &local_config_val {
276 276 Some(val) if val.len() > 0 => {
277 277 // presence of a local_config assures that
278 278 // current_dir
279 279 // wont result in an Error
280 280 let canpath = hg::utils::current_dir()
281 281 .unwrap()
282 282 .join(get_path_from_bytes(val))
283 283 .canonicalize();
284 284 canpath.ok().or(non_repo_config_val)
285 285 }
286 286 _ => non_repo_config_val,
287 287 }
288 288 }
289 289 };
290 290 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
291 291 }
292 292 };
293 293
294 294 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
295 295 {
296 296 Ok(repo) => Ok(repo),
297 297 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
298 298 // Not finding a repo is not fatal yet, if `-R` was not given
299 299 Err(NoRepoInCwdError { cwd: at })
300 300 }
301 301 Err(error) => exit(
302 302 &initial_current_dir,
303 303 &Ui::new_infallible(&non_repo_config),
304 304 OnUnsupported::from_config(&non_repo_config),
305 305 Err(error.into()),
306 306 // TODO: show a warning or combine with original error if
307 307 // `get_bool` returns an error
308 308 non_repo_config
309 309 .get_bool(b"ui", b"detailed-exit-code")
310 310 .unwrap_or(false),
311 311 ),
312 312 };
313 313
314 314 let config = if let Ok(repo) = &repo_result {
315 315 repo.config()
316 316 } else {
317 317 &non_repo_config
318 318 };
319 319 let ui = Ui::new(&config).unwrap_or_else(|error| {
320 320 exit(
321 321 &initial_current_dir,
322 322 &Ui::new_infallible(&config),
323 323 OnUnsupported::from_config(&config),
324 324 Err(error.into()),
325 325 config
326 326 .get_bool(b"ui", b"detailed-exit-code")
327 327 .unwrap_or(false),
328 328 )
329 329 });
330 330 let on_unsupported = OnUnsupported::from_config(config);
331 331
332 332 let result = main_with_result(
333 333 &process_start_time,
334 334 &ui,
335 335 repo_result.as_ref(),
336 336 config,
337 337 );
338 338 exit(
339 339 &initial_current_dir,
340 340 &ui,
341 341 on_unsupported,
342 342 result,
343 343 // TODO: show a warning or combine with original error if `get_bool`
344 344 // returns an error
345 345 config
346 346 .get_bool(b"ui", b"detailed-exit-code")
347 347 .unwrap_or(false),
348 348 )
349 349 }
350 350
351 351 fn exit_code(
352 352 result: &Result<(), CommandError>,
353 353 use_detailed_exit_code: bool,
354 354 ) -> i32 {
355 355 match result {
356 356 Ok(()) => exit_codes::OK,
357 357 Err(CommandError::Abort {
358 358 message: _,
359 359 detailed_exit_code,
360 360 }) => {
361 361 if use_detailed_exit_code {
362 362 *detailed_exit_code
363 363 } else {
364 364 exit_codes::ABORT
365 365 }
366 366 }
367 367 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
368 368
369 369 // Exit with a specific code and no error message to let a potential
370 370 // wrapper script fallback to Python-based Mercurial.
371 371 Err(CommandError::UnsupportedFeature { .. }) => {
372 372 exit_codes::UNIMPLEMENTED
373 373 }
374 374 }
375 375 }
376 376
377 377 fn exit(
378 378 initial_current_dir: &Option<PathBuf>,
379 379 ui: &Ui,
380 380 mut on_unsupported: OnUnsupported,
381 381 result: Result<(), CommandError>,
382 382 use_detailed_exit_code: bool,
383 383 ) -> ! {
384 384 if let (
385 385 OnUnsupported::Fallback { executable },
386 386 Err(CommandError::UnsupportedFeature { .. }),
387 387 ) = (&on_unsupported, &result)
388 388 {
389 389 let mut args = std::env::args_os();
390 390 let executable = match executable {
391 391 None => {
392 392 exit_no_fallback(
393 393 ui,
394 394 OnUnsupported::Abort,
395 395 Err(CommandError::abort(
396 396 "abort: 'rhg.on-unsupported=fallback' without \
397 397 'rhg.fallback-executable' set.",
398 398 )),
399 399 false,
400 400 );
401 401 }
402 402 Some(executable) => executable,
403 403 };
404 404 let executable_path = get_path_from_bytes(&executable);
405 405 let this_executable = args.next().expect("exepcted argv[0] to exist");
406 406 if executable_path == &PathBuf::from(this_executable) {
407 407 // Avoid spawning infinitely many processes until resource
408 408 // exhaustion.
409 409 let _ = ui.write_stderr(&format_bytes!(
410 410 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
411 411 points to `rhg` itself.\n",
412 412 executable
413 413 ));
414 414 on_unsupported = OnUnsupported::Abort
415 415 } else {
416 416 // `args` is now `argv[1..]` since we’ve already consumed
417 417 // `argv[0]`
418 418 let mut command = Command::new(executable_path);
419 419 command.args(args);
420 420 if let Some(initial) = initial_current_dir {
421 421 command.current_dir(initial);
422 422 }
423 423 let result = command.status();
424 424 match result {
425 425 Ok(status) => std::process::exit(
426 426 status.code().unwrap_or(exit_codes::ABORT),
427 427 ),
428 428 Err(error) => {
429 429 let _ = ui.write_stderr(&format_bytes!(
430 430 b"tried to fall back to a '{}' sub-process but got error {}\n",
431 431 executable, format_bytes::Utf8(error)
432 432 ));
433 433 on_unsupported = OnUnsupported::Abort
434 434 }
435 435 }
436 436 }
437 437 }
438 438 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
439 439 }
440 440
441 441 fn exit_no_fallback(
442 442 ui: &Ui,
443 443 on_unsupported: OnUnsupported,
444 444 result: Result<(), CommandError>,
445 445 use_detailed_exit_code: bool,
446 446 ) -> ! {
447 447 match &result {
448 448 Ok(_) => {}
449 449 Err(CommandError::Unsuccessful) => {}
450 450 Err(CommandError::Abort {
451 451 message,
452 452 detailed_exit_code: _,
453 453 }) => {
454 454 if !message.is_empty() {
455 455 // Ignore errors when writing to stderr, we’re already exiting
456 456 // with failure code so there’s not much more we can do.
457 457 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
458 458 }
459 459 }
460 460 Err(CommandError::UnsupportedFeature { message }) => {
461 461 match on_unsupported {
462 462 OnUnsupported::Abort => {
463 463 let _ = ui.write_stderr(&format_bytes!(
464 464 b"unsupported feature: {}\n",
465 465 message
466 466 ));
467 467 }
468 468 OnUnsupported::AbortSilent => {}
469 469 OnUnsupported::Fallback { .. } => unreachable!(),
470 470 }
471 471 }
472 472 }
473 473 std::process::exit(exit_code(&result, use_detailed_exit_code))
474 474 }
475 475
476 476 macro_rules! subcommands {
477 477 ($( $command: ident )+) => {
478 478 mod commands {
479 479 $(
480 480 pub mod $command;
481 481 )+
482 482 }
483 483
484 484 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
485 485 app
486 486 $(
487 487 .subcommand(commands::$command::args())
488 488 )+
489 489 }
490 490
491 491 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
492 492
493 493 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
494 494 match name {
495 495 $(
496 496 stringify!($command) => Some(commands::$command::run),
497 497 )+
498 498 _ => None,
499 499 }
500 500 }
501 501 };
502 502 }
503 503
504 504 subcommands! {
505 505 cat
506 506 debugdata
507 507 debugrequirements
508 508 debugignorerhg
509 509 files
510 510 root
511 511 config
512 512 status
513 513 }
514 514
515 515 pub struct CliInvocation<'a> {
516 516 ui: &'a Ui,
517 517 subcommand_args: &'a ArgMatches<'a>,
518 518 config: &'a Config,
519 519 /// References inside `Result` is a bit peculiar but allow
520 520 /// `invocation.repo?` to work out with `&CliInvocation` since this
521 521 /// `Result` type is `Copy`.
522 522 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
523 523 }
524 524
525 525 struct NoRepoInCwdError {
526 526 cwd: PathBuf,
527 527 }
528 528
529 529 /// CLI arguments to be parsed "early" in order to be able to read
530 530 /// configuration before using Clap. Ideally we would also use Clap for this,
531 531 /// see <https://github.com/clap-rs/clap/discussions/2366>.
532 532 ///
533 533 /// These arguments are still declared when we do use Clap later, so that Clap
534 534 /// does not return an error for their presence.
535 535 struct EarlyArgs {
536 536 /// Values of all `--config` arguments. (Possibly none)
537 537 config: Vec<Vec<u8>>,
538 538 /// Value of all the `--color` argument, if any.
539 539 color: Option<Vec<u8>>,
540 540 /// Value of the `-R` or `--repository` argument, if any.
541 541 repo: Option<Vec<u8>>,
542 542 /// Value of the `--cwd` argument, if any.
543 543 cwd: Option<Vec<u8>>,
544 544 }
545 545
546 546 impl EarlyArgs {
547 547 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
548 548 let mut args = args.into_iter().map(get_bytes_from_os_str);
549 549 let mut config = Vec::new();
550 550 let mut color = None;
551 551 let mut repo = None;
552 552 let mut cwd = None;
553 553 // Use `while let` instead of `for` so that we can also call
554 554 // `args.next()` inside the loop.
555 555 while let Some(arg) = args.next() {
556 556 if arg == b"--config" {
557 557 if let Some(value) = args.next() {
558 558 config.push(value)
559 559 }
560 560 } else if let Some(value) = arg.drop_prefix(b"--config=") {
561 561 config.push(value.to_owned())
562 562 }
563 563
564 564 if arg == b"--color" {
565 565 if let Some(value) = args.next() {
566 566 color = Some(value)
567 567 }
568 568 } else if let Some(value) = arg.drop_prefix(b"--color=") {
569 569 color = Some(value.to_owned())
570 570 }
571 571
572 572 if arg == b"--cwd" {
573 573 if let Some(value) = args.next() {
574 574 cwd = Some(value)
575 575 }
576 576 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
577 577 cwd = Some(value.to_owned())
578 578 }
579 579
580 580 if arg == b"--repository" || arg == b"-R" {
581 581 if let Some(value) = args.next() {
582 582 repo = Some(value)
583 583 }
584 584 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
585 585 repo = Some(value.to_owned())
586 586 } else if let Some(value) = arg.drop_prefix(b"-R") {
587 587 repo = Some(value.to_owned())
588 588 }
589 589 }
590 590 Self {
591 591 config,
592 592 color,
593 593 repo,
594 594 cwd,
595 595 }
596 596 }
597 597 }
598 598
599 599 /// What to do when encountering some unsupported feature.
600 600 ///
601 601 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
602 602 enum OnUnsupported {
603 603 /// Print an error message describing what feature is not supported,
604 604 /// and exit with code 252.
605 605 Abort,
606 606 /// Silently exit with code 252.
607 607 AbortSilent,
608 608 /// Try running a Python implementation
609 609 Fallback { executable: Option<Vec<u8>> },
610 610 }
611 611
612 612 impl OnUnsupported {
613 613 const DEFAULT: Self = OnUnsupported::Abort;
614 614
615 615 fn from_config(config: &Config) -> Self {
616 616 match config
617 617 .get(b"rhg", b"on-unsupported")
618 618 .map(|value| value.to_ascii_lowercase())
619 619 .as_deref()
620 620 {
621 621 Some(b"abort") => OnUnsupported::Abort,
622 622 Some(b"abort-silent") => OnUnsupported::AbortSilent,
623 623 Some(b"fallback") => OnUnsupported::Fallback {
624 624 executable: config
625 625 .get(b"rhg", b"fallback-executable")
626 626 .map(|x| x.to_owned()),
627 627 },
628 628 None => Self::DEFAULT,
629 629 Some(_) => {
630 630 // TODO: warn about unknown config value
631 631 Self::DEFAULT
632 632 }
633 633 }
634 634 }
635 635 }
636 636
637 637 /// The `*` extension is an edge-case for config sub-options that apply to all
638 638 /// extensions. For now, only `:required` exists, but that may change in the
639 639 /// future.
640 640 const SUPPORTED_EXTENSIONS: &[&[u8]] =
641 641 &[b"blackbox", b"share", b"sparse", b"narrow", b"*"];
642 642
643 643 fn check_extensions(config: &Config) -> Result<(), CommandError> {
644 644 let enabled: HashSet<&[u8]> = config
645 645 .get_section_keys(b"extensions")
646 646 .into_iter()
647 647 .map(|extension| {
648 648 // Ignore extension suboptions. Only `required` exists for now.
649 649 // `rhg` either supports an extension or doesn't, so it doesn't
650 650 // make sense to consider the loading of an extension.
651 651 extension.split_2(b':').unwrap_or((extension, b"")).0
652 652 })
653 653 .collect();
654 654
655 655 let mut unsupported = enabled;
656 656 for supported in SUPPORTED_EXTENSIONS {
657 657 unsupported.remove(supported);
658 658 }
659 659
660 660 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
661 661 {
662 662 for ignored in ignored_list {
663 663 unsupported.remove(ignored.as_slice());
664 664 }
665 665 }
666 666
667 667 if unsupported.is_empty() {
668 668 Ok(())
669 669 } else {
670 670 Err(CommandError::UnsupportedFeature {
671 671 message: format_bytes!(
672 672 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
673 673 join(unsupported, b", ")
674 674 ),
675 675 })
676 676 }
677 677 }
678 678
679 679 fn check_unsupported(
680 680 config: &Config,
681 681 repo: Result<&Repo, &NoRepoInCwdError>,
682 ui: &ui::Ui,
683 682 ) -> Result<(), CommandError> {
684 683 check_extensions(config)?;
685 684
686 685 if std::env::var_os("HG_PENDING").is_some() {
687 686 // TODO: only if the value is `== repo.working_directory`?
688 687 // What about relative v.s. absolute paths?
689 688 Err(CommandError::unsupported("$HG_PENDING"))?
690 689 }
691 690
692 691 if let Ok(repo) = repo {
693 692 if repo.has_subrepos()? {
694 693 Err(CommandError::unsupported("sub-repositories"))?
695 694 }
696 695 }
697 696
698 697 if config.has_non_empty_section(b"encode") {
699 698 Err(CommandError::unsupported("[encode] config"))?
700 699 }
701 700
702 701 if config.has_non_empty_section(b"decode") {
703 702 Err(CommandError::unsupported("[decode] config"))?
704 703 }
705 704
706 if let Some(color) = config.get(b"ui", b"color") {
707 if (color == b"always" || color == b"debug")
708 && !ui.plain(Some("color"))
709 {
710 Err(CommandError::unsupported("colored output"))?
711 }
712 }
713
714 705 Ok(())
715 706 }
@@ -1,245 +1,240 b''
1 1 use crate::color::ColorConfig;
2 2 use crate::color::Effect;
3 3 use format_bytes::format_bytes;
4 4 use format_bytes::write_bytes;
5 5 use hg::config::Config;
6 6 use hg::errors::HgError;
7 7 use hg::utils::files::get_bytes_from_os_string;
8 8 use std::borrow::Cow;
9 9 use std::env;
10 10 use std::io;
11 11 use std::io::{ErrorKind, Write};
12 12
13 13 pub struct Ui {
14 14 stdout: std::io::Stdout,
15 15 stderr: std::io::Stderr,
16 16 colors: Option<ColorConfig>,
17 17 }
18 18
19 19 /// The kind of user interface error
20 20 pub enum UiError {
21 21 /// The standard output stream cannot be written to
22 22 StdoutError(io::Error),
23 23 /// The standard error stream cannot be written to
24 24 StderrError(io::Error),
25 25 }
26 26
27 27 /// The commandline user interface
28 28 impl Ui {
29 29 pub fn new(config: &Config) -> Result<Self, HgError> {
30 30 Ok(Ui {
31 31 // If using something else, also adapt `isatty()` below.
32 32 stdout: std::io::stdout(),
33 33
34 34 stderr: std::io::stderr(),
35 35 colors: ColorConfig::new(config)?,
36 36 })
37 37 }
38 38
39 39 /// Default to no color if color configuration errors.
40 40 ///
41 41 /// Useful when we’re already handling another error.
42 42 pub fn new_infallible(config: &Config) -> Self {
43 43 Ui {
44 44 // If using something else, also adapt `isatty()` below.
45 45 stdout: std::io::stdout(),
46 46
47 47 stderr: std::io::stderr(),
48 48 colors: ColorConfig::new(config).unwrap_or(None),
49 49 }
50 50 }
51 51
52 52 /// Returns a buffered handle on stdout for faster batch printing
53 53 /// operations.
54 54 pub fn stdout_buffer(&self) -> StdoutBuffer<std::io::StdoutLock> {
55 55 StdoutBuffer::new(self.stdout.lock())
56 56 }
57 57
58 58 /// Write bytes to stdout
59 59 pub fn write_stdout(&self, bytes: &[u8]) -> Result<(), UiError> {
60 // Hack to silence "unused" warnings
61 if false {
62 return self.write_stdout_labelled(bytes, "");
63 }
64
65 60 let mut stdout = self.stdout.lock();
66 61
67 62 stdout.write_all(bytes).or_else(handle_stdout_error)?;
68 63
69 64 stdout.flush().or_else(handle_stdout_error)
70 65 }
71 66
72 67 /// Write bytes to stderr
73 68 pub fn write_stderr(&self, bytes: &[u8]) -> Result<(), UiError> {
74 69 let mut stderr = self.stderr.lock();
75 70
76 71 stderr.write_all(bytes).or_else(handle_stderr_error)?;
77 72
78 73 stderr.flush().or_else(handle_stderr_error)
79 74 }
80 75
81 76 /// Write bytes to stdout with the given label
82 77 ///
83 78 /// Like the optional `label` parameter in `mercurial/ui.py`,
84 79 /// this label influences the color used for this output.
85 80 pub fn write_stdout_labelled(
86 81 &self,
87 82 bytes: &[u8],
88 83 label: &str,
89 84 ) -> Result<(), UiError> {
90 85 if let Some(colors) = &self.colors {
91 86 if let Some(effects) = colors.styles.get(label.as_bytes()) {
92 87 if !effects.is_empty() {
93 88 return self
94 89 .write_stdout_with_effects(bytes, effects)
95 90 .or_else(handle_stdout_error);
96 91 }
97 92 }
98 93 }
99 94 self.write_stdout(bytes)
100 95 }
101 96
102 97 fn write_stdout_with_effects(
103 98 &self,
104 99 bytes: &[u8],
105 100 effects: &[Effect],
106 101 ) -> io::Result<()> {
107 102 let stdout = &mut self.stdout.lock();
108 103 let mut write_line = |line: &[u8], first: bool| {
109 104 // `line` does not include the newline delimiter
110 105 if !first {
111 106 stdout.write_all(b"\n")?;
112 107 }
113 108 if line.is_empty() {
114 109 return Ok(());
115 110 }
116 111 /// 0x1B == 27 == 0o33
117 112 const ASCII_ESCAPE: &[u8] = b"\x1b";
118 113 write_bytes!(stdout, b"{}[0", ASCII_ESCAPE)?;
119 114 for effect in effects {
120 115 write_bytes!(stdout, b";{}", effect)?;
121 116 }
122 117 write_bytes!(stdout, b"m")?;
123 118 stdout.write_all(line)?;
124 119 write_bytes!(stdout, b"{}[0m", ASCII_ESCAPE)
125 120 };
126 121 let mut lines = bytes.split(|&byte| byte == b'\n');
127 122 if let Some(first) = lines.next() {
128 123 write_line(first, true)?;
129 124 for line in lines {
130 125 write_line(line, false)?
131 126 }
132 127 }
133 128 stdout.flush()
134 129 }
135 130
136 131 /// Return whether plain mode is active.
137 132 ///
138 133 /// Plain mode means that all configuration variables which affect
139 134 /// the behavior and output of Mercurial should be
140 135 /// ignored. Additionally, the output should be stable,
141 136 /// reproducible and suitable for use in scripts or applications.
142 137 ///
143 138 /// The only way to trigger plain mode is by setting either the
144 139 /// `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
145 140 ///
146 141 /// The return value can either be
147 142 /// - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
148 143 /// - False if feature is disabled by default and not included in HGPLAIN
149 144 /// - True otherwise
150 145 pub fn plain(&self, feature: Option<&str>) -> bool {
151 146 plain(feature)
152 147 }
153 148 }
154 149
155 150 pub fn plain(opt_feature: Option<&str>) -> bool {
156 151 if let Some(except) = env::var_os("HGPLAINEXCEPT") {
157 152 opt_feature.map_or(true, |feature| {
158 153 get_bytes_from_os_string(except)
159 154 .split(|&byte| byte == b',')
160 155 .all(|exception| exception != feature.as_bytes())
161 156 })
162 157 } else {
163 158 env::var_os("HGPLAIN").is_some()
164 159 }
165 160 }
166 161
167 162 /// A buffered stdout writer for faster batch printing operations.
168 163 pub struct StdoutBuffer<W: Write> {
169 164 buf: io::BufWriter<W>,
170 165 }
171 166
172 167 impl<W: Write> StdoutBuffer<W> {
173 168 pub fn new(writer: W) -> Self {
174 169 let buf = io::BufWriter::new(writer);
175 170 Self { buf }
176 171 }
177 172
178 173 /// Write bytes to stdout buffer
179 174 pub fn write_all(&mut self, bytes: &[u8]) -> Result<(), UiError> {
180 175 self.buf.write_all(bytes).or_else(handle_stdout_error)
181 176 }
182 177
183 178 /// Flush bytes to stdout
184 179 pub fn flush(&mut self) -> Result<(), UiError> {
185 180 self.buf.flush().or_else(handle_stdout_error)
186 181 }
187 182 }
188 183
189 184 /// Sometimes writing to stdout is not possible, try writing to stderr to
190 185 /// signal that failure, otherwise just bail.
191 186 fn handle_stdout_error(error: io::Error) -> Result<(), UiError> {
192 187 if let ErrorKind::BrokenPipe = error.kind() {
193 188 // This makes `| head` work for example
194 189 return Ok(());
195 190 }
196 191 let mut stderr = io::stderr();
197 192
198 193 stderr
199 194 .write_all(&format_bytes!(
200 195 b"abort: {}\n",
201 196 error.to_string().as_bytes()
202 197 ))
203 198 .map_err(UiError::StderrError)?;
204 199
205 200 stderr.flush().map_err(UiError::StderrError)?;
206 201
207 202 Err(UiError::StdoutError(error))
208 203 }
209 204
210 205 /// Sometimes writing to stderr is not possible.
211 206 fn handle_stderr_error(error: io::Error) -> Result<(), UiError> {
212 207 // A broken pipe should not result in a error
213 208 // like with `| head` for example
214 209 if let ErrorKind::BrokenPipe = error.kind() {
215 210 return Ok(());
216 211 }
217 212 Err(UiError::StdoutError(error))
218 213 }
219 214
220 215 /// Encode rust strings according to the user system.
221 216 pub fn utf8_to_local(s: &str) -> Cow<[u8]> {
222 217 // TODO encode for the user's system //
223 218 let bytes = s.as_bytes();
224 219 Cow::Borrowed(bytes)
225 220 }
226 221
227 222 /// Should formatted output be used?
228 223 ///
229 224 /// Note: rhg does not have the formatter mechanism yet,
230 225 /// but this is also used when deciding whether to use color.
231 226 pub fn formatted(config: &Config) -> Result<bool, HgError> {
232 227 if let Some(formatted) = config.get_option(b"ui", b"formatted")? {
233 228 Ok(formatted)
234 229 } else {
235 230 isatty(config)
236 231 }
237 232 }
238 233
239 234 fn isatty(config: &Config) -> Result<bool, HgError> {
240 235 Ok(if config.get_bool(b"ui", b"nontty")? {
241 236 false
242 237 } else {
243 238 atty::is(atty::Stream::Stdout)
244 239 })
245 240 }
@@ -1,408 +1,407 b''
1 1 $ cat <<EOF >> $HGRCPATH
2 2 > [ui]
3 3 > color = always
4 4 > [color]
5 5 > mode = ansi
6 6 > EOF
7 7 Terminfo codes compatibility fix
8 8 $ echo "color.none=0" >> $HGRCPATH
9 9
10 10 $ hg init repo1
11 11 $ cd repo1
12 12 $ mkdir a b a/1 b/1 b/2
13 13 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
14 14
15 15 hg status in repo root:
16 16
17 17 $ hg status
18 18 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/1/in_a_1\x1b[0m (esc)
19 19 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/in_a\x1b[0m (esc)
20 20 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/1/in_b_1\x1b[0m (esc)
21 21 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/2/in_b_2\x1b[0m (esc)
22 22 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/in_b\x1b[0m (esc)
23 23 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_root\x1b[0m (esc)
24 24
25 25 $ hg status --color=debug
26 26 [status.unknown|? ][status.unknown|a/1/in_a_1]
27 27 [status.unknown|? ][status.unknown|a/in_a]
28 28 [status.unknown|? ][status.unknown|b/1/in_b_1]
29 29 [status.unknown|? ][status.unknown|b/2/in_b_2]
30 30 [status.unknown|? ][status.unknown|b/in_b]
31 31 [status.unknown|? ][status.unknown|in_root]
32 32 HGPLAIN disables color
33 33 $ HGPLAIN=1 hg status --color=debug
34 34 ? a/1/in_a_1 (glob)
35 35 ? a/in_a (glob)
36 36 ? b/1/in_b_1 (glob)
37 37 ? b/2/in_b_2 (glob)
38 38 ? b/in_b (glob)
39 39 ? in_root
40 40 HGPLAINEXCEPT=color does not disable color
41 41 $ HGPLAINEXCEPT=color hg status --color=debug
42 42 [status.unknown|? ][status.unknown|a/1/in_a_1] (glob)
43 43 [status.unknown|? ][status.unknown|a/in_a] (glob)
44 44 [status.unknown|? ][status.unknown|b/1/in_b_1] (glob)
45 45 [status.unknown|? ][status.unknown|b/2/in_b_2] (glob)
46 46 [status.unknown|? ][status.unknown|b/in_b] (glob)
47 47 [status.unknown|? ][status.unknown|in_root]
48 48
49 49 hg status with template
50 50 $ hg status -T "{label('red', path)}\n" --color=debug
51 51 [red|a/1/in_a_1]
52 52 [red|a/in_a]
53 53 [red|b/1/in_b_1]
54 54 [red|b/2/in_b_2]
55 55 [red|b/in_b]
56 56 [red|in_root]
57 57
58 58 hg status . in repo root:
59 59
60 60 $ hg status .
61 61 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/1/in_a_1\x1b[0m (esc)
62 62 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/in_a\x1b[0m (esc)
63 63 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/1/in_b_1\x1b[0m (esc)
64 64 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/2/in_b_2\x1b[0m (esc)
65 65 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/in_b\x1b[0m (esc)
66 66 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_root\x1b[0m (esc)
67 67
68 68 $ hg status --cwd a
69 69 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/1/in_a_1\x1b[0m (esc)
70 70 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/in_a\x1b[0m (esc)
71 71 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/1/in_b_1\x1b[0m (esc)
72 72 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/2/in_b_2\x1b[0m (esc)
73 73 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/in_b\x1b[0m (esc)
74 74 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_root\x1b[0m (esc)
75 75 $ hg status --cwd a .
76 76 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m1/in_a_1\x1b[0m (esc)
77 77 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_a\x1b[0m (esc)
78 78 $ hg status --cwd a ..
79 79 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m1/in_a_1\x1b[0m (esc)
80 80 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_a\x1b[0m (esc)
81 81 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m../b/1/in_b_1\x1b[0m (esc)
82 82 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m../b/2/in_b_2\x1b[0m (esc)
83 83 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m../b/in_b\x1b[0m (esc)
84 84 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m../in_root\x1b[0m (esc)
85 85
86 86 $ hg status --cwd b
87 87 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/1/in_a_1\x1b[0m (esc)
88 88 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/in_a\x1b[0m (esc)
89 89 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/1/in_b_1\x1b[0m (esc)
90 90 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/2/in_b_2\x1b[0m (esc)
91 91 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/in_b\x1b[0m (esc)
92 92 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_root\x1b[0m (esc)
93 93 $ hg status --cwd b .
94 94 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m1/in_b_1\x1b[0m (esc)
95 95 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m2/in_b_2\x1b[0m (esc)
96 96 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_b\x1b[0m (esc)
97 97 $ hg status --cwd b ..
98 98 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m../a/1/in_a_1\x1b[0m (esc)
99 99 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m../a/in_a\x1b[0m (esc)
100 100 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m1/in_b_1\x1b[0m (esc)
101 101 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m2/in_b_2\x1b[0m (esc)
102 102 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_b\x1b[0m (esc)
103 103 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m../in_root\x1b[0m (esc)
104 104
105 105 $ hg status --cwd a/1
106 106 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/1/in_a_1\x1b[0m (esc)
107 107 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/in_a\x1b[0m (esc)
108 108 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/1/in_b_1\x1b[0m (esc)
109 109 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/2/in_b_2\x1b[0m (esc)
110 110 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/in_b\x1b[0m (esc)
111 111 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_root\x1b[0m (esc)
112 112 $ hg status --cwd a/1 .
113 113 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_a_1\x1b[0m (esc)
114 114 $ hg status --cwd a/1 ..
115 115 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_a_1\x1b[0m (esc)
116 116 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m../in_a\x1b[0m (esc)
117 117
118 118 $ hg status --cwd b/1
119 119 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/1/in_a_1\x1b[0m (esc)
120 120 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/in_a\x1b[0m (esc)
121 121 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/1/in_b_1\x1b[0m (esc)
122 122 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/2/in_b_2\x1b[0m (esc)
123 123 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/in_b\x1b[0m (esc)
124 124 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_root\x1b[0m (esc)
125 125 $ hg status --cwd b/1 .
126 126 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_b_1\x1b[0m (esc)
127 127 $ hg status --cwd b/1 ..
128 128 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_b_1\x1b[0m (esc)
129 129 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m../2/in_b_2\x1b[0m (esc)
130 130 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m../in_b\x1b[0m (esc)
131 131
132 132 $ hg status --cwd b/2
133 133 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/1/in_a_1\x1b[0m (esc)
134 134 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4ma/in_a\x1b[0m (esc)
135 135 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/1/in_b_1\x1b[0m (esc)
136 136 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/2/in_b_2\x1b[0m (esc)
137 137 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4mb/in_b\x1b[0m (esc)
138 138 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_root\x1b[0m (esc)
139 139 $ hg status --cwd b/2 .
140 140 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_b_2\x1b[0m (esc)
141 141 $ hg status --cwd b/2 ..
142 142 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m../1/in_b_1\x1b[0m (esc)
143 143 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4min_b_2\x1b[0m (esc)
144 144 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4m../in_b\x1b[0m (esc)
145 145
146 146 Make sure --color=never works
147 147 $ hg status --color=never
148 148 ? a/1/in_a_1
149 149 ? a/in_a
150 150 ? b/1/in_b_1
151 151 ? b/2/in_b_2
152 152 ? b/in_b
153 153 ? in_root
154 154
155 155 Make sure ui.formatted=False works
156 156 $ hg status --color=auto --config ui.formatted=False
157 157 ? a/1/in_a_1
158 158 ? a/in_a
159 159 ? b/1/in_b_1
160 160 ? b/2/in_b_2
161 161 ? b/in_b
162 162 ? in_root
163 163
164 164 $ cd ..
165 165
166 166 $ hg init repo2
167 167 $ cd repo2
168 168 $ touch modified removed deleted ignored
169 169 $ echo "^ignored$" > .hgignore
170 170 $ hg ci -A -m 'initial checkin'
171 171 \x1b[0;32madding .hgignore\x1b[0m (esc)
172 172 \x1b[0;32madding deleted\x1b[0m (esc)
173 173 \x1b[0;32madding modified\x1b[0m (esc)
174 174 \x1b[0;32madding removed\x1b[0m (esc)
175 175 $ hg log --color=debug
176 176 [log.changeset changeset.draft|changeset: 0:389aef86a55e]
177 177 [log.tag|tag: tip]
178 178 [log.user|user: test]
179 179 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
180 180 [log.summary|summary: initial checkin]
181 181
182 182 $ hg log -Tcompact --color=debug
183 183 [log.changeset changeset.draft|0][tip] [log.node|389aef86a55e] [log.date|1970-01-01 00:00 +0000] [log.user|test]
184 184 [ui.note log.description|initial checkin]
185 185
186 186 Labels on empty strings should not be displayed, labels on custom
187 187 templates should be.
188 188
189 189 $ hg log --color=debug -T '{label("my.label",author)}\n{label("skipped.label","")}'
190 190 [my.label|test]
191 191 $ touch modified added unknown ignored
192 192 $ hg add added
193 193 $ hg remove removed
194 194 $ rm deleted
195 195
196 196 hg status:
197 197
198 198 $ hg status
199 199 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1madded\x1b[0m (esc)
200 200 \x1b[0;31;1mR \x1b[0m\x1b[0;31;1mremoved\x1b[0m (esc)
201 201 \x1b[0;36;1;4m! \x1b[0m\x1b[0;36;1;4mdeleted\x1b[0m (esc)
202 202 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4munknown\x1b[0m (esc)
203 203
204 204 hg status modified added removed deleted unknown never-existed ignored:
205 205
206 206 $ hg status modified added removed deleted unknown never-existed ignored
207 207 never-existed: * (glob)
208 208 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1madded\x1b[0m (esc)
209 209 \x1b[0;31;1mR \x1b[0m\x1b[0;31;1mremoved\x1b[0m (esc)
210 210 \x1b[0;36;1;4m! \x1b[0m\x1b[0;36;1;4mdeleted\x1b[0m (esc)
211 211 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4munknown\x1b[0m (esc)
212 212
213 213 $ hg copy modified copied
214 214
215 215 hg status -C:
216 216
217 217 $ hg status -C
218 218 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1madded\x1b[0m (esc)
219 219 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1mcopied\x1b[0m (esc)
220 220 \x1b[0;0m modified\x1b[0m (esc)
221 221 \x1b[0;31;1mR \x1b[0m\x1b[0;31;1mremoved\x1b[0m (esc)
222 222 \x1b[0;36;1;4m! \x1b[0m\x1b[0;36;1;4mdeleted\x1b[0m (esc)
223 223 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4munknown\x1b[0m (esc)
224 224
225 225 hg status -A:
226 226
227 227 $ hg status -A
228 228 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1madded\x1b[0m (esc)
229 229 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1mcopied\x1b[0m (esc)
230 230 \x1b[0;0m modified\x1b[0m (esc)
231 231 \x1b[0;31;1mR \x1b[0m\x1b[0;31;1mremoved\x1b[0m (esc)
232 232 \x1b[0;36;1;4m! \x1b[0m\x1b[0;36;1;4mdeleted\x1b[0m (esc)
233 233 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4munknown\x1b[0m (esc)
234 234 \x1b[0;30;1mI \x1b[0m\x1b[0;30;1mignored\x1b[0m (esc)
235 235 \x1b[0;0mC \x1b[0m\x1b[0;0m.hgignore\x1b[0m (esc)
236 236 \x1b[0;0mC \x1b[0m\x1b[0;0mmodified\x1b[0m (esc)
237 237
238 238
239 239 hg status -A (with terminfo color):
240 240
241 241 #if tic
242 242
243 243 $ tic -o "$TESTTMP/terminfo" "$TESTDIR/hgterm.ti"
244 244 $ ln -s "$TESTTMP/terminfo" "$TESTTMP/terminfo.cdb"
245 245 $ TERM=hgterm TERMINFO="$TESTTMP/terminfo" hg status --config color.mode=terminfo -A
246 246 \x1b[30m\x1b[32m\x1b[1mA \x1b[30m\x1b[30m\x1b[32m\x1b[1madded\x1b[30m (esc)
247 247 \x1b[30m\x1b[32m\x1b[1mA \x1b[30m\x1b[30m\x1b[32m\x1b[1mcopied\x1b[30m (esc)
248 248 \x1b[30m\x1b[30m modified\x1b[30m (esc)
249 249 \x1b[30m\x1b[31m\x1b[1mR \x1b[30m\x1b[30m\x1b[31m\x1b[1mremoved\x1b[30m (esc)
250 250 \x1b[30m\x1b[36m\x1b[1m\x1b[4m! \x1b[30m\x1b[30m\x1b[36m\x1b[1m\x1b[4mdeleted\x1b[30m (esc)
251 251 \x1b[30m\x1b[35m\x1b[1m\x1b[4m? \x1b[30m\x1b[30m\x1b[35m\x1b[1m\x1b[4munknown\x1b[30m (esc)
252 252 \x1b[30m\x1b[30m\x1b[1mI \x1b[30m\x1b[30m\x1b[30m\x1b[1mignored\x1b[30m (esc)
253 253 \x1b[30m\x1b[30mC \x1b[30m\x1b[30m\x1b[30m.hgignore\x1b[30m (esc)
254 254 \x1b[30m\x1b[30mC \x1b[30m\x1b[30m\x1b[30mmodified\x1b[30m (esc)
255 255
256 256 The user can define effects with raw terminfo codes:
257 257
258 258 $ cat <<EOF >> $HGRCPATH
259 259 > # Completely bogus code for dim
260 260 > terminfo.dim = \E[88m
261 261 > # We can override what's in the terminfo database, too
262 262 > terminfo.bold = \E[2m
263 263 > EOF
264 264 $ TERM=hgterm TERMINFO="$TESTTMP/terminfo" hg status --config color.mode=terminfo --config color.status.clean=dim -A
265 265 \x1b[30m\x1b[32m\x1b[2mA \x1b[30m\x1b[30m\x1b[32m\x1b[2madded\x1b[30m (esc)
266 266 \x1b[30m\x1b[32m\x1b[2mA \x1b[30m\x1b[30m\x1b[32m\x1b[2mcopied\x1b[30m (esc)
267 267 \x1b[30m\x1b[30m modified\x1b[30m (esc)
268 268 \x1b[30m\x1b[31m\x1b[2mR \x1b[30m\x1b[30m\x1b[31m\x1b[2mremoved\x1b[30m (esc)
269 269 \x1b[30m\x1b[36m\x1b[2m\x1b[4m! \x1b[30m\x1b[30m\x1b[36m\x1b[2m\x1b[4mdeleted\x1b[30m (esc)
270 270 \x1b[30m\x1b[35m\x1b[2m\x1b[4m? \x1b[30m\x1b[30m\x1b[35m\x1b[2m\x1b[4munknown\x1b[30m (esc)
271 271 \x1b[30m\x1b[30m\x1b[2mI \x1b[30m\x1b[30m\x1b[30m\x1b[2mignored\x1b[30m (esc)
272 272 \x1b[30m\x1b[88mC \x1b[30m\x1b[30m\x1b[88m.hgignore\x1b[30m (esc)
273 273 \x1b[30m\x1b[88mC \x1b[30m\x1b[30m\x1b[88mmodified\x1b[30m (esc)
274 274
275 275 #endif
276 276
277 277
278 278 $ echo "^ignoreddir$" > .hgignore
279 279 $ mkdir ignoreddir
280 280 $ touch ignoreddir/file
281 281
282 282 hg status ignoreddir/file:
283 283
284 284 $ hg status ignoreddir/file
285 285
286 286 hg status -i ignoreddir/file:
287 287
288 288 $ hg status -i ignoreddir/file
289 289 \x1b[0;30;1mI \x1b[0m\x1b[0;30;1mignoreddir/file\x1b[0m (esc)
290 290 $ cd ..
291 291
292 292 check 'status -q' and some combinations
293 293
294 294 $ hg init repo3
295 295 $ cd repo3
296 296 $ touch modified removed deleted ignored
297 297 $ echo "^ignored$" > .hgignore
298 298 $ hg commit -A -m 'initial checkin'
299 299 \x1b[0;32madding .hgignore\x1b[0m (esc)
300 300 \x1b[0;32madding deleted\x1b[0m (esc)
301 301 \x1b[0;32madding modified\x1b[0m (esc)
302 302 \x1b[0;32madding removed\x1b[0m (esc)
303 303 $ touch added unknown ignored
304 304 $ hg add added
305 305 $ echo "test" >> modified
306 306 $ hg remove removed
307 307 $ rm deleted
308 308 $ hg copy modified copied
309 309
310 310 test unknown color
311 311
312 312 $ hg --config color.status.modified=periwinkle status
313 313 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified)
314 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified)
315 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified)
316 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified) (rhg !)
314 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified) (no-rhg !)
315 ignoring unknown color/effect 'periwinkle' (configured in color.status.modified) (no-rhg !)
317 316 M modified
318 317 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1madded\x1b[0m (esc)
319 318 \x1b[0;32;1mA \x1b[0m\x1b[0;32;1mcopied\x1b[0m (esc)
320 319 \x1b[0;31;1mR \x1b[0m\x1b[0;31;1mremoved\x1b[0m (esc)
321 320 \x1b[0;36;1;4m! \x1b[0m\x1b[0;36;1;4mdeleted\x1b[0m (esc)
322 321 \x1b[0;35;1;4m? \x1b[0m\x1b[0;35;1;4munknown\x1b[0m (esc)
323 322
324 323 Run status with 2 different flags.
325 324 Check if result is the same or different.
326 325 If result is not as expected, raise error
327 326
328 327 $ assert() {
329 328 > hg status $1 > ../a
330 329 > hg status $2 > ../b
331 330 > if diff ../a ../b > /dev/null; then
332 331 > out=0
333 332 > else
334 333 > out=1
335 334 > fi
336 335 > if [ $3 -eq 0 ]; then
337 336 > df="same"
338 337 > else
339 338 > df="different"
340 339 > fi
341 340 > if [ $out -ne $3 ]; then
342 341 > echo "Error on $1 and $2, should be $df."
343 342 > fi
344 343 > }
345 344
346 345 assert flag1 flag2 [0-same | 1-different]
347 346
348 347 $ assert "-q" "-mard" 0
349 348 $ assert "-A" "-marduicC" 0
350 349 $ assert "-qA" "-mardcC" 0
351 350 $ assert "-qAui" "-A" 0
352 351 $ assert "-qAu" "-marducC" 0
353 352 $ assert "-qAi" "-mardicC" 0
354 353 $ assert "-qu" "-u" 0
355 354 $ assert "-q" "-u" 1
356 355 $ assert "-m" "-a" 1
357 356 $ assert "-r" "-d" 1
358 357 $ cd ..
359 358
360 359 test 'resolve -l'
361 360
362 361 $ hg init repo4
363 362 $ cd repo4
364 363 $ echo "file a" > a
365 364 $ echo "file b" > b
366 365 $ hg add a b
367 366 $ hg commit -m "initial"
368 367 $ echo "file a change 1" > a
369 368 $ echo "file b change 1" > b
370 369 $ hg commit -m "head 1"
371 370 $ hg update 0
372 371 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
373 372 $ echo "file a change 2" > a
374 373 $ echo "file b change 2" > b
375 374 $ hg commit -m "head 2"
376 375 created new head
377 376 $ hg merge
378 377 merging a
379 378 warning: conflicts while merging a! (edit, then use 'hg resolve --mark')
380 379 merging b
381 380 warning: conflicts while merging b! (edit, then use 'hg resolve --mark')
382 381 0 files updated, 0 files merged, 0 files removed, 2 files unresolved
383 382 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
384 383 [1]
385 384 $ hg resolve -m b
386 385
387 386 hg resolve with one unresolved, one resolved:
388 387
389 388 $ hg resolve -l
390 389 \x1b[0;31;1mU \x1b[0m\x1b[0;31;1ma\x1b[0m (esc)
391 390 \x1b[0;32;1mR \x1b[0m\x1b[0;32;1mb\x1b[0m (esc)
392 391
393 392 color coding of error message with current availability of curses
394 393
395 394 $ hg unknowncommand > /dev/null
396 395 hg: unknown command 'unknowncommand'
397 396 (use 'hg help' for a list of commands)
398 397 [10]
399 398
400 399 color coding of error message without curses
401 400
402 401 $ echo 'raise ImportError' > curses.py
403 402 $ PYTHONPATH=`pwd`:$PYTHONPATH hg unknowncommand > /dev/null
404 403 hg: unknown command 'unknowncommand'
405 404 (use 'hg help' for a list of commands)
406 405 [10]
407 406
408 407 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now