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