##// END OF EJS Templates
rhg: Add parsing for the --color global CLI argument...
Simon Sapin -
r49583:d4a5c219 default
parent child Browse files
Show More
@@ -1,544 +1,550 b''
1 1 // config.rs
2 2 //
3 3 // Copyright 2020
4 4 // Valentin Gatien-Baron,
5 5 // Raphaël Gomès <rgomes@octobus.net>
6 6 //
7 7 // This software may be used and distributed according to the terms of the
8 8 // GNU General Public License version 2 or any later version.
9 9
10 10 use super::layer;
11 11 use super::values;
12 12 use crate::config::layer::{
13 13 ConfigError, ConfigLayer, ConfigOrigin, ConfigValue,
14 14 };
15 15 use crate::utils::files::get_bytes_from_os_str;
16 16 use format_bytes::{write_bytes, DisplayBytes};
17 17 use std::collections::HashSet;
18 18 use std::env;
19 19 use std::fmt;
20 20 use std::path::{Path, PathBuf};
21 21 use std::str;
22 22
23 23 use crate::errors::{HgResultExt, IoResultExt};
24 24
25 25 /// Holds the config values for the current repository
26 26 /// TODO update this docstring once we support more sources
27 27 #[derive(Clone)]
28 28 pub struct Config {
29 29 layers: Vec<layer::ConfigLayer>,
30 30 }
31 31
32 32 impl DisplayBytes for Config {
33 33 fn display_bytes(
34 34 &self,
35 35 out: &mut dyn std::io::Write,
36 36 ) -> std::io::Result<()> {
37 37 for (index, layer) in self.layers.iter().rev().enumerate() {
38 38 write_bytes!(
39 39 out,
40 40 b"==== Layer {} (trusted: {}) ====\n{}",
41 41 index,
42 42 if layer.trusted {
43 43 &b"yes"[..]
44 44 } else {
45 45 &b"no"[..]
46 46 },
47 47 layer
48 48 )?;
49 49 }
50 50 Ok(())
51 51 }
52 52 }
53 53
54 54 pub enum ConfigSource {
55 55 /// Absolute path to a config file
56 56 AbsPath(PathBuf),
57 57 /// Already parsed (from the CLI, env, Python resources, etc.)
58 58 Parsed(layer::ConfigLayer),
59 59 }
60 60
61 61 #[derive(Debug)]
62 62 pub struct ConfigValueParseError {
63 63 pub origin: ConfigOrigin,
64 64 pub line: Option<usize>,
65 65 pub section: Vec<u8>,
66 66 pub item: Vec<u8>,
67 67 pub value: Vec<u8>,
68 68 pub expected_type: &'static str,
69 69 }
70 70
71 71 impl fmt::Display for ConfigValueParseError {
72 72 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
73 73 // TODO: add origin and line number information, here and in
74 74 // corresponding python code
75 75 write!(
76 76 f,
77 77 "config error: {}.{} is not a {} ('{}')",
78 78 String::from_utf8_lossy(&self.section),
79 79 String::from_utf8_lossy(&self.item),
80 80 self.expected_type,
81 81 String::from_utf8_lossy(&self.value)
82 82 )
83 83 }
84 84 }
85 85
86 86 impl Config {
87 87 /// The configuration to use when printing configuration-loading errors
88 88 pub fn empty() -> Self {
89 89 Self { layers: Vec::new() }
90 90 }
91 91
92 92 /// Load system and user configuration from various files.
93 93 ///
94 94 /// This is also affected by some environment variables.
95 95 pub fn load_non_repo() -> Result<Self, ConfigError> {
96 96 let mut config = Self { layers: Vec::new() };
97 97 let opt_rc_path = env::var_os("HGRCPATH");
98 98 // HGRCPATH replaces system config
99 99 if opt_rc_path.is_none() {
100 100 config.add_system_config()?
101 101 }
102 102
103 103 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
104 104 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
105 105 config.add_for_environment_variable("PAGER", b"pager", b"pager");
106 106
107 107 // These are set by `run-tests.py --rhg` to enable fallback for the
108 108 // entire test suite. Alternatives would be setting configuration
109 109 // through `$HGRCPATH` but some tests override that, or changing the
110 110 // `hg` shell alias to include `--config` but that disrupts tests that
111 111 // print command lines and check expected output.
112 112 config.add_for_environment_variable(
113 113 "RHG_ON_UNSUPPORTED",
114 114 b"rhg",
115 115 b"on-unsupported",
116 116 );
117 117 config.add_for_environment_variable(
118 118 "RHG_FALLBACK_EXECUTABLE",
119 119 b"rhg",
120 120 b"fallback-executable",
121 121 );
122 122 config.add_for_environment_variable("RHG_STATUS", b"rhg", b"status");
123 123
124 124 // HGRCPATH replaces user config
125 125 if opt_rc_path.is_none() {
126 126 config.add_user_config()?
127 127 }
128 128 if let Some(rc_path) = &opt_rc_path {
129 129 for path in env::split_paths(rc_path) {
130 130 if !path.as_os_str().is_empty() {
131 131 if path.is_dir() {
132 132 config.add_trusted_dir(&path)?
133 133 } else {
134 134 config.add_trusted_file(&path)?
135 135 }
136 136 }
137 137 }
138 138 }
139 139 Ok(config)
140 140 }
141 141
142 pub fn load_cli_args_config(
142 pub fn load_cli_args(
143 143 &mut self,
144 144 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
145 color_arg: Option<Vec<u8>>,
145 146 ) -> Result<(), ConfigError> {
146 147 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
147 148 self.layers.push(layer)
148 149 }
150 if let Some(arg) = color_arg {
151 let mut layer = ConfigLayer::new(ConfigOrigin::CommandLineColor);
152 layer.add(b"ui"[..].into(), b"color"[..].into(), arg, None);
153 self.layers.push(layer)
154 }
149 155 Ok(())
150 156 }
151 157
152 158 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
153 159 if let Some(entries) = std::fs::read_dir(path)
154 160 .when_reading_file(path)
155 161 .io_not_found_as_none()?
156 162 {
157 163 let mut file_paths = entries
158 164 .map(|result| {
159 165 result.when_reading_file(path).map(|entry| entry.path())
160 166 })
161 167 .collect::<Result<Vec<_>, _>>()?;
162 168 file_paths.sort();
163 169 for file_path in &file_paths {
164 170 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
165 171 self.add_trusted_file(&file_path)?
166 172 }
167 173 }
168 174 }
169 175 Ok(())
170 176 }
171 177
172 178 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
173 179 if let Some(data) = std::fs::read(path)
174 180 .when_reading_file(path)
175 181 .io_not_found_as_none()?
176 182 {
177 183 self.layers.extend(ConfigLayer::parse(path, &data)?)
178 184 }
179 185 Ok(())
180 186 }
181 187
182 188 fn add_for_environment_variable(
183 189 &mut self,
184 190 var: &str,
185 191 section: &[u8],
186 192 key: &[u8],
187 193 ) {
188 194 if let Some(value) = env::var_os(var) {
189 195 let origin = layer::ConfigOrigin::Environment(var.into());
190 196 let mut layer = ConfigLayer::new(origin);
191 197 layer.add(
192 198 section.to_owned(),
193 199 key.to_owned(),
194 200 get_bytes_from_os_str(value),
195 201 None,
196 202 );
197 203 self.layers.push(layer)
198 204 }
199 205 }
200 206
201 207 #[cfg(unix)] // TODO: other platforms
202 208 fn add_system_config(&mut self) -> Result<(), ConfigError> {
203 209 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
204 210 let etc = prefix.join("etc").join("mercurial");
205 211 self.add_trusted_file(&etc.join("hgrc"))?;
206 212 self.add_trusted_dir(&etc.join("hgrc.d"))
207 213 };
208 214 let root = Path::new("/");
209 215 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
210 216 // instead? TODO: can this be a relative path?
211 217 let hg = crate::utils::current_exe()?;
212 218 // TODO: this order (per-installation then per-system) matches
213 219 // `systemrcpath()` in `mercurial/scmposix.py`, but
214 220 // `mercurial/helptext/config.txt` suggests it should be reversed
215 221 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
216 222 if installation_prefix != root {
217 223 add_for_prefix(&installation_prefix)?
218 224 }
219 225 }
220 226 add_for_prefix(root)?;
221 227 Ok(())
222 228 }
223 229
224 230 #[cfg(unix)] // TODO: other plateforms
225 231 fn add_user_config(&mut self) -> Result<(), ConfigError> {
226 232 let opt_home = home::home_dir();
227 233 if let Some(home) = &opt_home {
228 234 self.add_trusted_file(&home.join(".hgrc"))?
229 235 }
230 236 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
231 237 if !darwin {
232 238 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
233 239 .map(PathBuf::from)
234 240 .or_else(|| opt_home.map(|home| home.join(".config")))
235 241 {
236 242 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
237 243 }
238 244 }
239 245 Ok(())
240 246 }
241 247
242 248 /// Loads in order, which means that the precedence is the same
243 249 /// as the order of `sources`.
244 250 pub fn load_from_explicit_sources(
245 251 sources: Vec<ConfigSource>,
246 252 ) -> Result<Self, ConfigError> {
247 253 let mut layers = vec![];
248 254
249 255 for source in sources.into_iter() {
250 256 match source {
251 257 ConfigSource::Parsed(c) => layers.push(c),
252 258 ConfigSource::AbsPath(c) => {
253 259 // TODO check if it should be trusted
254 260 // mercurial/ui.py:427
255 261 let data = match std::fs::read(&c) {
256 262 Err(_) => continue, // same as the python code
257 263 Ok(data) => data,
258 264 };
259 265 layers.extend(ConfigLayer::parse(&c, &data)?)
260 266 }
261 267 }
262 268 }
263 269
264 270 Ok(Config { layers })
265 271 }
266 272
267 273 /// Loads the per-repository config into a new `Config` which is combined
268 274 /// with `self`.
269 275 pub(crate) fn combine_with_repo(
270 276 &self,
271 277 repo_config_files: &[PathBuf],
272 278 ) -> Result<Self, ConfigError> {
273 279 let (cli_layers, other_layers) = self
274 280 .layers
275 281 .iter()
276 282 .cloned()
277 283 .partition(ConfigLayer::is_from_command_line);
278 284
279 285 let mut repo_config = Self {
280 286 layers: other_layers,
281 287 };
282 288 for path in repo_config_files {
283 289 // TODO: check if this file should be trusted:
284 290 // `mercurial/ui.py:427`
285 291 repo_config.add_trusted_file(path)?;
286 292 }
287 293 repo_config.layers.extend(cli_layers);
288 294 Ok(repo_config)
289 295 }
290 296
291 297 fn get_parse<'config, T: 'config>(
292 298 &'config self,
293 299 section: &[u8],
294 300 item: &[u8],
295 301 expected_type: &'static str,
296 302 parse: impl Fn(&'config [u8]) -> Option<T>,
297 303 ) -> Result<Option<T>, ConfigValueParseError> {
298 304 match self.get_inner(&section, &item) {
299 305 Some((layer, v)) => match parse(&v.bytes) {
300 306 Some(b) => Ok(Some(b)),
301 307 None => Err(ConfigValueParseError {
302 308 origin: layer.origin.to_owned(),
303 309 line: v.line,
304 310 value: v.bytes.to_owned(),
305 311 section: section.to_owned(),
306 312 item: item.to_owned(),
307 313 expected_type,
308 314 }),
309 315 },
310 316 None => Ok(None),
311 317 }
312 318 }
313 319
314 320 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
315 321 /// Otherwise, returns an `Ok(value)` if found, or `None`.
316 322 pub fn get_str(
317 323 &self,
318 324 section: &[u8],
319 325 item: &[u8],
320 326 ) -> Result<Option<&str>, ConfigValueParseError> {
321 327 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
322 328 str::from_utf8(value).ok()
323 329 })
324 330 }
325 331
326 332 /// Returns an `Err` if the first value found is not a valid unsigned
327 333 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
328 334 pub fn get_u32(
329 335 &self,
330 336 section: &[u8],
331 337 item: &[u8],
332 338 ) -> Result<Option<u32>, ConfigValueParseError> {
333 339 self.get_parse(section, item, "valid integer", |value| {
334 340 str::from_utf8(value).ok()?.parse().ok()
335 341 })
336 342 }
337 343
338 344 /// Returns an `Err` if the first value found is not a valid file size
339 345 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
340 346 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
341 347 pub fn get_byte_size(
342 348 &self,
343 349 section: &[u8],
344 350 item: &[u8],
345 351 ) -> Result<Option<u64>, ConfigValueParseError> {
346 352 self.get_parse(section, item, "byte quantity", values::parse_byte_size)
347 353 }
348 354
349 355 /// Returns an `Err` if the first value found is not a valid boolean.
350 356 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
351 357 /// found, or `None`.
352 358 pub fn get_option(
353 359 &self,
354 360 section: &[u8],
355 361 item: &[u8],
356 362 ) -> Result<Option<bool>, ConfigValueParseError> {
357 363 self.get_parse(section, item, "boolean", values::parse_bool)
358 364 }
359 365
360 366 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
361 367 /// if the value is not found, an `Err` if it's not a valid boolean.
362 368 pub fn get_bool(
363 369 &self,
364 370 section: &[u8],
365 371 item: &[u8],
366 372 ) -> Result<bool, ConfigValueParseError> {
367 373 Ok(self.get_option(section, item)?.unwrap_or(false))
368 374 }
369 375
370 376 /// Returns `true` if the extension is enabled, `false` otherwise
371 377 pub fn is_extension_enabled(&self, extension: &[u8]) -> bool {
372 378 let value = self.get(b"extensions", extension);
373 379 match value {
374 380 Some(c) => !c.starts_with(b"!"),
375 381 None => false,
376 382 }
377 383 }
378 384
379 385 /// If there is an `item` value in `section`, parse and return a list of
380 386 /// byte strings.
381 387 pub fn get_list(
382 388 &self,
383 389 section: &[u8],
384 390 item: &[u8],
385 391 ) -> Option<Vec<Vec<u8>>> {
386 392 self.get(section, item).map(values::parse_list)
387 393 }
388 394
389 395 /// Returns the raw value bytes of the first one found, or `None`.
390 396 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
391 397 self.get_inner(section, item)
392 398 .map(|(_, value)| value.bytes.as_ref())
393 399 }
394 400
395 401 /// Returns the layer and the value of the first one found, or `None`.
396 402 fn get_inner(
397 403 &self,
398 404 section: &[u8],
399 405 item: &[u8],
400 406 ) -> Option<(&ConfigLayer, &ConfigValue)> {
401 407 for layer in self.layers.iter().rev() {
402 408 if !layer.trusted {
403 409 continue;
404 410 }
405 411 if let Some(v) = layer.get(&section, &item) {
406 412 return Some((&layer, v));
407 413 }
408 414 }
409 415 None
410 416 }
411 417
412 418 /// Return all keys defined for the given section
413 419 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
414 420 self.layers
415 421 .iter()
416 422 .flat_map(|layer| layer.iter_keys(section))
417 423 .collect()
418 424 }
419 425
420 426 /// Returns whether any key is defined in the given section
421 427 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
422 428 self.layers
423 429 .iter()
424 430 .any(|layer| layer.has_non_empty_section(section))
425 431 }
426 432
427 433 /// Yields (key, value) pairs for everything in the given section
428 434 pub fn iter_section<'a>(
429 435 &'a self,
430 436 section: &'a [u8],
431 437 ) -> impl Iterator<Item = (&[u8], &[u8])> + 'a {
432 438 // TODO: Use `Iterator`’s `.peekable()` when its `peek_mut` is
433 439 // available:
434 440 // https://doc.rust-lang.org/nightly/std/iter/struct.Peekable.html#method.peek_mut
435 441 struct Peekable<I: Iterator> {
436 442 iter: I,
437 443 /// Remember a peeked value, even if it was None.
438 444 peeked: Option<Option<I::Item>>,
439 445 }
440 446
441 447 impl<I: Iterator> Peekable<I> {
442 448 fn new(iter: I) -> Self {
443 449 Self { iter, peeked: None }
444 450 }
445 451
446 452 fn next(&mut self) {
447 453 self.peeked = None
448 454 }
449 455
450 456 fn peek_mut(&mut self) -> Option<&mut I::Item> {
451 457 let iter = &mut self.iter;
452 458 self.peeked.get_or_insert_with(|| iter.next()).as_mut()
453 459 }
454 460 }
455 461
456 462 // Deduplicate keys redefined in multiple layers
457 463 let mut keys_already_seen = HashSet::new();
458 464 let mut key_is_new =
459 465 move |&(key, _value): &(&'a [u8], &'a [u8])| -> bool {
460 466 keys_already_seen.insert(key)
461 467 };
462 468 // This is similar to `flat_map` + `filter_map`, except with a single
463 469 // closure that owns `key_is_new` (and therefore the
464 470 // `keys_already_seen` set):
465 471 let mut layer_iters = Peekable::new(
466 472 self.layers
467 473 .iter()
468 474 .rev()
469 475 .map(move |layer| layer.iter_section(section)),
470 476 );
471 477 std::iter::from_fn(move || loop {
472 478 if let Some(pair) = layer_iters.peek_mut()?.find(&mut key_is_new) {
473 479 return Some(pair);
474 480 } else {
475 481 layer_iters.next();
476 482 }
477 483 })
478 484 }
479 485
480 486 /// Get raw values bytes from all layers (even untrusted ones) in order
481 487 /// of precedence.
482 488 #[cfg(test)]
483 489 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
484 490 let mut res = vec![];
485 491 for layer in self.layers.iter().rev() {
486 492 if let Some(v) = layer.get(&section, &item) {
487 493 res.push(v.bytes.as_ref());
488 494 }
489 495 }
490 496 res
491 497 }
492 498 }
493 499
494 500 #[cfg(test)]
495 501 mod tests {
496 502 use super::*;
497 503 use pretty_assertions::assert_eq;
498 504 use std::fs::File;
499 505 use std::io::Write;
500 506
501 507 #[test]
502 508 fn test_include_layer_ordering() {
503 509 let tmpdir = tempfile::tempdir().unwrap();
504 510 let tmpdir_path = tmpdir.path();
505 511 let mut included_file =
506 512 File::create(&tmpdir_path.join("included.rc")).unwrap();
507 513
508 514 included_file.write_all(b"[section]\nitem=value1").unwrap();
509 515 let base_config_path = tmpdir_path.join("base.rc");
510 516 let mut config_file = File::create(&base_config_path).unwrap();
511 517 let data =
512 518 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
513 519 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
514 520 config_file.write_all(data).unwrap();
515 521
516 522 let sources = vec![ConfigSource::AbsPath(base_config_path)];
517 523 let config = Config::load_from_explicit_sources(sources)
518 524 .expect("expected valid config");
519 525
520 526 let (_, value) = config.get_inner(b"section", b"item").unwrap();
521 527 assert_eq!(
522 528 value,
523 529 &ConfigValue {
524 530 bytes: b"value2".to_vec(),
525 531 line: Some(4)
526 532 }
527 533 );
528 534
529 535 let value = config.get(b"section", b"item").unwrap();
530 536 assert_eq!(value, b"value2",);
531 537 assert_eq!(
532 538 config.get_all(b"section", b"item"),
533 539 [b"value2", b"value1", b"value0"]
534 540 );
535 541
536 542 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
537 543 assert_eq!(
538 544 config.get_byte_size(b"section2", b"size").unwrap(),
539 545 Some(1024 + 512)
540 546 );
541 547 assert!(config.get_u32(b"section2", b"not-count").is_err());
542 548 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
543 549 }
544 550 }
@@ -1,341 +1,343 b''
1 1 // layer.rs
2 2 //
3 3 // Copyright 2020
4 4 // Valentin Gatien-Baron,
5 5 // Raphaël Gomès <rgomes@octobus.net>
6 6 //
7 7 // This software may be used and distributed according to the terms of the
8 8 // GNU General Public License version 2 or any later version.
9 9
10 10 use crate::errors::HgError;
11 11 use crate::exit_codes::CONFIG_PARSE_ERROR_ABORT;
12 12 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
13 13 use format_bytes::{format_bytes, write_bytes, DisplayBytes};
14 14 use lazy_static::lazy_static;
15 15 use regex::bytes::Regex;
16 16 use std::collections::HashMap;
17 17 use std::path::{Path, PathBuf};
18 18
19 19 lazy_static! {
20 20 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
21 21 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
22 22 /// Continuation whitespace
23 23 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
24 24 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
25 25 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
26 26 /// A directive that allows for removing previous entries
27 27 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
28 28 /// A directive that allows for including other config files
29 29 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
30 30 }
31 31
32 32 /// All config values separated by layers of precedence.
33 33 /// Each config source may be split in multiple layers if `%include` directives
34 34 /// are used.
35 35 /// TODO detail the general precedence
36 36 #[derive(Clone)]
37 37 pub struct ConfigLayer {
38 38 /// Mapping of the sections to their items
39 39 sections: HashMap<Vec<u8>, ConfigItem>,
40 40 /// All sections (and their items/values) in a layer share the same origin
41 41 pub origin: ConfigOrigin,
42 42 /// Whether this layer comes from a trusted user or group
43 43 pub trusted: bool,
44 44 }
45 45
46 46 impl ConfigLayer {
47 47 pub fn new(origin: ConfigOrigin) -> Self {
48 48 ConfigLayer {
49 49 sections: HashMap::new(),
50 50 trusted: true, // TODO check
51 51 origin,
52 52 }
53 53 }
54 54
55 55 /// Parse `--config` CLI arguments and return a layer if there’s any
56 56 pub(crate) fn parse_cli_args(
57 57 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
58 58 ) -> Result<Option<Self>, ConfigError> {
59 59 fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
60 60 use crate::utils::SliceExt;
61 61
62 62 let (section_and_item, value) = arg.split_2(b'=')?;
63 63 let (section, item) = section_and_item.trim().split_2(b'.')?;
64 64 Some((
65 65 section.to_owned(),
66 66 item.to_owned(),
67 67 value.trim().to_owned(),
68 68 ))
69 69 }
70 70
71 71 let mut layer = Self::new(ConfigOrigin::CommandLine);
72 72 for arg in cli_config_args {
73 73 let arg = arg.as_ref();
74 74 if let Some((section, item, value)) = parse_one(arg) {
75 75 layer.add(section, item, value, None);
76 76 } else {
77 77 Err(HgError::abort(
78 78 format!(
79 79 "abort: malformed --config option: '{}' \
80 80 (use --config section.name=value)",
81 81 String::from_utf8_lossy(arg),
82 82 ),
83 83 CONFIG_PARSE_ERROR_ABORT,
84 84 ))?
85 85 }
86 86 }
87 87 if layer.sections.is_empty() {
88 88 Ok(None)
89 89 } else {
90 90 Ok(Some(layer))
91 91 }
92 92 }
93 93
94 94 /// Returns whether this layer comes from `--config` CLI arguments
95 95 pub(crate) fn is_from_command_line(&self) -> bool {
96 96 if let ConfigOrigin::CommandLine = self.origin {
97 97 true
98 98 } else {
99 99 false
100 100 }
101 101 }
102 102
103 103 /// Add an entry to the config, overwriting the old one if already present.
104 104 pub fn add(
105 105 &mut self,
106 106 section: Vec<u8>,
107 107 item: Vec<u8>,
108 108 value: Vec<u8>,
109 109 line: Option<usize>,
110 110 ) {
111 111 self.sections
112 112 .entry(section)
113 113 .or_insert_with(|| HashMap::new())
114 114 .insert(item, ConfigValue { bytes: value, line });
115 115 }
116 116
117 117 /// Returns the config value in `<section>.<item>` if it exists
118 118 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
119 119 Some(self.sections.get(section)?.get(item)?)
120 120 }
121 121
122 122 /// Returns the keys defined in the given section
123 123 pub fn iter_keys(&self, section: &[u8]) -> impl Iterator<Item = &[u8]> {
124 124 self.sections
125 125 .get(section)
126 126 .into_iter()
127 127 .flat_map(|section| section.keys().map(|vec| &**vec))
128 128 }
129 129
130 130 /// Returns the (key, value) pairs defined in the given section
131 131 pub fn iter_section<'layer>(
132 132 &'layer self,
133 133 section: &[u8],
134 134 ) -> impl Iterator<Item = (&'layer [u8], &'layer [u8])> {
135 135 self.sections
136 136 .get(section)
137 137 .into_iter()
138 138 .flat_map(|section| section.iter().map(|(k, v)| (&**k, &*v.bytes)))
139 139 }
140 140
141 141 /// Returns whether any key is defined in the given section
142 142 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
143 143 self.sections
144 144 .get(section)
145 145 .map_or(false, |section| !section.is_empty())
146 146 }
147 147
148 148 pub fn is_empty(&self) -> bool {
149 149 self.sections.is_empty()
150 150 }
151 151
152 152 /// Returns a `Vec` of layers in order of precedence (so, in read order),
153 153 /// recursively parsing the `%include` directives if any.
154 154 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
155 155 let mut layers = vec![];
156 156
157 157 // Discard byte order mark if any
158 158 let data = if data.starts_with(b"\xef\xbb\xbf") {
159 159 &data[3..]
160 160 } else {
161 161 data
162 162 };
163 163
164 164 // TODO check if it's trusted
165 165 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
166 166
167 167 let mut lines_iter =
168 168 data.split(|b| *b == b'\n').enumerate().peekable();
169 169 let mut section = b"".to_vec();
170 170
171 171 while let Some((index, bytes)) = lines_iter.next() {
172 172 let line = Some(index + 1);
173 173 if let Some(m) = INCLUDE_RE.captures(&bytes) {
174 174 let filename_bytes = &m[1];
175 175 let filename_bytes = crate::utils::expand_vars(filename_bytes);
176 176 // `Path::parent` only fails for the root directory,
177 177 // which `src` can’t be since we’ve managed to open it as a
178 178 // file.
179 179 let dir = src
180 180 .parent()
181 181 .expect("Path::parent fail on a file we’ve read");
182 182 // `Path::join` with an absolute argument correctly ignores the
183 183 // base path
184 184 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
185 185 match std::fs::read(&filename) {
186 186 Ok(data) => {
187 187 layers.push(current_layer);
188 188 layers.extend(Self::parse(&filename, &data)?);
189 189 current_layer =
190 190 Self::new(ConfigOrigin::File(src.to_owned()));
191 191 }
192 192 Err(error) => {
193 193 if error.kind() != std::io::ErrorKind::NotFound {
194 194 return Err(ConfigParseError {
195 195 origin: ConfigOrigin::File(src.to_owned()),
196 196 line,
197 197 message: format_bytes!(
198 198 b"cannot include {} ({})",
199 199 filename_bytes,
200 200 format_bytes::Utf8(error)
201 201 ),
202 202 }
203 203 .into());
204 204 }
205 205 }
206 206 }
207 207 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
208 208 } else if let Some(m) = SECTION_RE.captures(&bytes) {
209 209 section = m[1].to_vec();
210 210 } else if let Some(m) = ITEM_RE.captures(&bytes) {
211 211 let item = m[1].to_vec();
212 212 let mut value = m[2].to_vec();
213 213 loop {
214 214 match lines_iter.peek() {
215 215 None => break,
216 216 Some((_, v)) => {
217 217 if let Some(_) = COMMENT_RE.captures(&v) {
218 218 } else if let Some(_) = CONT_RE.captures(&v) {
219 219 value.extend(b"\n");
220 220 value.extend(&m[1]);
221 221 } else {
222 222 break;
223 223 }
224 224 }
225 225 };
226 226 lines_iter.next();
227 227 }
228 228 current_layer.add(section.clone(), item, value, line);
229 229 } else if let Some(m) = UNSET_RE.captures(&bytes) {
230 230 if let Some(map) = current_layer.sections.get_mut(&section) {
231 231 map.remove(&m[1]);
232 232 }
233 233 } else {
234 234 let message = if bytes.starts_with(b" ") {
235 235 format_bytes!(b"unexpected leading whitespace: {}", bytes)
236 236 } else {
237 237 bytes.to_owned()
238 238 };
239 239 return Err(ConfigParseError {
240 240 origin: ConfigOrigin::File(src.to_owned()),
241 241 line,
242 242 message,
243 243 }
244 244 .into());
245 245 }
246 246 }
247 247 if !current_layer.is_empty() {
248 248 layers.push(current_layer);
249 249 }
250 250 Ok(layers)
251 251 }
252 252 }
253 253
254 254 impl DisplayBytes for ConfigLayer {
255 255 fn display_bytes(
256 256 &self,
257 257 out: &mut dyn std::io::Write,
258 258 ) -> std::io::Result<()> {
259 259 let mut sections: Vec<_> = self.sections.iter().collect();
260 260 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
261 261
262 262 for (section, items) in sections.into_iter() {
263 263 let mut items: Vec<_> = items.into_iter().collect();
264 264 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
265 265
266 266 for (item, config_entry) in items {
267 267 write_bytes!(
268 268 out,
269 269 b"{}.{}={} # {}\n",
270 270 section,
271 271 item,
272 272 &config_entry.bytes,
273 273 &self.origin,
274 274 )?
275 275 }
276 276 }
277 277 Ok(())
278 278 }
279 279 }
280 280
281 281 /// Mapping of section item to value.
282 282 /// In the following:
283 283 /// ```text
284 284 /// [ui]
285 285 /// paginate=no
286 286 /// ```
287 287 /// "paginate" is the section item and "no" the value.
288 288 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
289 289
290 290 #[derive(Clone, Debug, PartialEq)]
291 291 pub struct ConfigValue {
292 292 /// The raw bytes of the value (be it from the CLI, env or from a file)
293 293 pub bytes: Vec<u8>,
294 294 /// Only present if the value comes from a file, 1-indexed.
295 295 pub line: Option<usize>,
296 296 }
297 297
298 298 #[derive(Clone, Debug)]
299 299 pub enum ConfigOrigin {
300 300 /// From a configuration file
301 301 File(PathBuf),
302 302 /// From a `--config` CLI argument
303 303 CommandLine,
304 /// From a `--color` CLI argument
305 CommandLineColor,
304 306 /// From environment variables like `$PAGER` or `$EDITOR`
305 307 Environment(Vec<u8>),
306 /* TODO cli
307 * TODO defaults (configitems.py)
308 /* TODO defaults (configitems.py)
308 309 * TODO extensions
309 310 * TODO Python resources?
310 311 * Others? */
311 312 }
312 313
313 314 impl DisplayBytes for ConfigOrigin {
314 315 fn display_bytes(
315 316 &self,
316 317 out: &mut dyn std::io::Write,
317 318 ) -> std::io::Result<()> {
318 319 match self {
319 320 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
320 321 ConfigOrigin::CommandLine => out.write_all(b"--config"),
322 ConfigOrigin::CommandLineColor => out.write_all(b"--color"),
321 323 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
322 324 }
323 325 }
324 326 }
325 327
326 328 #[derive(Debug)]
327 329 pub struct ConfigParseError {
328 330 pub origin: ConfigOrigin,
329 331 pub line: Option<usize>,
330 332 pub message: Vec<u8>,
331 333 }
332 334
333 335 #[derive(Debug, derive_more::From)]
334 336 pub enum ConfigError {
335 337 Parse(ConfigParseError),
336 338 Other(HgError),
337 339 }
338 340
339 341 fn make_regex(pattern: &'static str) -> Regex {
340 342 Regex::new(pattern).expect("expected a valid regex")
341 343 }
@@ -1,690 +1,714 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 error;
21 21 mod ui;
22 22 pub mod utils {
23 23 pub mod path_utils;
24 24 }
25 25
26 26 fn main_with_result(
27 27 process_start_time: &blackbox::ProcessStartTime,
28 28 ui: &ui::Ui,
29 29 repo: Result<&Repo, &NoRepoInCwdError>,
30 30 config: &Config,
31 31 ) -> Result<(), CommandError> {
32 32 check_unsupported(config, repo, ui)?;
33 33
34 34 let app = App::new("rhg")
35 35 .global_setting(AppSettings::AllowInvalidUtf8)
36 36 .global_setting(AppSettings::DisableVersion)
37 37 .setting(AppSettings::SubcommandRequired)
38 38 .setting(AppSettings::VersionlessSubcommands)
39 39 .arg(
40 40 Arg::with_name("repository")
41 41 .help("repository root directory")
42 42 .short("-R")
43 43 .long("--repository")
44 44 .value_name("REPO")
45 45 .takes_value(true)
46 46 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
47 47 .global(true),
48 48 )
49 49 .arg(
50 50 Arg::with_name("config")
51 51 .help("set/override config option (use 'section.name=value')")
52 52 .long("--config")
53 53 .value_name("CONFIG")
54 54 .takes_value(true)
55 55 .global(true)
56 56 // Ok: `--config section.key1=val --config section.key2=val2`
57 57 .multiple(true)
58 58 // Not ok: `--config section.key1=val section.key2=val2`
59 59 .number_of_values(1),
60 60 )
61 61 .arg(
62 62 Arg::with_name("cwd")
63 63 .help("change working directory")
64 64 .long("--cwd")
65 65 .value_name("DIR")
66 66 .takes_value(true)
67 67 .global(true),
68 68 )
69 .arg(
70 Arg::with_name("color")
71 .help("when to colorize (boolean, always, auto, never, or debug)")
72 .long("--color")
73 .value_name("TYPE")
74 .takes_value(true)
75 .global(true),
76 )
69 77 .version("0.0.1");
70 78 let app = add_subcommand_args(app);
71 79
72 80 let matches = app.clone().get_matches_safe()?;
73 81
74 82 let (subcommand_name, subcommand_matches) = matches.subcommand();
75 83
76 84 // Mercurial allows users to define "defaults" for commands, fallback
77 85 // if a default is detected for the current command
78 86 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
79 87 if defaults?.is_some() {
80 88 let msg = "`defaults` config set";
81 89 return Err(CommandError::unsupported(msg));
82 90 }
83 91
84 92 for prefix in ["pre", "post", "fail"].iter() {
85 93 // Mercurial allows users to define generic hooks for commands,
86 94 // fallback if any are detected
87 95 let item = format!("{}-{}", prefix, subcommand_name);
88 96 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
89 97 if hook_for_command.is_some() {
90 98 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
91 99 return Err(CommandError::unsupported(msg));
92 100 }
93 101 }
94 102 let run = subcommand_run_fn(subcommand_name)
95 103 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
96 104 let subcommand_args = subcommand_matches
97 105 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
98 106
99 107 let invocation = CliInvocation {
100 108 ui,
101 109 subcommand_args,
102 110 config,
103 111 repo,
104 112 };
105 113
106 114 if let Ok(repo) = repo {
107 115 // We don't support subrepos, fallback if the subrepos file is present
108 116 if repo.working_directory_vfs().join(".hgsub").exists() {
109 117 let msg = "subrepos (.hgsub is present)";
110 118 return Err(CommandError::unsupported(msg));
111 119 }
112 120 }
113 121
114 122 if config.is_extension_enabled(b"blackbox") {
115 123 let blackbox =
116 124 blackbox::Blackbox::new(&invocation, process_start_time)?;
117 125 blackbox.log_command_start();
118 126 let result = run(&invocation);
119 127 blackbox.log_command_end(exit_code(
120 128 &result,
121 129 // TODO: show a warning or combine with original error if
122 130 // `get_bool` returns an error
123 131 config
124 132 .get_bool(b"ui", b"detailed-exit-code")
125 133 .unwrap_or(false),
126 134 ));
127 135 result
128 136 } else {
129 137 run(&invocation)
130 138 }
131 139 }
132 140
133 141 fn main() {
134 142 // Run this first, before we find out if the blackbox extension is even
135 143 // enabled, in order to include everything in-between in the duration
136 144 // measurements. Reading config files can be slow if they’re on NFS.
137 145 let process_start_time = blackbox::ProcessStartTime::now();
138 146
139 147 env_logger::init();
140 148
141 149 let early_args = EarlyArgs::parse(std::env::args_os());
142 150
143 151 let initial_current_dir = early_args.cwd.map(|cwd| {
144 152 let cwd = get_path_from_bytes(&cwd);
145 153 std::env::current_dir()
146 154 .and_then(|initial| {
147 155 std::env::set_current_dir(cwd)?;
148 156 Ok(initial)
149 157 })
150 158 .unwrap_or_else(|error| {
151 159 exit(
152 160 &None,
153 161 &Ui::new_infallible(&Config::empty()),
154 162 OnUnsupported::Abort,
155 163 Err(CommandError::abort(format!(
156 164 "abort: {}: '{}'",
157 165 error,
158 166 cwd.display()
159 167 ))),
160 168 false,
161 169 )
162 170 })
163 171 });
164 172
165 173 let mut non_repo_config =
166 174 Config::load_non_repo().unwrap_or_else(|error| {
167 175 // Normally this is decided based on config, but we don’t have that
168 176 // available. As of this writing config loading never returns an
169 177 // "unsupported" error but that is not enforced by the type system.
170 178 let on_unsupported = OnUnsupported::Abort;
171 179
172 180 exit(
173 181 &initial_current_dir,
174 182 &Ui::new_infallible(&Config::empty()),
175 183 on_unsupported,
176 184 Err(error.into()),
177 185 false,
178 186 )
179 187 });
180 188
181 189 non_repo_config
182 .load_cli_args_config(early_args.config)
190 .load_cli_args(early_args.config, early_args.color)
183 191 .unwrap_or_else(|error| {
184 192 exit(
185 193 &initial_current_dir,
186 194 &Ui::new_infallible(&non_repo_config),
187 195 OnUnsupported::from_config(&non_repo_config),
188 196 Err(error.into()),
189 197 non_repo_config
190 198 .get_bool(b"ui", b"detailed-exit-code")
191 199 .unwrap_or(false),
192 200 )
193 201 });
194 202
195 203 if let Some(repo_path_bytes) = &early_args.repo {
196 204 lazy_static::lazy_static! {
197 205 static ref SCHEME_RE: regex::bytes::Regex =
198 206 // Same as `_matchscheme` in `mercurial/util.py`
199 207 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
200 208 }
201 209 if SCHEME_RE.is_match(&repo_path_bytes) {
202 210 exit(
203 211 &initial_current_dir,
204 212 &Ui::new_infallible(&non_repo_config),
205 213 OnUnsupported::from_config(&non_repo_config),
206 214 Err(CommandError::UnsupportedFeature {
207 215 message: format_bytes!(
208 216 b"URL-like --repository {}",
209 217 repo_path_bytes
210 218 ),
211 219 }),
212 220 // TODO: show a warning or combine with original error if
213 221 // `get_bool` returns an error
214 222 non_repo_config
215 223 .get_bool(b"ui", b"detailed-exit-code")
216 224 .unwrap_or(false),
217 225 )
218 226 }
219 227 }
220 228 let repo_arg = early_args.repo.unwrap_or(Vec::new());
221 229 let repo_path: Option<PathBuf> = {
222 230 if repo_arg.is_empty() {
223 231 None
224 232 } else {
225 233 let local_config = {
226 234 if std::env::var_os("HGRCSKIPREPO").is_none() {
227 235 // TODO: handle errors from find_repo_root
228 236 if let Ok(current_dir_path) = Repo::find_repo_root() {
229 237 let config_files = vec![
230 238 ConfigSource::AbsPath(
231 239 current_dir_path.join(".hg/hgrc"),
232 240 ),
233 241 ConfigSource::AbsPath(
234 242 current_dir_path.join(".hg/hgrc-not-shared"),
235 243 ),
236 244 ];
237 245 // TODO: handle errors from
238 246 // `load_from_explicit_sources`
239 247 Config::load_from_explicit_sources(config_files).ok()
240 248 } else {
241 249 None
242 250 }
243 251 } else {
244 252 None
245 253 }
246 254 };
247 255
248 256 let non_repo_config_val = {
249 257 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
250 258 match &non_repo_val {
251 259 Some(val) if val.len() > 0 => home::home_dir()
252 260 .unwrap_or_else(|| PathBuf::from("~"))
253 261 .join(get_path_from_bytes(val))
254 262 .canonicalize()
255 263 // TODO: handle error and make it similar to python
256 264 // implementation maybe?
257 265 .ok(),
258 266 _ => None,
259 267 }
260 268 };
261 269
262 270 let config_val = match &local_config {
263 271 None => non_repo_config_val,
264 272 Some(val) => {
265 273 let local_config_val = val.get(b"paths", &repo_arg);
266 274 match &local_config_val {
267 275 Some(val) if val.len() > 0 => {
268 276 // presence of a local_config assures that
269 277 // current_dir
270 278 // wont result in an Error
271 279 let canpath = hg::utils::current_dir()
272 280 .unwrap()
273 281 .join(get_path_from_bytes(val))
274 282 .canonicalize();
275 283 canpath.ok().or(non_repo_config_val)
276 284 }
277 285 _ => non_repo_config_val,
278 286 }
279 287 }
280 288 };
281 289 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
282 290 }
283 291 };
284 292
285 293 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
286 294 {
287 295 Ok(repo) => Ok(repo),
288 296 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
289 297 // Not finding a repo is not fatal yet, if `-R` was not given
290 298 Err(NoRepoInCwdError { cwd: at })
291 299 }
292 300 Err(error) => exit(
293 301 &initial_current_dir,
294 302 &Ui::new_infallible(&non_repo_config),
295 303 OnUnsupported::from_config(&non_repo_config),
296 304 Err(error.into()),
297 305 // TODO: show a warning or combine with original error if
298 306 // `get_bool` returns an error
299 307 non_repo_config
300 308 .get_bool(b"ui", b"detailed-exit-code")
301 309 .unwrap_or(false),
302 310 ),
303 311 };
304 312
305 313 let config = if let Ok(repo) = &repo_result {
306 314 repo.config()
307 315 } else {
308 316 &non_repo_config
309 317 };
310 318 let ui = Ui::new(&config).unwrap_or_else(|error| {
311 319 exit(
312 320 &initial_current_dir,
313 321 &Ui::new_infallible(&config),
314 322 OnUnsupported::from_config(&config),
315 323 Err(error.into()),
316 324 config
317 325 .get_bool(b"ui", b"detailed-exit-code")
318 326 .unwrap_or(false),
319 327 )
320 328 });
321 329 let on_unsupported = OnUnsupported::from_config(config);
322 330
323 331 let result = main_with_result(
324 332 &process_start_time,
325 333 &ui,
326 334 repo_result.as_ref(),
327 335 config,
328 336 );
329 337 exit(
330 338 &initial_current_dir,
331 339 &ui,
332 340 on_unsupported,
333 341 result,
334 342 // TODO: show a warning or combine with original error if `get_bool`
335 343 // returns an error
336 344 config
337 345 .get_bool(b"ui", b"detailed-exit-code")
338 346 .unwrap_or(false),
339 347 )
340 348 }
341 349
342 350 fn exit_code(
343 351 result: &Result<(), CommandError>,
344 352 use_detailed_exit_code: bool,
345 353 ) -> i32 {
346 354 match result {
347 355 Ok(()) => exit_codes::OK,
348 356 Err(CommandError::Abort {
349 357 message: _,
350 358 detailed_exit_code,
351 359 }) => {
352 360 if use_detailed_exit_code {
353 361 *detailed_exit_code
354 362 } else {
355 363 exit_codes::ABORT
356 364 }
357 365 }
358 366 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
359 367
360 368 // Exit with a specific code and no error message to let a potential
361 369 // wrapper script fallback to Python-based Mercurial.
362 370 Err(CommandError::UnsupportedFeature { .. }) => {
363 371 exit_codes::UNIMPLEMENTED
364 372 }
365 373 }
366 374 }
367 375
368 376 fn exit(
369 377 initial_current_dir: &Option<PathBuf>,
370 378 ui: &Ui,
371 379 mut on_unsupported: OnUnsupported,
372 380 result: Result<(), CommandError>,
373 381 use_detailed_exit_code: bool,
374 382 ) -> ! {
375 383 if let (
376 384 OnUnsupported::Fallback { executable },
377 385 Err(CommandError::UnsupportedFeature { .. }),
378 386 ) = (&on_unsupported, &result)
379 387 {
380 388 let mut args = std::env::args_os();
381 389 let executable = match executable {
382 390 None => {
383 391 exit_no_fallback(
384 392 ui,
385 393 OnUnsupported::Abort,
386 394 Err(CommandError::abort(
387 395 "abort: 'rhg.on-unsupported=fallback' without \
388 396 'rhg.fallback-executable' set.",
389 397 )),
390 398 false,
391 399 );
392 400 }
393 401 Some(executable) => executable,
394 402 };
395 403 let executable_path = get_path_from_bytes(&executable);
396 404 let this_executable = args.next().expect("exepcted argv[0] to exist");
397 405 if executable_path == &PathBuf::from(this_executable) {
398 406 // Avoid spawning infinitely many processes until resource
399 407 // exhaustion.
400 408 let _ = ui.write_stderr(&format_bytes!(
401 409 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
402 410 points to `rhg` itself.\n",
403 411 executable
404 412 ));
405 413 on_unsupported = OnUnsupported::Abort
406 414 } else {
407 415 // `args` is now `argv[1..]` since we’ve already consumed
408 416 // `argv[0]`
409 417 let mut command = Command::new(executable_path);
410 418 command.args(args);
411 419 if let Some(initial) = initial_current_dir {
412 420 command.current_dir(initial);
413 421 }
414 422 let result = command.status();
415 423 match result {
416 424 Ok(status) => std::process::exit(
417 425 status.code().unwrap_or(exit_codes::ABORT),
418 426 ),
419 427 Err(error) => {
420 428 let _ = ui.write_stderr(&format_bytes!(
421 429 b"tried to fall back to a '{}' sub-process but got error {}\n",
422 430 executable, format_bytes::Utf8(error)
423 431 ));
424 432 on_unsupported = OnUnsupported::Abort
425 433 }
426 434 }
427 435 }
428 436 }
429 437 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
430 438 }
431 439
432 440 fn exit_no_fallback(
433 441 ui: &Ui,
434 442 on_unsupported: OnUnsupported,
435 443 result: Result<(), CommandError>,
436 444 use_detailed_exit_code: bool,
437 445 ) -> ! {
438 446 match &result {
439 447 Ok(_) => {}
440 448 Err(CommandError::Unsuccessful) => {}
441 449 Err(CommandError::Abort {
442 450 message,
443 451 detailed_exit_code: _,
444 452 }) => {
445 453 if !message.is_empty() {
446 454 // Ignore errors when writing to stderr, we’re already exiting
447 455 // with failure code so there’s not much more we can do.
448 456 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
449 457 }
450 458 }
451 459 Err(CommandError::UnsupportedFeature { message }) => {
452 460 match on_unsupported {
453 461 OnUnsupported::Abort => {
454 462 let _ = ui.write_stderr(&format_bytes!(
455 463 b"unsupported feature: {}\n",
456 464 message
457 465 ));
458 466 }
459 467 OnUnsupported::AbortSilent => {}
460 468 OnUnsupported::Fallback { .. } => unreachable!(),
461 469 }
462 470 }
463 471 }
464 472 std::process::exit(exit_code(&result, use_detailed_exit_code))
465 473 }
466 474
467 475 macro_rules! subcommands {
468 476 ($( $command: ident )+) => {
469 477 mod commands {
470 478 $(
471 479 pub mod $command;
472 480 )+
473 481 }
474 482
475 483 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
476 484 app
477 485 $(
478 486 .subcommand(commands::$command::args())
479 487 )+
480 488 }
481 489
482 490 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
483 491
484 492 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
485 493 match name {
486 494 $(
487 495 stringify!($command) => Some(commands::$command::run),
488 496 )+
489 497 _ => None,
490 498 }
491 499 }
492 500 };
493 501 }
494 502
495 503 subcommands! {
496 504 cat
497 505 debugdata
498 506 debugrequirements
499 507 debugignorerhg
500 508 files
501 509 root
502 510 config
503 511 status
504 512 }
505 513
506 514 pub struct CliInvocation<'a> {
507 515 ui: &'a Ui,
508 516 subcommand_args: &'a ArgMatches<'a>,
509 517 config: &'a Config,
510 518 /// References inside `Result` is a bit peculiar but allow
511 519 /// `invocation.repo?` to work out with `&CliInvocation` since this
512 520 /// `Result` type is `Copy`.
513 521 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
514 522 }
515 523
516 524 struct NoRepoInCwdError {
517 525 cwd: PathBuf,
518 526 }
519 527
520 528 /// CLI arguments to be parsed "early" in order to be able to read
521 529 /// configuration before using Clap. Ideally we would also use Clap for this,
522 530 /// see <https://github.com/clap-rs/clap/discussions/2366>.
523 531 ///
524 532 /// These arguments are still declared when we do use Clap later, so that Clap
525 533 /// does not return an error for their presence.
526 534 struct EarlyArgs {
527 535 /// Values of all `--config` arguments. (Possibly none)
528 536 config: Vec<Vec<u8>>,
537 /// Value of all the `--color` argument, if any.
538 color: Option<Vec<u8>>,
529 539 /// Value of the `-R` or `--repository` argument, if any.
530 540 repo: Option<Vec<u8>>,
531 541 /// Value of the `--cwd` argument, if any.
532 542 cwd: Option<Vec<u8>>,
533 543 }
534 544
535 545 impl EarlyArgs {
536 546 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
537 547 let mut args = args.into_iter().map(get_bytes_from_os_str);
538 548 let mut config = Vec::new();
549 let mut color = None;
539 550 let mut repo = None;
540 551 let mut cwd = None;
541 552 // Use `while let` instead of `for` so that we can also call
542 553 // `args.next()` inside the loop.
543 554 while let Some(arg) = args.next() {
544 555 if arg == b"--config" {
545 556 if let Some(value) = args.next() {
546 557 config.push(value)
547 558 }
548 559 } else if let Some(value) = arg.drop_prefix(b"--config=") {
549 560 config.push(value.to_owned())
550 561 }
551 562
563 if arg == b"--color" {
564 if let Some(value) = args.next() {
565 color = Some(value)
566 }
567 } else if let Some(value) = arg.drop_prefix(b"--color=") {
568 color = Some(value.to_owned())
569 }
570
552 571 if arg == b"--cwd" {
553 572 if let Some(value) = args.next() {
554 573 cwd = Some(value)
555 574 }
556 575 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
557 576 cwd = Some(value.to_owned())
558 577 }
559 578
560 579 if arg == b"--repository" || arg == b"-R" {
561 580 if let Some(value) = args.next() {
562 581 repo = Some(value)
563 582 }
564 583 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
565 584 repo = Some(value.to_owned())
566 585 } else if let Some(value) = arg.drop_prefix(b"-R") {
567 586 repo = Some(value.to_owned())
568 587 }
569 588 }
570 Self { config, repo, cwd }
589 Self {
590 config,
591 color,
592 repo,
593 cwd,
594 }
571 595 }
572 596 }
573 597
574 598 /// What to do when encountering some unsupported feature.
575 599 ///
576 600 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
577 601 enum OnUnsupported {
578 602 /// Print an error message describing what feature is not supported,
579 603 /// and exit with code 252.
580 604 Abort,
581 605 /// Silently exit with code 252.
582 606 AbortSilent,
583 607 /// Try running a Python implementation
584 608 Fallback { executable: Option<Vec<u8>> },
585 609 }
586 610
587 611 impl OnUnsupported {
588 612 const DEFAULT: Self = OnUnsupported::Abort;
589 613
590 614 fn from_config(config: &Config) -> Self {
591 615 match config
592 616 .get(b"rhg", b"on-unsupported")
593 617 .map(|value| value.to_ascii_lowercase())
594 618 .as_deref()
595 619 {
596 620 Some(b"abort") => OnUnsupported::Abort,
597 621 Some(b"abort-silent") => OnUnsupported::AbortSilent,
598 622 Some(b"fallback") => OnUnsupported::Fallback {
599 623 executable: config
600 624 .get(b"rhg", b"fallback-executable")
601 625 .map(|x| x.to_owned()),
602 626 },
603 627 None => Self::DEFAULT,
604 628 Some(_) => {
605 629 // TODO: warn about unknown config value
606 630 Self::DEFAULT
607 631 }
608 632 }
609 633 }
610 634 }
611 635
612 636 /// The `*` extension is an edge-case for config sub-options that apply to all
613 637 /// extensions. For now, only `:required` exists, but that may change in the
614 638 /// future.
615 639 const SUPPORTED_EXTENSIONS: &[&[u8]] =
616 640 &[b"blackbox", b"share", b"sparse", b"narrow", b"*"];
617 641
618 642 fn check_extensions(config: &Config) -> Result<(), CommandError> {
619 643 let enabled: HashSet<&[u8]> = config
620 644 .get_section_keys(b"extensions")
621 645 .into_iter()
622 646 .map(|extension| {
623 647 // Ignore extension suboptions. Only `required` exists for now.
624 648 // `rhg` either supports an extension or doesn't, so it doesn't
625 649 // make sense to consider the loading of an extension.
626 650 extension.split_2(b':').unwrap_or((extension, b"")).0
627 651 })
628 652 .collect();
629 653
630 654 let mut unsupported = enabled;
631 655 for supported in SUPPORTED_EXTENSIONS {
632 656 unsupported.remove(supported);
633 657 }
634 658
635 659 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
636 660 {
637 661 for ignored in ignored_list {
638 662 unsupported.remove(ignored.as_slice());
639 663 }
640 664 }
641 665
642 666 if unsupported.is_empty() {
643 667 Ok(())
644 668 } else {
645 669 Err(CommandError::UnsupportedFeature {
646 670 message: format_bytes!(
647 671 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
648 672 join(unsupported, b", ")
649 673 ),
650 674 })
651 675 }
652 676 }
653 677
654 678 fn check_unsupported(
655 679 config: &Config,
656 680 repo: Result<&Repo, &NoRepoInCwdError>,
657 681 ui: &ui::Ui,
658 682 ) -> Result<(), CommandError> {
659 683 check_extensions(config)?;
660 684
661 685 if std::env::var_os("HG_PENDING").is_some() {
662 686 // TODO: only if the value is `== repo.working_directory`?
663 687 // What about relative v.s. absolute paths?
664 688 Err(CommandError::unsupported("$HG_PENDING"))?
665 689 }
666 690
667 691 if let Ok(repo) = repo {
668 692 if repo.has_subrepos()? {
669 693 Err(CommandError::unsupported("sub-repositories"))?
670 694 }
671 695 }
672 696
673 697 if config.has_non_empty_section(b"encode") {
674 698 Err(CommandError::unsupported("[encode] config"))?
675 699 }
676 700
677 701 if config.has_non_empty_section(b"decode") {
678 702 Err(CommandError::unsupported("[decode] config"))?
679 703 }
680 704
681 705 if let Some(color) = config.get(b"ui", b"color") {
682 706 if (color == b"always" || color == b"debug")
683 707 && !ui.plain(Some("color"))
684 708 {
685 709 Err(CommandError::unsupported("colored output"))?
686 710 }
687 711 }
688 712
689 713 Ok(())
690 714 }
General Comments 0
You need to be logged in to leave comments. Login now