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