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