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