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