##// END OF EJS Templates
rhg: Switch rhg.ignored-extensions config to Python-compatible list syntax...
Simon Sapin -
r48763:cff41e16 default
parent child Browse files
Show More
@@ -1,496 +1,469 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 use crate::utils::SliceExt;
17 16 use format_bytes::{write_bytes, DisplayBytes};
18 17 use std::collections::HashSet;
19 18 use std::env;
20 19 use std::fmt;
21 20 use std::path::{Path, PathBuf};
22 21 use std::str;
23 22
24 23 use crate::errors::{HgResultExt, IoResultExt};
25 24
26 25 /// Holds the config values for the current repository
27 26 /// TODO update this docstring once we support more sources
28 27 #[derive(Clone)]
29 28 pub struct Config {
30 29 layers: Vec<layer::ConfigLayer>,
31 30 }
32 31
33 32 impl DisplayBytes for Config {
34 33 fn display_bytes(
35 34 &self,
36 35 out: &mut dyn std::io::Write,
37 36 ) -> std::io::Result<()> {
38 37 for (index, layer) in self.layers.iter().rev().enumerate() {
39 38 write_bytes!(
40 39 out,
41 40 b"==== Layer {} (trusted: {}) ====\n{}",
42 41 index,
43 42 if layer.trusted {
44 43 &b"yes"[..]
45 44 } else {
46 45 &b"no"[..]
47 46 },
48 47 layer
49 48 )?;
50 49 }
51 50 Ok(())
52 51 }
53 52 }
54 53
55 54 pub enum ConfigSource {
56 55 /// Absolute path to a config file
57 56 AbsPath(PathBuf),
58 57 /// Already parsed (from the CLI, env, Python resources, etc.)
59 58 Parsed(layer::ConfigLayer),
60 59 }
61 60
62 61 #[derive(Debug)]
63 62 pub struct ConfigValueParseError {
64 63 pub origin: ConfigOrigin,
65 64 pub line: Option<usize>,
66 65 pub section: Vec<u8>,
67 66 pub item: Vec<u8>,
68 67 pub value: Vec<u8>,
69 68 pub expected_type: &'static str,
70 69 }
71 70
72 71 impl fmt::Display for ConfigValueParseError {
73 72 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74 73 // TODO: add origin and line number information, here and in
75 74 // corresponding python code
76 75 write!(
77 76 f,
78 77 "config error: {}.{} is not a {} ('{}')",
79 78 String::from_utf8_lossy(&self.section),
80 79 String::from_utf8_lossy(&self.item),
81 80 self.expected_type,
82 81 String::from_utf8_lossy(&self.value)
83 82 )
84 83 }
85 84 }
86 85
87 86 impl Config {
88 87 /// Load system and user configuration from various files.
89 88 ///
90 89 /// This is also affected by some environment variables.
91 90 pub fn load_non_repo() -> Result<Self, ConfigError> {
92 91 let mut config = Self { layers: Vec::new() };
93 92 let opt_rc_path = env::var_os("HGRCPATH");
94 93 // HGRCPATH replaces system config
95 94 if opt_rc_path.is_none() {
96 95 config.add_system_config()?
97 96 }
98 97
99 98 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
100 99 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
101 100 config.add_for_environment_variable("PAGER", b"pager", b"pager");
102 101
103 102 // These are set by `run-tests.py --rhg` to enable fallback for the
104 103 // entire test suite. Alternatives would be setting configuration
105 104 // through `$HGRCPATH` but some tests override that, or changing the
106 105 // `hg` shell alias to include `--config` but that disrupts tests that
107 106 // print command lines and check expected output.
108 107 config.add_for_environment_variable(
109 108 "RHG_ON_UNSUPPORTED",
110 109 b"rhg",
111 110 b"on-unsupported",
112 111 );
113 112 config.add_for_environment_variable(
114 113 "RHG_FALLBACK_EXECUTABLE",
115 114 b"rhg",
116 115 b"fallback-executable",
117 116 );
118 117
119 118 // HGRCPATH replaces user config
120 119 if opt_rc_path.is_none() {
121 120 config.add_user_config()?
122 121 }
123 122 if let Some(rc_path) = &opt_rc_path {
124 123 for path in env::split_paths(rc_path) {
125 124 if !path.as_os_str().is_empty() {
126 125 if path.is_dir() {
127 126 config.add_trusted_dir(&path)?
128 127 } else {
129 128 config.add_trusted_file(&path)?
130 129 }
131 130 }
132 131 }
133 132 }
134 133 Ok(config)
135 134 }
136 135
137 136 pub fn load_cli_args_config(
138 137 &mut self,
139 138 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
140 139 ) -> Result<(), ConfigError> {
141 140 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
142 141 self.layers.push(layer)
143 142 }
144 143 Ok(())
145 144 }
146 145
147 146 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
148 147 if let Some(entries) = std::fs::read_dir(path)
149 148 .when_reading_file(path)
150 149 .io_not_found_as_none()?
151 150 {
152 151 let mut file_paths = entries
153 152 .map(|result| {
154 153 result.when_reading_file(path).map(|entry| entry.path())
155 154 })
156 155 .collect::<Result<Vec<_>, _>>()?;
157 156 file_paths.sort();
158 157 for file_path in &file_paths {
159 158 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
160 159 self.add_trusted_file(&file_path)?
161 160 }
162 161 }
163 162 }
164 163 Ok(())
165 164 }
166 165
167 166 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
168 167 if let Some(data) = std::fs::read(path)
169 168 .when_reading_file(path)
170 169 .io_not_found_as_none()?
171 170 {
172 171 self.layers.extend(ConfigLayer::parse(path, &data)?)
173 172 }
174 173 Ok(())
175 174 }
176 175
177 176 fn add_for_environment_variable(
178 177 &mut self,
179 178 var: &str,
180 179 section: &[u8],
181 180 key: &[u8],
182 181 ) {
183 182 if let Some(value) = env::var_os(var) {
184 183 let origin = layer::ConfigOrigin::Environment(var.into());
185 184 let mut layer = ConfigLayer::new(origin);
186 185 layer.add(
187 186 section.to_owned(),
188 187 key.to_owned(),
189 188 get_bytes_from_os_str(value),
190 189 None,
191 190 );
192 191 self.layers.push(layer)
193 192 }
194 193 }
195 194
196 195 #[cfg(unix)] // TODO: other platforms
197 196 fn add_system_config(&mut self) -> Result<(), ConfigError> {
198 197 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
199 198 let etc = prefix.join("etc").join("mercurial");
200 199 self.add_trusted_file(&etc.join("hgrc"))?;
201 200 self.add_trusted_dir(&etc.join("hgrc.d"))
202 201 };
203 202 let root = Path::new("/");
204 203 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
205 204 // instead? TODO: can this be a relative path?
206 205 let hg = crate::utils::current_exe()?;
207 206 // TODO: this order (per-installation then per-system) matches
208 207 // `systemrcpath()` in `mercurial/scmposix.py`, but
209 208 // `mercurial/helptext/config.txt` suggests it should be reversed
210 209 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
211 210 if installation_prefix != root {
212 211 add_for_prefix(&installation_prefix)?
213 212 }
214 213 }
215 214 add_for_prefix(root)?;
216 215 Ok(())
217 216 }
218 217
219 218 #[cfg(unix)] // TODO: other plateforms
220 219 fn add_user_config(&mut self) -> Result<(), ConfigError> {
221 220 let opt_home = home::home_dir();
222 221 if let Some(home) = &opt_home {
223 222 self.add_trusted_file(&home.join(".hgrc"))?
224 223 }
225 224 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
226 225 if !darwin {
227 226 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
228 227 .map(PathBuf::from)
229 228 .or_else(|| opt_home.map(|home| home.join(".config")))
230 229 {
231 230 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
232 231 }
233 232 }
234 233 Ok(())
235 234 }
236 235
237 236 /// Loads in order, which means that the precedence is the same
238 237 /// as the order of `sources`.
239 238 pub fn load_from_explicit_sources(
240 239 sources: Vec<ConfigSource>,
241 240 ) -> Result<Self, ConfigError> {
242 241 let mut layers = vec![];
243 242
244 243 for source in sources.into_iter() {
245 244 match source {
246 245 ConfigSource::Parsed(c) => layers.push(c),
247 246 ConfigSource::AbsPath(c) => {
248 247 // TODO check if it should be trusted
249 248 // mercurial/ui.py:427
250 249 let data = match std::fs::read(&c) {
251 250 Err(_) => continue, // same as the python code
252 251 Ok(data) => data,
253 252 };
254 253 layers.extend(ConfigLayer::parse(&c, &data)?)
255 254 }
256 255 }
257 256 }
258 257
259 258 Ok(Config { layers })
260 259 }
261 260
262 261 /// Loads the per-repository config into a new `Config` which is combined
263 262 /// with `self`.
264 263 pub(crate) fn combine_with_repo(
265 264 &self,
266 265 repo_config_files: &[PathBuf],
267 266 ) -> Result<Self, ConfigError> {
268 267 let (cli_layers, other_layers) = self
269 268 .layers
270 269 .iter()
271 270 .cloned()
272 271 .partition(ConfigLayer::is_from_command_line);
273 272
274 273 let mut repo_config = Self {
275 274 layers: other_layers,
276 275 };
277 276 for path in repo_config_files {
278 277 // TODO: check if this file should be trusted:
279 278 // `mercurial/ui.py:427`
280 279 repo_config.add_trusted_file(path)?;
281 280 }
282 281 repo_config.layers.extend(cli_layers);
283 282 Ok(repo_config)
284 283 }
285 284
286 285 fn get_parse<'config, T: 'config>(
287 286 &'config self,
288 287 section: &[u8],
289 288 item: &[u8],
290 289 expected_type: &'static str,
291 290 parse: impl Fn(&'config [u8]) -> Option<T>,
292 291 ) -> Result<Option<T>, ConfigValueParseError> {
293 292 match self.get_inner(&section, &item) {
294 293 Some((layer, v)) => match parse(&v.bytes) {
295 294 Some(b) => Ok(Some(b)),
296 295 None => Err(ConfigValueParseError {
297 296 origin: layer.origin.to_owned(),
298 297 line: v.line,
299 298 value: v.bytes.to_owned(),
300 299 section: section.to_owned(),
301 300 item: item.to_owned(),
302 301 expected_type,
303 302 }),
304 303 },
305 304 None => Ok(None),
306 305 }
307 306 }
308 307
309 308 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
310 309 /// Otherwise, returns an `Ok(value)` if found, or `None`.
311 310 pub fn get_str(
312 311 &self,
313 312 section: &[u8],
314 313 item: &[u8],
315 314 ) -> Result<Option<&str>, ConfigValueParseError> {
316 315 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
317 316 str::from_utf8(value).ok()
318 317 })
319 318 }
320 319
321 320 /// Returns an `Err` if the first value found is not a valid unsigned
322 321 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
323 322 pub fn get_u32(
324 323 &self,
325 324 section: &[u8],
326 325 item: &[u8],
327 326 ) -> Result<Option<u32>, ConfigValueParseError> {
328 327 self.get_parse(section, item, "valid integer", |value| {
329 328 str::from_utf8(value).ok()?.parse().ok()
330 329 })
331 330 }
332 331
333 332 /// Returns an `Err` if the first value found is not a valid file size
334 333 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
335 334 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
336 335 pub fn get_byte_size(
337 336 &self,
338 337 section: &[u8],
339 338 item: &[u8],
340 339 ) -> Result<Option<u64>, ConfigValueParseError> {
341 340 self.get_parse(section, item, "byte quantity", values::parse_byte_size)
342 341 }
343 342
344 343 /// Returns an `Err` if the first value found is not a valid boolean.
345 344 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
346 345 /// found, or `None`.
347 346 pub fn get_option(
348 347 &self,
349 348 section: &[u8],
350 349 item: &[u8],
351 350 ) -> Result<Option<bool>, ConfigValueParseError> {
352 351 self.get_parse(section, item, "boolean", values::parse_bool)
353 352 }
354 353
355 354 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
356 355 /// if the value is not found, an `Err` if it's not a valid boolean.
357 356 pub fn get_bool(
358 357 &self,
359 358 section: &[u8],
360 359 item: &[u8],
361 360 ) -> Result<bool, ConfigValueParseError> {
362 361 Ok(self.get_option(section, item)?.unwrap_or(false))
363 362 }
364 363
365 /// Returns the corresponding list-value in the config if found, or `None`.
366 ///
367 /// This is appropriate for new configuration keys. The value syntax is
368 /// **not** the same as most existing list-valued config, which has Python
369 /// parsing implemented in `parselist()` in
370 /// `mercurial/utils/stringutil.py`. Faithfully porting that parsing
371 /// algorithm to Rust (including behavior that are arguably bugs)
372 /// turned out to be non-trivial and hasn’t been completed as of this
373 /// writing.
374 ///
375 /// Instead, the "simple" syntax is: split on comma, then trim leading and
376 /// trailing whitespace of each component. Quotes or backslashes are not
377 /// interpreted in any way. Commas are mandatory between values. Values
378 /// that contain a comma are not supported.
379 pub fn get_simple_list(
380 &self,
381 section: &[u8],
382 item: &[u8],
383 ) -> Option<impl Iterator<Item = &[u8]>> {
384 self.get(section, item).map(|value| {
385 value
386 .split(|&byte| byte == b',')
387 .map(|component| component.trim())
388 })
389 }
390
391 364 /// If there is an `item` value in `section`, parse and return a list of
392 365 /// byte strings.
393 366 pub fn get_list(
394 367 &self,
395 368 section: &[u8],
396 369 item: &[u8],
397 370 ) -> Option<Vec<Vec<u8>>> {
398 371 self.get(section, item).map(values::parse_list)
399 372 }
400 373
401 374 /// Returns the raw value bytes of the first one found, or `None`.
402 375 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
403 376 self.get_inner(section, item)
404 377 .map(|(_, value)| value.bytes.as_ref())
405 378 }
406 379
407 380 /// Returns the layer and the value of the first one found, or `None`.
408 381 fn get_inner(
409 382 &self,
410 383 section: &[u8],
411 384 item: &[u8],
412 385 ) -> Option<(&ConfigLayer, &ConfigValue)> {
413 386 for layer in self.layers.iter().rev() {
414 387 if !layer.trusted {
415 388 continue;
416 389 }
417 390 if let Some(v) = layer.get(&section, &item) {
418 391 return Some((&layer, v));
419 392 }
420 393 }
421 394 None
422 395 }
423 396
424 397 /// Return all keys defined for the given section
425 398 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
426 399 self.layers
427 400 .iter()
428 401 .flat_map(|layer| layer.iter_keys(section))
429 402 .collect()
430 403 }
431 404
432 405 /// Get raw values bytes from all layers (even untrusted ones) in order
433 406 /// of precedence.
434 407 #[cfg(test)]
435 408 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
436 409 let mut res = vec![];
437 410 for layer in self.layers.iter().rev() {
438 411 if let Some(v) = layer.get(&section, &item) {
439 412 res.push(v.bytes.as_ref());
440 413 }
441 414 }
442 415 res
443 416 }
444 417 }
445 418
446 419 #[cfg(test)]
447 420 mod tests {
448 421 use super::*;
449 422 use pretty_assertions::assert_eq;
450 423 use std::fs::File;
451 424 use std::io::Write;
452 425
453 426 #[test]
454 427 fn test_include_layer_ordering() {
455 428 let tmpdir = tempfile::tempdir().unwrap();
456 429 let tmpdir_path = tmpdir.path();
457 430 let mut included_file =
458 431 File::create(&tmpdir_path.join("included.rc")).unwrap();
459 432
460 433 included_file.write_all(b"[section]\nitem=value1").unwrap();
461 434 let base_config_path = tmpdir_path.join("base.rc");
462 435 let mut config_file = File::create(&base_config_path).unwrap();
463 436 let data =
464 437 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
465 438 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
466 439 config_file.write_all(data).unwrap();
467 440
468 441 let sources = vec![ConfigSource::AbsPath(base_config_path)];
469 442 let config = Config::load_from_explicit_sources(sources)
470 443 .expect("expected valid config");
471 444
472 445 let (_, value) = config.get_inner(b"section", b"item").unwrap();
473 446 assert_eq!(
474 447 value,
475 448 &ConfigValue {
476 449 bytes: b"value2".to_vec(),
477 450 line: Some(4)
478 451 }
479 452 );
480 453
481 454 let value = config.get(b"section", b"item").unwrap();
482 455 assert_eq!(value, b"value2",);
483 456 assert_eq!(
484 457 config.get_all(b"section", b"item"),
485 458 [b"value2", b"value1", b"value0"]
486 459 );
487 460
488 461 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
489 462 assert_eq!(
490 463 config.get_byte_size(b"section2", b"size").unwrap(),
491 464 Some(1024 + 512)
492 465 );
493 466 assert!(config.get_u32(b"section2", b"not-count").is_err());
494 467 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
495 468 }
496 469 }
@@ -1,588 +1,587 b''
1 1 extern crate log;
2 2 use crate::ui::Ui;
3 3 use clap::App;
4 4 use clap::AppSettings;
5 5 use clap::Arg;
6 6 use clap::ArgMatches;
7 7 use format_bytes::{format_bytes, join};
8 8 use hg::config::{Config, ConfigSource};
9 9 use hg::exit_codes;
10 10 use hg::repo::{Repo, RepoError};
11 11 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
12 12 use hg::utils::SliceExt;
13 13 use std::ffi::OsString;
14 14 use std::path::PathBuf;
15 15 use std::process::Command;
16 16
17 17 mod blackbox;
18 18 mod error;
19 19 mod ui;
20 20 use error::CommandError;
21 21
22 22 fn main_with_result(
23 23 process_start_time: &blackbox::ProcessStartTime,
24 24 ui: &ui::Ui,
25 25 repo: Result<&Repo, &NoRepoInCwdError>,
26 26 config: &Config,
27 27 ) -> Result<(), CommandError> {
28 28 check_extensions(config)?;
29 29
30 30 let app = App::new("rhg")
31 31 .global_setting(AppSettings::AllowInvalidUtf8)
32 32 .global_setting(AppSettings::DisableVersion)
33 33 .setting(AppSettings::SubcommandRequired)
34 34 .setting(AppSettings::VersionlessSubcommands)
35 35 .arg(
36 36 Arg::with_name("repository")
37 37 .help("repository root directory")
38 38 .short("-R")
39 39 .long("--repository")
40 40 .value_name("REPO")
41 41 .takes_value(true)
42 42 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
43 43 .global(true),
44 44 )
45 45 .arg(
46 46 Arg::with_name("config")
47 47 .help("set/override config option (use 'section.name=value')")
48 48 .long("--config")
49 49 .value_name("CONFIG")
50 50 .takes_value(true)
51 51 .global(true)
52 52 // Ok: `--config section.key1=val --config section.key2=val2`
53 53 .multiple(true)
54 54 // Not ok: `--config section.key1=val section.key2=val2`
55 55 .number_of_values(1),
56 56 )
57 57 .arg(
58 58 Arg::with_name("cwd")
59 59 .help("change working directory")
60 60 .long("--cwd")
61 61 .value_name("DIR")
62 62 .takes_value(true)
63 63 .global(true),
64 64 )
65 65 .version("0.0.1");
66 66 let app = add_subcommand_args(app);
67 67
68 68 let matches = app.clone().get_matches_safe()?;
69 69
70 70 let (subcommand_name, subcommand_matches) = matches.subcommand();
71 71 let run = subcommand_run_fn(subcommand_name)
72 72 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
73 73 let subcommand_args = subcommand_matches
74 74 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
75 75
76 76 let invocation = CliInvocation {
77 77 ui,
78 78 subcommand_args,
79 79 config,
80 80 repo,
81 81 };
82 82 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
83 83 blackbox.log_command_start();
84 84 let result = run(&invocation);
85 85 blackbox.log_command_end(exit_code(
86 86 &result,
87 87 // TODO: show a warning or combine with original error if `get_bool`
88 88 // returns an error
89 89 config
90 90 .get_bool(b"ui", b"detailed-exit-code")
91 91 .unwrap_or(false),
92 92 ));
93 93 result
94 94 }
95 95
96 96 fn main() {
97 97 // Run this first, before we find out if the blackbox extension is even
98 98 // enabled, in order to include everything in-between in the duration
99 99 // measurements. Reading config files can be slow if they’re on NFS.
100 100 let process_start_time = blackbox::ProcessStartTime::now();
101 101
102 102 env_logger::init();
103 103 let ui = ui::Ui::new();
104 104
105 105 let early_args = EarlyArgs::parse(std::env::args_os());
106 106
107 107 let initial_current_dir = early_args.cwd.map(|cwd| {
108 108 let cwd = get_path_from_bytes(&cwd);
109 109 std::env::current_dir()
110 110 .and_then(|initial| {
111 111 std::env::set_current_dir(cwd)?;
112 112 Ok(initial)
113 113 })
114 114 .unwrap_or_else(|error| {
115 115 exit(
116 116 &None,
117 117 &ui,
118 118 OnUnsupported::Abort,
119 119 Err(CommandError::abort(format!(
120 120 "abort: {}: '{}'",
121 121 error,
122 122 cwd.display()
123 123 ))),
124 124 false,
125 125 )
126 126 })
127 127 });
128 128
129 129 let mut non_repo_config =
130 130 Config::load_non_repo().unwrap_or_else(|error| {
131 131 // Normally this is decided based on config, but we don’t have that
132 132 // available. As of this writing config loading never returns an
133 133 // "unsupported" error but that is not enforced by the type system.
134 134 let on_unsupported = OnUnsupported::Abort;
135 135
136 136 exit(
137 137 &initial_current_dir,
138 138 &ui,
139 139 on_unsupported,
140 140 Err(error.into()),
141 141 false,
142 142 )
143 143 });
144 144
145 145 non_repo_config
146 146 .load_cli_args_config(early_args.config)
147 147 .unwrap_or_else(|error| {
148 148 exit(
149 149 &initial_current_dir,
150 150 &ui,
151 151 OnUnsupported::from_config(&ui, &non_repo_config),
152 152 Err(error.into()),
153 153 non_repo_config
154 154 .get_bool(b"ui", b"detailed-exit-code")
155 155 .unwrap_or(false),
156 156 )
157 157 });
158 158
159 159 if let Some(repo_path_bytes) = &early_args.repo {
160 160 lazy_static::lazy_static! {
161 161 static ref SCHEME_RE: regex::bytes::Regex =
162 162 // Same as `_matchscheme` in `mercurial/util.py`
163 163 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
164 164 }
165 165 if SCHEME_RE.is_match(&repo_path_bytes) {
166 166 exit(
167 167 &initial_current_dir,
168 168 &ui,
169 169 OnUnsupported::from_config(&ui, &non_repo_config),
170 170 Err(CommandError::UnsupportedFeature {
171 171 message: format_bytes!(
172 172 b"URL-like --repository {}",
173 173 repo_path_bytes
174 174 ),
175 175 }),
176 176 // TODO: show a warning or combine with original error if
177 177 // `get_bool` returns an error
178 178 non_repo_config
179 179 .get_bool(b"ui", b"detailed-exit-code")
180 180 .unwrap_or(false),
181 181 )
182 182 }
183 183 }
184 184 let repo_arg = early_args.repo.unwrap_or(Vec::new());
185 185 let repo_path: Option<PathBuf> = {
186 186 if repo_arg.is_empty() {
187 187 None
188 188 } else {
189 189 let local_config = {
190 190 if std::env::var_os("HGRCSKIPREPO").is_none() {
191 191 // TODO: handle errors from find_repo_root
192 192 if let Ok(current_dir_path) = Repo::find_repo_root() {
193 193 let config_files = vec![
194 194 ConfigSource::AbsPath(
195 195 current_dir_path.join(".hg/hgrc"),
196 196 ),
197 197 ConfigSource::AbsPath(
198 198 current_dir_path.join(".hg/hgrc-not-shared"),
199 199 ),
200 200 ];
201 201 // TODO: handle errors from
202 202 // `load_from_explicit_sources`
203 203 Config::load_from_explicit_sources(config_files).ok()
204 204 } else {
205 205 None
206 206 }
207 207 } else {
208 208 None
209 209 }
210 210 };
211 211
212 212 let non_repo_config_val = {
213 213 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
214 214 match &non_repo_val {
215 215 Some(val) if val.len() > 0 => home::home_dir()
216 216 .unwrap_or_else(|| PathBuf::from("~"))
217 217 .join(get_path_from_bytes(val))
218 218 .canonicalize()
219 219 // TODO: handle error and make it similar to python
220 220 // implementation maybe?
221 221 .ok(),
222 222 _ => None,
223 223 }
224 224 };
225 225
226 226 let config_val = match &local_config {
227 227 None => non_repo_config_val,
228 228 Some(val) => {
229 229 let local_config_val = val.get(b"paths", &repo_arg);
230 230 match &local_config_val {
231 231 Some(val) if val.len() > 0 => {
232 232 // presence of a local_config assures that
233 233 // current_dir
234 234 // wont result in an Error
235 235 let canpath = hg::utils::current_dir()
236 236 .unwrap()
237 237 .join(get_path_from_bytes(val))
238 238 .canonicalize();
239 239 canpath.ok().or(non_repo_config_val)
240 240 }
241 241 _ => non_repo_config_val,
242 242 }
243 243 }
244 244 };
245 245 config_val.or(Some(get_path_from_bytes(&repo_arg).to_path_buf()))
246 246 }
247 247 };
248 248
249 249 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
250 250 {
251 251 Ok(repo) => Ok(repo),
252 252 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
253 253 // Not finding a repo is not fatal yet, if `-R` was not given
254 254 Err(NoRepoInCwdError { cwd: at })
255 255 }
256 256 Err(error) => exit(
257 257 &initial_current_dir,
258 258 &ui,
259 259 OnUnsupported::from_config(&ui, &non_repo_config),
260 260 Err(error.into()),
261 261 // TODO: show a warning or combine with original error if
262 262 // `get_bool` returns an error
263 263 non_repo_config
264 264 .get_bool(b"ui", b"detailed-exit-code")
265 265 .unwrap_or(false),
266 266 ),
267 267 };
268 268
269 269 let config = if let Ok(repo) = &repo_result {
270 270 repo.config()
271 271 } else {
272 272 &non_repo_config
273 273 };
274 274 let on_unsupported = OnUnsupported::from_config(&ui, config);
275 275
276 276 let result = main_with_result(
277 277 &process_start_time,
278 278 &ui,
279 279 repo_result.as_ref(),
280 280 config,
281 281 );
282 282 exit(
283 283 &initial_current_dir,
284 284 &ui,
285 285 on_unsupported,
286 286 result,
287 287 // TODO: show a warning or combine with original error if `get_bool`
288 288 // returns an error
289 289 config
290 290 .get_bool(b"ui", b"detailed-exit-code")
291 291 .unwrap_or(false),
292 292 )
293 293 }
294 294
295 295 fn exit_code(
296 296 result: &Result<(), CommandError>,
297 297 use_detailed_exit_code: bool,
298 298 ) -> i32 {
299 299 match result {
300 300 Ok(()) => exit_codes::OK,
301 301 Err(CommandError::Abort {
302 302 message: _,
303 303 detailed_exit_code,
304 304 }) => {
305 305 if use_detailed_exit_code {
306 306 *detailed_exit_code
307 307 } else {
308 308 exit_codes::ABORT
309 309 }
310 310 }
311 311 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
312 312
313 313 // Exit with a specific code and no error message to let a potential
314 314 // wrapper script fallback to Python-based Mercurial.
315 315 Err(CommandError::UnsupportedFeature { .. }) => {
316 316 exit_codes::UNIMPLEMENTED
317 317 }
318 318 }
319 319 }
320 320
321 321 fn exit(
322 322 initial_current_dir: &Option<PathBuf>,
323 323 ui: &Ui,
324 324 mut on_unsupported: OnUnsupported,
325 325 result: Result<(), CommandError>,
326 326 use_detailed_exit_code: bool,
327 327 ) -> ! {
328 328 if let (
329 329 OnUnsupported::Fallback { executable },
330 330 Err(CommandError::UnsupportedFeature { .. }),
331 331 ) = (&on_unsupported, &result)
332 332 {
333 333 let mut args = std::env::args_os();
334 334 let executable_path = get_path_from_bytes(&executable);
335 335 let this_executable = args.next().expect("exepcted argv[0] to exist");
336 336 if executable_path == &PathBuf::from(this_executable) {
337 337 // Avoid spawning infinitely many processes until resource
338 338 // exhaustion.
339 339 let _ = ui.write_stderr(&format_bytes!(
340 340 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
341 341 points to `rhg` itself.\n",
342 342 executable
343 343 ));
344 344 on_unsupported = OnUnsupported::Abort
345 345 } else {
346 346 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
347 347 let mut command = Command::new(executable_path);
348 348 command.args(args);
349 349 if let Some(initial) = initial_current_dir {
350 350 command.current_dir(initial);
351 351 }
352 352 let result = command.status();
353 353 match result {
354 354 Ok(status) => std::process::exit(
355 355 status.code().unwrap_or(exit_codes::ABORT),
356 356 ),
357 357 Err(error) => {
358 358 let _ = ui.write_stderr(&format_bytes!(
359 359 b"tried to fall back to a '{}' sub-process but got error {}\n",
360 360 executable, format_bytes::Utf8(error)
361 361 ));
362 362 on_unsupported = OnUnsupported::Abort
363 363 }
364 364 }
365 365 }
366 366 }
367 367 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
368 368 }
369 369
370 370 fn exit_no_fallback(
371 371 ui: &Ui,
372 372 on_unsupported: OnUnsupported,
373 373 result: Result<(), CommandError>,
374 374 use_detailed_exit_code: bool,
375 375 ) -> ! {
376 376 match &result {
377 377 Ok(_) => {}
378 378 Err(CommandError::Unsuccessful) => {}
379 379 Err(CommandError::Abort {
380 380 message,
381 381 detailed_exit_code: _,
382 382 }) => {
383 383 if !message.is_empty() {
384 384 // Ignore errors when writing to stderr, we’re already exiting
385 385 // with failure code so there’s not much more we can do.
386 386 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
387 387 }
388 388 }
389 389 Err(CommandError::UnsupportedFeature { message }) => {
390 390 match on_unsupported {
391 391 OnUnsupported::Abort => {
392 392 let _ = ui.write_stderr(&format_bytes!(
393 393 b"unsupported feature: {}\n",
394 394 message
395 395 ));
396 396 }
397 397 OnUnsupported::AbortSilent => {}
398 398 OnUnsupported::Fallback { .. } => unreachable!(),
399 399 }
400 400 }
401 401 }
402 402 std::process::exit(exit_code(&result, use_detailed_exit_code))
403 403 }
404 404
405 405 macro_rules! subcommands {
406 406 ($( $command: ident )+) => {
407 407 mod commands {
408 408 $(
409 409 pub mod $command;
410 410 )+
411 411 }
412 412
413 413 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
414 414 app
415 415 $(
416 416 .subcommand(commands::$command::args())
417 417 )+
418 418 }
419 419
420 420 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
421 421
422 422 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
423 423 match name {
424 424 $(
425 425 stringify!($command) => Some(commands::$command::run),
426 426 )+
427 427 _ => None,
428 428 }
429 429 }
430 430 };
431 431 }
432 432
433 433 subcommands! {
434 434 cat
435 435 debugdata
436 436 debugrequirements
437 437 files
438 438 root
439 439 config
440 440 status
441 441 }
442 442
443 443 pub struct CliInvocation<'a> {
444 444 ui: &'a Ui,
445 445 subcommand_args: &'a ArgMatches<'a>,
446 446 config: &'a Config,
447 447 /// References inside `Result` is a bit peculiar but allow
448 448 /// `invocation.repo?` to work out with `&CliInvocation` since this
449 449 /// `Result` type is `Copy`.
450 450 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
451 451 }
452 452
453 453 struct NoRepoInCwdError {
454 454 cwd: PathBuf,
455 455 }
456 456
457 457 /// CLI arguments to be parsed "early" in order to be able to read
458 458 /// configuration before using Clap. Ideally we would also use Clap for this,
459 459 /// see <https://github.com/clap-rs/clap/discussions/2366>.
460 460 ///
461 461 /// These arguments are still declared when we do use Clap later, so that Clap
462 462 /// does not return an error for their presence.
463 463 struct EarlyArgs {
464 464 /// Values of all `--config` arguments. (Possibly none)
465 465 config: Vec<Vec<u8>>,
466 466 /// Value of the `-R` or `--repository` argument, if any.
467 467 repo: Option<Vec<u8>>,
468 468 /// Value of the `--cwd` argument, if any.
469 469 cwd: Option<Vec<u8>>,
470 470 }
471 471
472 472 impl EarlyArgs {
473 473 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
474 474 let mut args = args.into_iter().map(get_bytes_from_os_str);
475 475 let mut config = Vec::new();
476 476 let mut repo = None;
477 477 let mut cwd = None;
478 478 // Use `while let` instead of `for` so that we can also call
479 479 // `args.next()` inside the loop.
480 480 while let Some(arg) = args.next() {
481 481 if arg == b"--config" {
482 482 if let Some(value) = args.next() {
483 483 config.push(value)
484 484 }
485 485 } else if let Some(value) = arg.drop_prefix(b"--config=") {
486 486 config.push(value.to_owned())
487 487 }
488 488
489 489 if arg == b"--cwd" {
490 490 if let Some(value) = args.next() {
491 491 cwd = Some(value)
492 492 }
493 493 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
494 494 cwd = Some(value.to_owned())
495 495 }
496 496
497 497 if arg == b"--repository" || arg == b"-R" {
498 498 if let Some(value) = args.next() {
499 499 repo = Some(value)
500 500 }
501 501 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
502 502 repo = Some(value.to_owned())
503 503 } else if let Some(value) = arg.drop_prefix(b"-R") {
504 504 repo = Some(value.to_owned())
505 505 }
506 506 }
507 507 Self { config, repo, cwd }
508 508 }
509 509 }
510 510
511 511 /// What to do when encountering some unsupported feature.
512 512 ///
513 513 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
514 514 enum OnUnsupported {
515 515 /// Print an error message describing what feature is not supported,
516 516 /// and exit with code 252.
517 517 Abort,
518 518 /// Silently exit with code 252.
519 519 AbortSilent,
520 520 /// Try running a Python implementation
521 521 Fallback { executable: Vec<u8> },
522 522 }
523 523
524 524 impl OnUnsupported {
525 525 const DEFAULT: Self = OnUnsupported::Abort;
526 526
527 527 fn from_config(ui: &Ui, config: &Config) -> Self {
528 528 match config
529 529 .get(b"rhg", b"on-unsupported")
530 530 .map(|value| value.to_ascii_lowercase())
531 531 .as_deref()
532 532 {
533 533 Some(b"abort") => OnUnsupported::Abort,
534 534 Some(b"abort-silent") => OnUnsupported::AbortSilent,
535 535 Some(b"fallback") => OnUnsupported::Fallback {
536 536 executable: config
537 537 .get(b"rhg", b"fallback-executable")
538 538 .unwrap_or_else(|| {
539 539 exit_no_fallback(
540 540 ui,
541 541 Self::Abort,
542 542 Err(CommandError::abort(
543 543 "abort: 'rhg.on-unsupported=fallback' without \
544 544 'rhg.fallback-executable' set."
545 545 )),
546 546 false,
547 547 )
548 548 })
549 549 .to_owned(),
550 550 },
551 551 None => Self::DEFAULT,
552 552 Some(_) => {
553 553 // TODO: warn about unknown config value
554 554 Self::DEFAULT
555 555 }
556 556 }
557 557 }
558 558 }
559 559
560 560 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[b"blackbox", b"share"];
561 561
562 562 fn check_extensions(config: &Config) -> Result<(), CommandError> {
563 563 let enabled = config.get_section_keys(b"extensions");
564 564
565 565 let mut unsupported = enabled;
566 566 for supported in SUPPORTED_EXTENSIONS {
567 567 unsupported.remove(supported);
568 568 }
569 569
570 if let Some(ignored_list) =
571 config.get_simple_list(b"rhg", b"ignored-extensions")
570 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
572 571 {
573 572 for ignored in ignored_list {
574 unsupported.remove(ignored);
573 unsupported.remove(ignored.as_slice());
575 574 }
576 575 }
577 576
578 577 if unsupported.is_empty() {
579 578 Ok(())
580 579 } else {
581 580 Err(CommandError::UnsupportedFeature {
582 581 message: format_bytes!(
583 582 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
584 583 join(unsupported, b", ")
585 584 ),
586 585 })
587 586 }
588 587 }
General Comments 0
You need to be logged in to leave comments. Login now