##// END OF EJS Templates
rust-config: add config getters that don't fall back to defaults...
Raphaël Gomès -
r51657:8ff187fb default
parent child Browse files
Show More
@@ -1,674 +1,728
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 //! Mercurial config parsing and interfaces.
10 //! Mercurial config parsing and interfaces.
11
11
12 pub mod config_items;
12 pub mod config_items;
13 mod layer;
13 mod layer;
14 mod plain_info;
14 mod plain_info;
15 mod values;
15 mod values;
16 pub use layer::{ConfigError, ConfigOrigin, ConfigParseError};
16 pub use layer::{ConfigError, ConfigOrigin, ConfigParseError};
17 use lazy_static::lazy_static;
17 use lazy_static::lazy_static;
18 pub use plain_info::PlainInfo;
18 pub use plain_info::PlainInfo;
19
19
20 use self::config_items::DefaultConfig;
20 use self::config_items::DefaultConfig;
21 use self::config_items::DefaultConfigItem;
21 use self::config_items::DefaultConfigItem;
22 use self::layer::ConfigLayer;
22 use self::layer::ConfigLayer;
23 use self::layer::ConfigValue;
23 use self::layer::ConfigValue;
24 use crate::errors::HgError;
24 use crate::errors::HgError;
25 use crate::errors::{HgResultExt, IoResultExt};
25 use crate::errors::{HgResultExt, IoResultExt};
26 use crate::utils::files::get_bytes_from_os_str;
26 use crate::utils::files::get_bytes_from_os_str;
27 use format_bytes::{write_bytes, DisplayBytes};
27 use format_bytes::{write_bytes, DisplayBytes};
28 use std::collections::HashSet;
28 use std::collections::HashSet;
29 use std::env;
29 use std::env;
30 use std::fmt;
30 use std::fmt;
31 use std::path::{Path, PathBuf};
31 use std::path::{Path, PathBuf};
32 use std::str;
32 use std::str;
33
33
34 lazy_static! {
34 lazy_static! {
35 static ref DEFAULT_CONFIG: Result<DefaultConfig, HgError> = {
35 static ref DEFAULT_CONFIG: Result<DefaultConfig, HgError> = {
36 DefaultConfig::from_contents(include_str!(
36 DefaultConfig::from_contents(include_str!(
37 "../../../../mercurial/configitems.toml"
37 "../../../../mercurial/configitems.toml"
38 ))
38 ))
39 };
39 };
40 }
40 }
41
41
42 /// Holds the config values for the current repository
42 /// Holds the config values for the current repository
43 /// TODO update this docstring once we support more sources
43 /// TODO update this docstring once we support more sources
44 #[derive(Clone)]
44 #[derive(Clone)]
45 pub struct Config {
45 pub struct Config {
46 layers: Vec<layer::ConfigLayer>,
46 layers: Vec<layer::ConfigLayer>,
47 plain: PlainInfo,
47 plain: PlainInfo,
48 }
48 }
49
49
50 impl DisplayBytes for Config {
50 impl DisplayBytes for Config {
51 fn display_bytes(
51 fn display_bytes(
52 &self,
52 &self,
53 out: &mut dyn std::io::Write,
53 out: &mut dyn std::io::Write,
54 ) -> std::io::Result<()> {
54 ) -> std::io::Result<()> {
55 for (index, layer) in self.layers.iter().rev().enumerate() {
55 for (index, layer) in self.layers.iter().rev().enumerate() {
56 write_bytes!(
56 write_bytes!(
57 out,
57 out,
58 b"==== Layer {} (trusted: {}) ====\n{}",
58 b"==== Layer {} (trusted: {}) ====\n{}",
59 index,
59 index,
60 if layer.trusted {
60 if layer.trusted {
61 &b"yes"[..]
61 &b"yes"[..]
62 } else {
62 } else {
63 &b"no"[..]
63 &b"no"[..]
64 },
64 },
65 layer
65 layer
66 )?;
66 )?;
67 }
67 }
68 Ok(())
68 Ok(())
69 }
69 }
70 }
70 }
71
71
72 pub enum ConfigSource {
72 pub enum ConfigSource {
73 /// Absolute path to a config file
73 /// Absolute path to a config file
74 AbsPath(PathBuf),
74 AbsPath(PathBuf),
75 /// Already parsed (from the CLI, env, Python resources, etc.)
75 /// Already parsed (from the CLI, env, Python resources, etc.)
76 Parsed(layer::ConfigLayer),
76 Parsed(layer::ConfigLayer),
77 }
77 }
78
78
79 #[derive(Debug)]
79 #[derive(Debug)]
80 pub struct ConfigValueParseErrorDetails {
80 pub struct ConfigValueParseErrorDetails {
81 pub origin: ConfigOrigin,
81 pub origin: ConfigOrigin,
82 pub line: Option<usize>,
82 pub line: Option<usize>,
83 pub section: Vec<u8>,
83 pub section: Vec<u8>,
84 pub item: Vec<u8>,
84 pub item: Vec<u8>,
85 pub value: Vec<u8>,
85 pub value: Vec<u8>,
86 pub expected_type: &'static str,
86 pub expected_type: &'static str,
87 }
87 }
88
88
89 // boxed to avoid very large Result types
89 // boxed to avoid very large Result types
90 pub type ConfigValueParseError = Box<ConfigValueParseErrorDetails>;
90 pub type ConfigValueParseError = Box<ConfigValueParseErrorDetails>;
91
91
92 impl fmt::Display for ConfigValueParseError {
92 impl fmt::Display for ConfigValueParseError {
93 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
93 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
94 // TODO: add origin and line number information, here and in
94 // TODO: add origin and line number information, here and in
95 // corresponding python code
95 // corresponding python code
96 write!(
96 write!(
97 f,
97 f,
98 "config error: {}.{} is not a {} ('{}')",
98 "config error: {}.{} is not a {} ('{}')",
99 String::from_utf8_lossy(&self.section),
99 String::from_utf8_lossy(&self.section),
100 String::from_utf8_lossy(&self.item),
100 String::from_utf8_lossy(&self.item),
101 self.expected_type,
101 self.expected_type,
102 String::from_utf8_lossy(&self.value)
102 String::from_utf8_lossy(&self.value)
103 )
103 )
104 }
104 }
105 }
105 }
106
106
107 /// Returns true if the config item is disabled by PLAIN or PLAINEXCEPT
107 /// Returns true if the config item is disabled by PLAIN or PLAINEXCEPT
108 fn should_ignore(plain: &PlainInfo, section: &[u8], item: &[u8]) -> bool {
108 fn should_ignore(plain: &PlainInfo, section: &[u8], item: &[u8]) -> bool {
109 // duplication with [_applyconfig] in [ui.py],
109 // duplication with [_applyconfig] in [ui.py],
110 if !plain.is_plain() {
110 if !plain.is_plain() {
111 return false;
111 return false;
112 }
112 }
113 if section == b"alias" {
113 if section == b"alias" {
114 return plain.plainalias();
114 return plain.plainalias();
115 }
115 }
116 if section == b"revsetalias" {
116 if section == b"revsetalias" {
117 return plain.plainrevsetalias();
117 return plain.plainrevsetalias();
118 }
118 }
119 if section == b"templatealias" {
119 if section == b"templatealias" {
120 return plain.plaintemplatealias();
120 return plain.plaintemplatealias();
121 }
121 }
122 if section == b"ui" {
122 if section == b"ui" {
123 let to_delete: &[&[u8]] = &[
123 let to_delete: &[&[u8]] = &[
124 b"debug",
124 b"debug",
125 b"fallbackencoding",
125 b"fallbackencoding",
126 b"quiet",
126 b"quiet",
127 b"slash",
127 b"slash",
128 b"logtemplate",
128 b"logtemplate",
129 b"message-output",
129 b"message-output",
130 b"statuscopies",
130 b"statuscopies",
131 b"style",
131 b"style",
132 b"traceback",
132 b"traceback",
133 b"verbose",
133 b"verbose",
134 ];
134 ];
135 return to_delete.contains(&item);
135 return to_delete.contains(&item);
136 }
136 }
137 let sections_to_delete: &[&[u8]] =
137 let sections_to_delete: &[&[u8]] =
138 &[b"defaults", b"commands", b"command-templates"];
138 &[b"defaults", b"commands", b"command-templates"];
139 sections_to_delete.contains(&section)
139 sections_to_delete.contains(&section)
140 }
140 }
141
141
142 impl Config {
142 impl Config {
143 /// The configuration to use when printing configuration-loading errors
143 /// The configuration to use when printing configuration-loading errors
144 pub fn empty() -> Self {
144 pub fn empty() -> Self {
145 Self {
145 Self {
146 layers: Vec::new(),
146 layers: Vec::new(),
147 plain: PlainInfo::empty(),
147 plain: PlainInfo::empty(),
148 }
148 }
149 }
149 }
150
150
151 /// Load system and user configuration from various files.
151 /// Load system and user configuration from various files.
152 ///
152 ///
153 /// This is also affected by some environment variables.
153 /// This is also affected by some environment variables.
154 pub fn load_non_repo() -> Result<Self, ConfigError> {
154 pub fn load_non_repo() -> Result<Self, ConfigError> {
155 let mut config = Self::empty();
155 let mut config = Self::empty();
156 let opt_rc_path = env::var_os("HGRCPATH");
156 let opt_rc_path = env::var_os("HGRCPATH");
157 // HGRCPATH replaces system config
157 // HGRCPATH replaces system config
158 if opt_rc_path.is_none() {
158 if opt_rc_path.is_none() {
159 config.add_system_config()?
159 config.add_system_config()?
160 }
160 }
161
161
162 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
162 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
163 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
163 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
164 config.add_for_environment_variable("PAGER", b"pager", b"pager");
164 config.add_for_environment_variable("PAGER", b"pager", b"pager");
165
165
166 // These are set by `run-tests.py --rhg` to enable fallback for the
166 // These are set by `run-tests.py --rhg` to enable fallback for the
167 // entire test suite. Alternatives would be setting configuration
167 // entire test suite. Alternatives would be setting configuration
168 // through `$HGRCPATH` but some tests override that, or changing the
168 // through `$HGRCPATH` but some tests override that, or changing the
169 // `hg` shell alias to include `--config` but that disrupts tests that
169 // `hg` shell alias to include `--config` but that disrupts tests that
170 // print command lines and check expected output.
170 // print command lines and check expected output.
171 config.add_for_environment_variable(
171 config.add_for_environment_variable(
172 "RHG_ON_UNSUPPORTED",
172 "RHG_ON_UNSUPPORTED",
173 b"rhg",
173 b"rhg",
174 b"on-unsupported",
174 b"on-unsupported",
175 );
175 );
176 config.add_for_environment_variable(
176 config.add_for_environment_variable(
177 "RHG_FALLBACK_EXECUTABLE",
177 "RHG_FALLBACK_EXECUTABLE",
178 b"rhg",
178 b"rhg",
179 b"fallback-executable",
179 b"fallback-executable",
180 );
180 );
181
181
182 // HGRCPATH replaces user config
182 // HGRCPATH replaces user config
183 if opt_rc_path.is_none() {
183 if opt_rc_path.is_none() {
184 config.add_user_config()?
184 config.add_user_config()?
185 }
185 }
186 if let Some(rc_path) = &opt_rc_path {
186 if let Some(rc_path) = &opt_rc_path {
187 for path in env::split_paths(rc_path) {
187 for path in env::split_paths(rc_path) {
188 if !path.as_os_str().is_empty() {
188 if !path.as_os_str().is_empty() {
189 if path.is_dir() {
189 if path.is_dir() {
190 config.add_trusted_dir(&path)?
190 config.add_trusted_dir(&path)?
191 } else {
191 } else {
192 config.add_trusted_file(&path)?
192 config.add_trusted_file(&path)?
193 }
193 }
194 }
194 }
195 }
195 }
196 }
196 }
197 Ok(config)
197 Ok(config)
198 }
198 }
199
199
200 pub fn load_cli_args(
200 pub fn load_cli_args(
201 &mut self,
201 &mut self,
202 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
202 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
203 color_arg: Option<Vec<u8>>,
203 color_arg: Option<Vec<u8>>,
204 ) -> Result<(), ConfigError> {
204 ) -> Result<(), ConfigError> {
205 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
205 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
206 self.layers.push(layer)
206 self.layers.push(layer)
207 }
207 }
208 if let Some(arg) = color_arg {
208 if let Some(arg) = color_arg {
209 let mut layer = ConfigLayer::new(ConfigOrigin::CommandLineColor);
209 let mut layer = ConfigLayer::new(ConfigOrigin::CommandLineColor);
210 layer.add(b"ui"[..].into(), b"color"[..].into(), arg, None);
210 layer.add(b"ui"[..].into(), b"color"[..].into(), arg, None);
211 self.layers.push(layer)
211 self.layers.push(layer)
212 }
212 }
213 Ok(())
213 Ok(())
214 }
214 }
215
215
216 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
216 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
217 if let Some(entries) = std::fs::read_dir(path)
217 if let Some(entries) = std::fs::read_dir(path)
218 .when_reading_file(path)
218 .when_reading_file(path)
219 .io_not_found_as_none()?
219 .io_not_found_as_none()?
220 {
220 {
221 let mut file_paths = entries
221 let mut file_paths = entries
222 .map(|result| {
222 .map(|result| {
223 result.when_reading_file(path).map(|entry| entry.path())
223 result.when_reading_file(path).map(|entry| entry.path())
224 })
224 })
225 .collect::<Result<Vec<_>, _>>()?;
225 .collect::<Result<Vec<_>, _>>()?;
226 file_paths.sort();
226 file_paths.sort();
227 for file_path in &file_paths {
227 for file_path in &file_paths {
228 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
228 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
229 self.add_trusted_file(file_path)?
229 self.add_trusted_file(file_path)?
230 }
230 }
231 }
231 }
232 }
232 }
233 Ok(())
233 Ok(())
234 }
234 }
235
235
236 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
236 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
237 if let Some(data) = std::fs::read(path)
237 if let Some(data) = std::fs::read(path)
238 .when_reading_file(path)
238 .when_reading_file(path)
239 .io_not_found_as_none()?
239 .io_not_found_as_none()?
240 {
240 {
241 self.layers.extend(ConfigLayer::parse(path, &data)?)
241 self.layers.extend(ConfigLayer::parse(path, &data)?)
242 }
242 }
243 Ok(())
243 Ok(())
244 }
244 }
245
245
246 fn add_for_environment_variable(
246 fn add_for_environment_variable(
247 &mut self,
247 &mut self,
248 var: &str,
248 var: &str,
249 section: &[u8],
249 section: &[u8],
250 key: &[u8],
250 key: &[u8],
251 ) {
251 ) {
252 if let Some(value) = env::var_os(var) {
252 if let Some(value) = env::var_os(var) {
253 let origin = layer::ConfigOrigin::Environment(var.into());
253 let origin = layer::ConfigOrigin::Environment(var.into());
254 let mut layer = ConfigLayer::new(origin);
254 let mut layer = ConfigLayer::new(origin);
255 layer.add(
255 layer.add(
256 section.to_owned(),
256 section.to_owned(),
257 key.to_owned(),
257 key.to_owned(),
258 get_bytes_from_os_str(value),
258 get_bytes_from_os_str(value),
259 None,
259 None,
260 );
260 );
261 self.layers.push(layer)
261 self.layers.push(layer)
262 }
262 }
263 }
263 }
264
264
265 #[cfg(unix)] // TODO: other platforms
265 #[cfg(unix)] // TODO: other platforms
266 fn add_system_config(&mut self) -> Result<(), ConfigError> {
266 fn add_system_config(&mut self) -> Result<(), ConfigError> {
267 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
267 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
268 let etc = prefix.join("etc").join("mercurial");
268 let etc = prefix.join("etc").join("mercurial");
269 self.add_trusted_file(&etc.join("hgrc"))?;
269 self.add_trusted_file(&etc.join("hgrc"))?;
270 self.add_trusted_dir(&etc.join("hgrc.d"))
270 self.add_trusted_dir(&etc.join("hgrc.d"))
271 };
271 };
272 let root = Path::new("/");
272 let root = Path::new("/");
273 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
273 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
274 // instead? TODO: can this be a relative path?
274 // instead? TODO: can this be a relative path?
275 let hg = crate::utils::current_exe()?;
275 let hg = crate::utils::current_exe()?;
276 // TODO: this order (per-installation then per-system) matches
276 // TODO: this order (per-installation then per-system) matches
277 // `systemrcpath()` in `mercurial/scmposix.py`, but
277 // `systemrcpath()` in `mercurial/scmposix.py`, but
278 // `mercurial/helptext/config.txt` suggests it should be reversed
278 // `mercurial/helptext/config.txt` suggests it should be reversed
279 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
279 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
280 if installation_prefix != root {
280 if installation_prefix != root {
281 add_for_prefix(installation_prefix)?
281 add_for_prefix(installation_prefix)?
282 }
282 }
283 }
283 }
284 add_for_prefix(root)?;
284 add_for_prefix(root)?;
285 Ok(())
285 Ok(())
286 }
286 }
287
287
288 #[cfg(unix)] // TODO: other plateforms
288 #[cfg(unix)] // TODO: other plateforms
289 fn add_user_config(&mut self) -> Result<(), ConfigError> {
289 fn add_user_config(&mut self) -> Result<(), ConfigError> {
290 let opt_home = home::home_dir();
290 let opt_home = home::home_dir();
291 if let Some(home) = &opt_home {
291 if let Some(home) = &opt_home {
292 self.add_trusted_file(&home.join(".hgrc"))?
292 self.add_trusted_file(&home.join(".hgrc"))?
293 }
293 }
294 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
294 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
295 if !darwin {
295 if !darwin {
296 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
296 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
297 .map(PathBuf::from)
297 .map(PathBuf::from)
298 .or_else(|| opt_home.map(|home| home.join(".config")))
298 .or_else(|| opt_home.map(|home| home.join(".config")))
299 {
299 {
300 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
300 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
301 }
301 }
302 }
302 }
303 Ok(())
303 Ok(())
304 }
304 }
305
305
306 /// Loads in order, which means that the precedence is the same
306 /// Loads in order, which means that the precedence is the same
307 /// as the order of `sources`.
307 /// as the order of `sources`.
308 pub fn load_from_explicit_sources(
308 pub fn load_from_explicit_sources(
309 sources: Vec<ConfigSource>,
309 sources: Vec<ConfigSource>,
310 ) -> Result<Self, ConfigError> {
310 ) -> Result<Self, ConfigError> {
311 let mut layers = vec![];
311 let mut layers = vec![];
312
312
313 for source in sources.into_iter() {
313 for source in sources.into_iter() {
314 match source {
314 match source {
315 ConfigSource::Parsed(c) => layers.push(c),
315 ConfigSource::Parsed(c) => layers.push(c),
316 ConfigSource::AbsPath(c) => {
316 ConfigSource::AbsPath(c) => {
317 // TODO check if it should be trusted
317 // TODO check if it should be trusted
318 // mercurial/ui.py:427
318 // mercurial/ui.py:427
319 let data = match std::fs::read(&c) {
319 let data = match std::fs::read(&c) {
320 Err(_) => continue, // same as the python code
320 Err(_) => continue, // same as the python code
321 Ok(data) => data,
321 Ok(data) => data,
322 };
322 };
323 layers.extend(ConfigLayer::parse(&c, &data)?)
323 layers.extend(ConfigLayer::parse(&c, &data)?)
324 }
324 }
325 }
325 }
326 }
326 }
327
327
328 Ok(Config {
328 Ok(Config {
329 layers,
329 layers,
330 plain: PlainInfo::empty(),
330 plain: PlainInfo::empty(),
331 })
331 })
332 }
332 }
333
333
334 /// Loads the per-repository config into a new `Config` which is combined
334 /// Loads the per-repository config into a new `Config` which is combined
335 /// with `self`.
335 /// with `self`.
336 pub(crate) fn combine_with_repo(
336 pub(crate) fn combine_with_repo(
337 &self,
337 &self,
338 repo_config_files: &[PathBuf],
338 repo_config_files: &[PathBuf],
339 ) -> Result<Self, ConfigError> {
339 ) -> Result<Self, ConfigError> {
340 let (cli_layers, other_layers) = self
340 let (cli_layers, other_layers) = self
341 .layers
341 .layers
342 .iter()
342 .iter()
343 .cloned()
343 .cloned()
344 .partition(ConfigLayer::is_from_command_line);
344 .partition(ConfigLayer::is_from_command_line);
345
345
346 let mut repo_config = Self {
346 let mut repo_config = Self {
347 layers: other_layers,
347 layers: other_layers,
348 plain: PlainInfo::empty(),
348 plain: PlainInfo::empty(),
349 };
349 };
350 for path in repo_config_files {
350 for path in repo_config_files {
351 // TODO: check if this file should be trusted:
351 // TODO: check if this file should be trusted:
352 // `mercurial/ui.py:427`
352 // `mercurial/ui.py:427`
353 repo_config.add_trusted_file(path)?;
353 repo_config.add_trusted_file(path)?;
354 }
354 }
355 repo_config.layers.extend(cli_layers);
355 repo_config.layers.extend(cli_layers);
356 Ok(repo_config)
356 Ok(repo_config)
357 }
357 }
358
358
359 pub fn apply_plain(&mut self, plain: PlainInfo) {
359 pub fn apply_plain(&mut self, plain: PlainInfo) {
360 self.plain = plain;
360 self.plain = plain;
361 }
361 }
362
362
363 /// Returns the default value for the given config item, if any.
363 /// Returns the default value for the given config item, if any.
364 pub fn get_default(
364 pub fn get_default(
365 &self,
365 &self,
366 section: &[u8],
366 section: &[u8],
367 item: &[u8],
367 item: &[u8],
368 ) -> Result<Option<&DefaultConfigItem>, HgError> {
368 ) -> Result<Option<&DefaultConfigItem>, HgError> {
369 let default_config = DEFAULT_CONFIG.as_ref().map_err(|e| {
369 let default_config = DEFAULT_CONFIG.as_ref().map_err(|e| {
370 HgError::abort(
370 HgError::abort(
371 e.to_string(),
371 e.to_string(),
372 crate::exit_codes::ABORT,
372 crate::exit_codes::ABORT,
373 Some("`mercurial/configitems.toml` is not valid".into()),
373 Some("`mercurial/configitems.toml` is not valid".into()),
374 )
374 )
375 })?;
375 })?;
376 Ok(default_config.get(section, item))
376 Ok(default_config.get(section, item))
377 }
377 }
378
378
379 fn get_parse<'config, T: 'config>(
379 fn get_parse<'config, T: 'config>(
380 &'config self,
380 &'config self,
381 section: &[u8],
381 section: &[u8],
382 item: &[u8],
382 item: &[u8],
383 expected_type: &'static str,
383 expected_type: &'static str,
384 parse: impl Fn(&'config [u8]) -> Option<T>,
384 parse: impl Fn(&'config [u8]) -> Option<T>,
385 fallback_to_default: bool,
385 ) -> Result<Option<T>, HgError>
386 ) -> Result<Option<T>, HgError>
386 where
387 where
387 Option<T>: TryFrom<&'config DefaultConfigItem, Error = HgError>,
388 Option<T>: TryFrom<&'config DefaultConfigItem, Error = HgError>,
388 {
389 {
389 match self.get_inner(section, item) {
390 match self.get_inner(section, item) {
390 Some((layer, v)) => match parse(&v.bytes) {
391 Some((layer, v)) => match parse(&v.bytes) {
391 Some(b) => Ok(Some(b)),
392 Some(b) => Ok(Some(b)),
392 None => Err(Box::new(ConfigValueParseErrorDetails {
393 None => Err(Box::new(ConfigValueParseErrorDetails {
393 origin: layer.origin.to_owned(),
394 origin: layer.origin.to_owned(),
394 line: v.line,
395 line: v.line,
395 value: v.bytes.to_owned(),
396 value: v.bytes.to_owned(),
396 section: section.to_owned(),
397 section: section.to_owned(),
397 item: item.to_owned(),
398 item: item.to_owned(),
398 expected_type,
399 expected_type,
399 })
400 })
400 .into()),
401 .into()),
401 },
402 },
402 None => match self.get_default(section, item)? {
403 None => {
403 Some(default) => Ok(default.try_into()?),
404 if !fallback_to_default {
404 None => {
405 return Ok(None);
405 Ok(None)
406 }
406 }
407 },
407 match self.get_default(section, item)? {
408 Some(default) => Ok(default.try_into()?),
409 None => Ok(None),
410 }
411 }
408 }
412 }
409 }
413 }
410
414
411 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
415 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
412 /// Otherwise, returns an `Ok(value)` if found, or `None`.
416 /// Otherwise, returns an `Ok(value)` if found, or `None`.
413 pub fn get_str(
417 pub fn get_str(
414 &self,
418 &self,
415 section: &[u8],
419 section: &[u8],
416 item: &[u8],
420 item: &[u8],
417 ) -> Result<Option<&str>, HgError> {
421 ) -> Result<Option<&str>, HgError> {
418 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
422 self.get_parse(
419 str::from_utf8(value).ok()
423 section,
420 })
424 item,
425 "ASCII or UTF-8 string",
426 |value| str::from_utf8(value).ok(),
427 true,
428 )
429 }
430
431 /// Same as `get_str`, but doesn't fall back to the default `configitem`
432 /// if not defined in the user config.
433 pub fn get_str_no_default(
434 &self,
435 section: &[u8],
436 item: &[u8],
437 ) -> Result<Option<&str>, HgError> {
438 self.get_parse(
439 section,
440 item,
441 "ASCII or UTF-8 string",
442 |value| str::from_utf8(value).ok(),
443 false,
444 )
421 }
445 }
422
446
423 /// Returns an `Err` if the first value found is not a valid unsigned
447 /// Returns an `Err` if the first value found is not a valid unsigned
424 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
448 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
425 pub fn get_u32(
449 pub fn get_u32(
426 &self,
450 &self,
427 section: &[u8],
451 section: &[u8],
428 item: &[u8],
452 item: &[u8],
429 ) -> Result<Option<u32>, HgError> {
453 ) -> Result<Option<u32>, HgError> {
430 self.get_parse(section, item, "valid integer", |value| {
454 self.get_parse(
431 str::from_utf8(value).ok()?.parse().ok()
455 section,
432 })
456 item,
457 "valid integer",
458 |value| str::from_utf8(value).ok()?.parse().ok(),
459 true,
460 )
433 }
461 }
434
462
435 /// Returns an `Err` if the first value found is not a valid file size
463 /// Returns an `Err` if the first value found is not a valid file size
436 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
464 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
437 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
465 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
438 pub fn get_byte_size(
466 pub fn get_byte_size(
439 &self,
467 &self,
440 section: &[u8],
468 section: &[u8],
441 item: &[u8],
469 item: &[u8],
442 ) -> Result<Option<u64>, HgError> {
470 ) -> Result<Option<u64>, HgError> {
443 self.get_parse(section, item, "byte quantity", values::parse_byte_size)
471 self.get_parse(
472 section,
473 item,
474 "byte quantity",
475 values::parse_byte_size,
476 true,
477 )
444 }
478 }
445
479
446 /// Returns an `Err` if the first value found is not a valid boolean.
480 /// Returns an `Err` if the first value found is not a valid boolean.
447 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
481 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
448 /// found, or `None`.
482 /// found, or `None`.
449 pub fn get_option(
483 pub fn get_option(
450 &self,
484 &self,
451 section: &[u8],
485 section: &[u8],
452 item: &[u8],
486 item: &[u8],
453 ) -> Result<Option<bool>, HgError> {
487 ) -> Result<Option<bool>, HgError> {
454 self.get_parse(section, item, "boolean", values::parse_bool)
488 self.get_parse(section, item, "boolean", values::parse_bool, true)
489 }
490
491 /// Same as `get_option`, but doesn't fall back to the default `configitem`
492 /// if not defined in the user config.
493 pub fn get_option_no_default(
494 &self,
495 section: &[u8],
496 item: &[u8],
497 ) -> Result<Option<bool>, HgError> {
498 self.get_parse(section, item, "boolean", values::parse_bool, false)
455 }
499 }
456
500
457 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
501 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
458 /// if the value is not found, an `Err` if it's not a valid boolean.
502 /// if the value is not found, an `Err` if it's not a valid boolean.
459 pub fn get_bool(
503 pub fn get_bool(
460 &self,
504 &self,
461 section: &[u8],
505 section: &[u8],
462 item: &[u8],
506 item: &[u8],
463 ) -> Result<bool, HgError> {
507 ) -> Result<bool, HgError> {
464 Ok(self.get_option(section, item)?.unwrap_or(false))
508 Ok(self.get_option(section, item)?.unwrap_or(false))
465 }
509 }
466
510
511 /// Same as `get_bool`, but doesn't fall back to the default `configitem`
512 /// if not defined in the user config.
513 pub fn get_bool_no_default(
514 &self,
515 section: &[u8],
516 item: &[u8],
517 ) -> Result<bool, HgError> {
518 Ok(self.get_option_no_default(section, item)?.unwrap_or(false))
519 }
520
467 /// Returns `true` if the extension is enabled, `false` otherwise
521 /// Returns `true` if the extension is enabled, `false` otherwise
468 pub fn is_extension_enabled(&self, extension: &[u8]) -> bool {
522 pub fn is_extension_enabled(&self, extension: &[u8]) -> bool {
469 let value = self.get(b"extensions", extension);
523 let value = self.get(b"extensions", extension);
470 match value {
524 match value {
471 Some(c) => !c.starts_with(b"!"),
525 Some(c) => !c.starts_with(b"!"),
472 None => false,
526 None => false,
473 }
527 }
474 }
528 }
475
529
476 /// If there is an `item` value in `section`, parse and return a list of
530 /// If there is an `item` value in `section`, parse and return a list of
477 /// byte strings.
531 /// byte strings.
478 pub fn get_list(
532 pub fn get_list(
479 &self,
533 &self,
480 section: &[u8],
534 section: &[u8],
481 item: &[u8],
535 item: &[u8],
482 ) -> Option<Vec<Vec<u8>>> {
536 ) -> Option<Vec<Vec<u8>>> {
483 self.get(section, item).map(values::parse_list)
537 self.get(section, item).map(values::parse_list)
484 }
538 }
485
539
486 /// Returns the raw value bytes of the first one found, or `None`.
540 /// Returns the raw value bytes of the first one found, or `None`.
487 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
541 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
488 self.get_inner(section, item)
542 self.get_inner(section, item)
489 .map(|(_, value)| value.bytes.as_ref())
543 .map(|(_, value)| value.bytes.as_ref())
490 }
544 }
491
545
492 /// Returns the raw value bytes of the first one found, or `None`.
546 /// Returns the raw value bytes of the first one found, or `None`.
493 pub fn get_with_origin(
547 pub fn get_with_origin(
494 &self,
548 &self,
495 section: &[u8],
549 section: &[u8],
496 item: &[u8],
550 item: &[u8],
497 ) -> Option<(&[u8], &ConfigOrigin)> {
551 ) -> Option<(&[u8], &ConfigOrigin)> {
498 self.get_inner(section, item)
552 self.get_inner(section, item)
499 .map(|(layer, value)| (value.bytes.as_ref(), &layer.origin))
553 .map(|(layer, value)| (value.bytes.as_ref(), &layer.origin))
500 }
554 }
501
555
502 /// Returns the layer and the value of the first one found, or `None`.
556 /// Returns the layer and the value of the first one found, or `None`.
503 fn get_inner(
557 fn get_inner(
504 &self,
558 &self,
505 section: &[u8],
559 section: &[u8],
506 item: &[u8],
560 item: &[u8],
507 ) -> Option<(&ConfigLayer, &ConfigValue)> {
561 ) -> Option<(&ConfigLayer, &ConfigValue)> {
508 // Filter out the config items that are hidden by [PLAIN].
562 // Filter out the config items that are hidden by [PLAIN].
509 // This differs from python hg where we delete them from the config.
563 // This differs from python hg where we delete them from the config.
510 let should_ignore = should_ignore(&self.plain, section, item);
564 let should_ignore = should_ignore(&self.plain, section, item);
511 for layer in self.layers.iter().rev() {
565 for layer in self.layers.iter().rev() {
512 if !layer.trusted {
566 if !layer.trusted {
513 continue;
567 continue;
514 }
568 }
515 //The [PLAIN] config should not affect the defaults.
569 //The [PLAIN] config should not affect the defaults.
516 //
570 //
517 // However, PLAIN should also affect the "tweaked" defaults (unless
571 // However, PLAIN should also affect the "tweaked" defaults (unless
518 // "tweakdefault" is part of "HGPLAINEXCEPT").
572 // "tweakdefault" is part of "HGPLAINEXCEPT").
519 //
573 //
520 // In practice the tweak-default layer is only added when it is
574 // In practice the tweak-default layer is only added when it is
521 // relevant, so we can safely always take it into
575 // relevant, so we can safely always take it into
522 // account here.
576 // account here.
523 if should_ignore && !(layer.origin == ConfigOrigin::Tweakdefaults)
577 if should_ignore && !(layer.origin == ConfigOrigin::Tweakdefaults)
524 {
578 {
525 continue;
579 continue;
526 }
580 }
527 if let Some(v) = layer.get(section, item) {
581 if let Some(v) = layer.get(section, item) {
528 return Some((layer, v));
582 return Some((layer, v));
529 }
583 }
530 }
584 }
531 None
585 None
532 }
586 }
533
587
534 /// Return all keys defined for the given section
588 /// Return all keys defined for the given section
535 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
589 pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
536 self.layers
590 self.layers
537 .iter()
591 .iter()
538 .flat_map(|layer| layer.iter_keys(section))
592 .flat_map(|layer| layer.iter_keys(section))
539 .collect()
593 .collect()
540 }
594 }
541
595
542 /// Returns whether any key is defined in the given section
596 /// Returns whether any key is defined in the given section
543 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
597 pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
544 self.layers
598 self.layers
545 .iter()
599 .iter()
546 .any(|layer| layer.has_non_empty_section(section))
600 .any(|layer| layer.has_non_empty_section(section))
547 }
601 }
548
602
549 /// Yields (key, value) pairs for everything in the given section
603 /// Yields (key, value) pairs for everything in the given section
550 pub fn iter_section<'a>(
604 pub fn iter_section<'a>(
551 &'a self,
605 &'a self,
552 section: &'a [u8],
606 section: &'a [u8],
553 ) -> impl Iterator<Item = (&[u8], &[u8])> + 'a {
607 ) -> impl Iterator<Item = (&[u8], &[u8])> + 'a {
554 // Deduplicate keys redefined in multiple layers
608 // Deduplicate keys redefined in multiple layers
555 let mut keys_already_seen = HashSet::new();
609 let mut keys_already_seen = HashSet::new();
556 let mut key_is_new =
610 let mut key_is_new =
557 move |&(key, _value): &(&'a [u8], &'a [u8])| -> bool {
611 move |&(key, _value): &(&'a [u8], &'a [u8])| -> bool {
558 keys_already_seen.insert(key)
612 keys_already_seen.insert(key)
559 };
613 };
560 // This is similar to `flat_map` + `filter_map`, except with a single
614 // This is similar to `flat_map` + `filter_map`, except with a single
561 // closure that owns `key_is_new` (and therefore the
615 // closure that owns `key_is_new` (and therefore the
562 // `keys_already_seen` set):
616 // `keys_already_seen` set):
563 let mut layer_iters = self
617 let mut layer_iters = self
564 .layers
618 .layers
565 .iter()
619 .iter()
566 .rev()
620 .rev()
567 .map(move |layer| layer.iter_section(section))
621 .map(move |layer| layer.iter_section(section))
568 .peekable();
622 .peekable();
569 std::iter::from_fn(move || loop {
623 std::iter::from_fn(move || loop {
570 if let Some(pair) = layer_iters.peek_mut()?.find(&mut key_is_new) {
624 if let Some(pair) = layer_iters.peek_mut()?.find(&mut key_is_new) {
571 return Some(pair);
625 return Some(pair);
572 } else {
626 } else {
573 layer_iters.next();
627 layer_iters.next();
574 }
628 }
575 })
629 })
576 }
630 }
577
631
578 /// Get raw values bytes from all layers (even untrusted ones) in order
632 /// Get raw values bytes from all layers (even untrusted ones) in order
579 /// of precedence.
633 /// of precedence.
580 #[cfg(test)]
634 #[cfg(test)]
581 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
635 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
582 let mut res = vec![];
636 let mut res = vec![];
583 for layer in self.layers.iter().rev() {
637 for layer in self.layers.iter().rev() {
584 if let Some(v) = layer.get(section, item) {
638 if let Some(v) = layer.get(section, item) {
585 res.push(v.bytes.as_ref());
639 res.push(v.bytes.as_ref());
586 }
640 }
587 }
641 }
588 res
642 res
589 }
643 }
590
644
591 // a config layer that's introduced by ui.tweakdefaults
645 // a config layer that's introduced by ui.tweakdefaults
592 fn tweakdefaults_layer() -> ConfigLayer {
646 fn tweakdefaults_layer() -> ConfigLayer {
593 let mut layer = ConfigLayer::new(ConfigOrigin::Tweakdefaults);
647 let mut layer = ConfigLayer::new(ConfigOrigin::Tweakdefaults);
594
648
595 let mut add = |section: &[u8], item: &[u8], value: &[u8]| {
649 let mut add = |section: &[u8], item: &[u8], value: &[u8]| {
596 layer.add(
650 layer.add(
597 section[..].into(),
651 section[..].into(),
598 item[..].into(),
652 item[..].into(),
599 value[..].into(),
653 value[..].into(),
600 None,
654 None,
601 );
655 );
602 };
656 };
603 // duplication of [tweakrc] from [ui.py]
657 // duplication of [tweakrc] from [ui.py]
604 add(b"ui", b"rollback", b"False");
658 add(b"ui", b"rollback", b"False");
605 add(b"ui", b"statuscopies", b"yes");
659 add(b"ui", b"statuscopies", b"yes");
606 add(b"ui", b"interface", b"curses");
660 add(b"ui", b"interface", b"curses");
607 add(b"ui", b"relative-paths", b"yes");
661 add(b"ui", b"relative-paths", b"yes");
608 add(b"commands", b"grep.all-files", b"True");
662 add(b"commands", b"grep.all-files", b"True");
609 add(b"commands", b"update.check", b"noconflict");
663 add(b"commands", b"update.check", b"noconflict");
610 add(b"commands", b"status.verbose", b"True");
664 add(b"commands", b"status.verbose", b"True");
611 add(b"commands", b"resolve.explicit-re-merge", b"True");
665 add(b"commands", b"resolve.explicit-re-merge", b"True");
612 add(b"git", b"git", b"1");
666 add(b"git", b"git", b"1");
613 add(b"git", b"showfunc", b"1");
667 add(b"git", b"showfunc", b"1");
614 add(b"git", b"word-diff", b"1");
668 add(b"git", b"word-diff", b"1");
615 layer
669 layer
616 }
670 }
617
671
618 // introduce the tweaked defaults as implied by ui.tweakdefaults
672 // introduce the tweaked defaults as implied by ui.tweakdefaults
619 pub fn tweakdefaults(&mut self) {
673 pub fn tweakdefaults(&mut self) {
620 self.layers.insert(0, Config::tweakdefaults_layer());
674 self.layers.insert(0, Config::tweakdefaults_layer());
621 }
675 }
622 }
676 }
623
677
624 #[cfg(test)]
678 #[cfg(test)]
625 mod tests {
679 mod tests {
626 use super::*;
680 use super::*;
627 use pretty_assertions::assert_eq;
681 use pretty_assertions::assert_eq;
628 use std::fs::File;
682 use std::fs::File;
629 use std::io::Write;
683 use std::io::Write;
630
684
631 #[test]
685 #[test]
632 fn test_include_layer_ordering() {
686 fn test_include_layer_ordering() {
633 let tmpdir = tempfile::tempdir().unwrap();
687 let tmpdir = tempfile::tempdir().unwrap();
634 let tmpdir_path = tmpdir.path();
688 let tmpdir_path = tmpdir.path();
635 let mut included_file =
689 let mut included_file =
636 File::create(&tmpdir_path.join("included.rc")).unwrap();
690 File::create(&tmpdir_path.join("included.rc")).unwrap();
637
691
638 included_file.write_all(b"[section]\nitem=value1").unwrap();
692 included_file.write_all(b"[section]\nitem=value1").unwrap();
639 let base_config_path = tmpdir_path.join("base.rc");
693 let base_config_path = tmpdir_path.join("base.rc");
640 let mut config_file = File::create(&base_config_path).unwrap();
694 let mut config_file = File::create(&base_config_path).unwrap();
641 let data =
695 let data =
642 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
696 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
643 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
697 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
644 config_file.write_all(data).unwrap();
698 config_file.write_all(data).unwrap();
645
699
646 let sources = vec![ConfigSource::AbsPath(base_config_path)];
700 let sources = vec![ConfigSource::AbsPath(base_config_path)];
647 let config = Config::load_from_explicit_sources(sources)
701 let config = Config::load_from_explicit_sources(sources)
648 .expect("expected valid config");
702 .expect("expected valid config");
649
703
650 let (_, value) = config.get_inner(b"section", b"item").unwrap();
704 let (_, value) = config.get_inner(b"section", b"item").unwrap();
651 assert_eq!(
705 assert_eq!(
652 value,
706 value,
653 &ConfigValue {
707 &ConfigValue {
654 bytes: b"value2".to_vec(),
708 bytes: b"value2".to_vec(),
655 line: Some(4)
709 line: Some(4)
656 }
710 }
657 );
711 );
658
712
659 let value = config.get(b"section", b"item").unwrap();
713 let value = config.get(b"section", b"item").unwrap();
660 assert_eq!(value, b"value2",);
714 assert_eq!(value, b"value2",);
661 assert_eq!(
715 assert_eq!(
662 config.get_all(b"section", b"item"),
716 config.get_all(b"section", b"item"),
663 [b"value2", b"value1", b"value0"]
717 [b"value2", b"value1", b"value0"]
664 );
718 );
665
719
666 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
720 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
667 assert_eq!(
721 assert_eq!(
668 config.get_byte_size(b"section2", b"size").unwrap(),
722 config.get_byte_size(b"section2", b"size").unwrap(),
669 Some(1024 + 512)
723 Some(1024 + 512)
670 );
724 );
671 assert!(config.get_u32(b"section2", b"not-count").is_err());
725 assert!(config.get_u32(b"section2", b"not-count").is_err());
672 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
726 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
673 }
727 }
674 }
728 }
@@ -1,832 +1,833
1 extern crate log;
1 extern crate log;
2 use crate::error::CommandError;
2 use crate::error::CommandError;
3 use crate::ui::{local_to_utf8, Ui};
3 use crate::ui::{local_to_utf8, Ui};
4 use clap::{command, Arg, ArgMatches};
4 use clap::{command, Arg, ArgMatches};
5 use format_bytes::{format_bytes, join};
5 use format_bytes::{format_bytes, join};
6 use hg::config::{Config, ConfigSource, PlainInfo};
6 use hg::config::{Config, ConfigSource, PlainInfo};
7 use hg::repo::{Repo, RepoError};
7 use hg::repo::{Repo, RepoError};
8 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
8 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
9 use hg::utils::SliceExt;
9 use hg::utils::SliceExt;
10 use hg::{exit_codes, requirements};
10 use hg::{exit_codes, requirements};
11 use std::borrow::Cow;
11 use std::borrow::Cow;
12 use std::collections::HashSet;
12 use std::collections::HashSet;
13 use std::ffi::OsString;
13 use std::ffi::OsString;
14 use std::os::unix::prelude::CommandExt;
14 use std::os::unix::prelude::CommandExt;
15 use std::path::PathBuf;
15 use std::path::PathBuf;
16 use std::process::Command;
16 use std::process::Command;
17
17
18 mod blackbox;
18 mod blackbox;
19 mod color;
19 mod color;
20 mod error;
20 mod error;
21 mod ui;
21 mod ui;
22 pub mod utils {
22 pub mod utils {
23 pub mod path_utils;
23 pub mod path_utils;
24 }
24 }
25
25
26 fn main_with_result(
26 fn main_with_result(
27 argv: Vec<OsString>,
27 argv: Vec<OsString>,
28 process_start_time: &blackbox::ProcessStartTime,
28 process_start_time: &blackbox::ProcessStartTime,
29 ui: &ui::Ui,
29 ui: &ui::Ui,
30 repo: Result<&Repo, &NoRepoInCwdError>,
30 repo: Result<&Repo, &NoRepoInCwdError>,
31 config: &Config,
31 config: &Config,
32 ) -> Result<(), CommandError> {
32 ) -> Result<(), CommandError> {
33 check_unsupported(config, repo)?;
33 check_unsupported(config, repo)?;
34
34
35 let app = command!()
35 let app = command!()
36 .subcommand_required(true)
36 .subcommand_required(true)
37 .arg(
37 .arg(
38 Arg::new("repository")
38 Arg::new("repository")
39 .help("repository root directory")
39 .help("repository root directory")
40 .short('R')
40 .short('R')
41 .value_name("REPO")
41 .value_name("REPO")
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::new("config")
46 Arg::new("config")
47 .help("set/override config option (use 'section.name=value')")
47 .help("set/override config option (use 'section.name=value')")
48 .value_name("CONFIG")
48 .value_name("CONFIG")
49 .global(true)
49 .global(true)
50 .long("config")
50 .long("config")
51 // Ok: `--config section.key1=val --config section.key2=val2`
51 // Ok: `--config section.key1=val --config section.key2=val2`
52 // Not ok: `--config section.key1=val section.key2=val2`
52 // Not ok: `--config section.key1=val section.key2=val2`
53 .action(clap::ArgAction::Append),
53 .action(clap::ArgAction::Append),
54 )
54 )
55 .arg(
55 .arg(
56 Arg::new("cwd")
56 Arg::new("cwd")
57 .help("change working directory")
57 .help("change working directory")
58 .value_name("DIR")
58 .value_name("DIR")
59 .long("cwd")
59 .long("cwd")
60 .global(true),
60 .global(true),
61 )
61 )
62 .arg(
62 .arg(
63 Arg::new("color")
63 Arg::new("color")
64 .help("when to colorize (boolean, always, auto, never, or debug)")
64 .help("when to colorize (boolean, always, auto, never, or debug)")
65 .value_name("TYPE")
65 .value_name("TYPE")
66 .long("color")
66 .long("color")
67 .global(true),
67 .global(true),
68 )
68 )
69 .version("0.0.1");
69 .version("0.0.1");
70 let app = add_subcommand_args(app);
70 let app = add_subcommand_args(app);
71
71
72 let matches = app.try_get_matches_from(argv.iter())?;
72 let matches = app.try_get_matches_from(argv.iter())?;
73
73
74 let (subcommand_name, subcommand_args) =
74 let (subcommand_name, subcommand_args) =
75 matches.subcommand().expect("subcommand required");
75 matches.subcommand().expect("subcommand required");
76
76
77 // Mercurial allows users to define "defaults" for commands, fallback
77 // Mercurial allows users to define "defaults" for commands, fallback
78 // if a default is detected for the current command
78 // if a default is detected for the current command
79 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
79 let defaults = config.get_str(b"defaults", subcommand_name.as_bytes());
80 if defaults?.is_some() {
80 if defaults?.is_some() {
81 let msg = "`defaults` config set";
81 let msg = "`defaults` config set";
82 return Err(CommandError::unsupported(msg));
82 return Err(CommandError::unsupported(msg));
83 }
83 }
84
84
85 for prefix in ["pre", "post", "fail"].iter() {
85 for prefix in ["pre", "post", "fail"].iter() {
86 // Mercurial allows users to define generic hooks for commands,
86 // Mercurial allows users to define generic hooks for commands,
87 // fallback if any are detected
87 // fallback if any are detected
88 let item = format!("{}-{}", prefix, subcommand_name);
88 let item = format!("{}-{}", prefix, subcommand_name);
89 let hook_for_command = config.get_str(b"hooks", item.as_bytes())?;
89 let hook_for_command =
90 config.get_str_no_default(b"hooks", item.as_bytes())?;
90 if hook_for_command.is_some() {
91 if hook_for_command.is_some() {
91 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
92 let msg = format!("{}-{} hook defined", prefix, subcommand_name);
92 return Err(CommandError::unsupported(msg));
93 return Err(CommandError::unsupported(msg));
93 }
94 }
94 }
95 }
95 let run = subcommand_run_fn(subcommand_name)
96 let run = subcommand_run_fn(subcommand_name)
96 .expect("unknown subcommand name from clap despite Command::subcommand_required");
97 .expect("unknown subcommand name from clap despite Command::subcommand_required");
97
98
98 let invocation = CliInvocation {
99 let invocation = CliInvocation {
99 ui,
100 ui,
100 subcommand_args,
101 subcommand_args,
101 config,
102 config,
102 repo,
103 repo,
103 };
104 };
104
105
105 if let Ok(repo) = repo {
106 if let Ok(repo) = repo {
106 // We don't support subrepos, fallback if the subrepos file is present
107 // We don't support subrepos, fallback if the subrepos file is present
107 if repo.working_directory_vfs().join(".hgsub").exists() {
108 if repo.working_directory_vfs().join(".hgsub").exists() {
108 let msg = "subrepos (.hgsub is present)";
109 let msg = "subrepos (.hgsub is present)";
109 return Err(CommandError::unsupported(msg));
110 return Err(CommandError::unsupported(msg));
110 }
111 }
111 }
112 }
112
113
113 if config.is_extension_enabled(b"blackbox") {
114 if config.is_extension_enabled(b"blackbox") {
114 let blackbox =
115 let blackbox =
115 blackbox::Blackbox::new(&invocation, process_start_time)?;
116 blackbox::Blackbox::new(&invocation, process_start_time)?;
116 blackbox.log_command_start(argv.iter());
117 blackbox.log_command_start(argv.iter());
117 let result = run(&invocation);
118 let result = run(&invocation);
118 blackbox.log_command_end(
119 blackbox.log_command_end(
119 argv.iter(),
120 argv.iter(),
120 exit_code(
121 exit_code(
121 &result,
122 &result,
122 // TODO: show a warning or combine with original error if
123 // TODO: show a warning or combine with original error if
123 // `get_bool` returns an error
124 // `get_bool` returns an error
124 config
125 config
125 .get_bool(b"ui", b"detailed-exit-code")
126 .get_bool(b"ui", b"detailed-exit-code")
126 .unwrap_or(false),
127 .unwrap_or(false),
127 ),
128 ),
128 );
129 );
129 result
130 result
130 } else {
131 } else {
131 run(&invocation)
132 run(&invocation)
132 }
133 }
133 }
134 }
134
135
135 fn rhg_main(argv: Vec<OsString>) -> ! {
136 fn rhg_main(argv: Vec<OsString>) -> ! {
136 // Run this first, before we find out if the blackbox extension is even
137 // Run this first, before we find out if the blackbox extension is even
137 // enabled, in order to include everything in-between in the duration
138 // enabled, in order to include everything in-between in the duration
138 // measurements. Reading config files can be slow if they’re on NFS.
139 // measurements. Reading config files can be slow if they’re on NFS.
139 let process_start_time = blackbox::ProcessStartTime::now();
140 let process_start_time = blackbox::ProcessStartTime::now();
140
141
141 env_logger::init();
142 env_logger::init();
142
143
143 // Make sure nothing in a future version of `rhg` sets the global
144 // Make sure nothing in a future version of `rhg` sets the global
144 // threadpool before we can cap default threads. (This is also called
145 // threadpool before we can cap default threads. (This is also called
145 // in core because Python uses the same code path, we're adding a
146 // in core because Python uses the same code path, we're adding a
146 // redundant check.)
147 // redundant check.)
147 hg::utils::cap_default_rayon_threads()
148 hg::utils::cap_default_rayon_threads()
148 .expect("Rayon threadpool already initialized");
149 .expect("Rayon threadpool already initialized");
149
150
150 let early_args = EarlyArgs::parse(&argv);
151 let early_args = EarlyArgs::parse(&argv);
151
152
152 let initial_current_dir = early_args.cwd.map(|cwd| {
153 let initial_current_dir = early_args.cwd.map(|cwd| {
153 let cwd = get_path_from_bytes(&cwd);
154 let cwd = get_path_from_bytes(&cwd);
154 std::env::current_dir()
155 std::env::current_dir()
155 .and_then(|initial| {
156 .and_then(|initial| {
156 std::env::set_current_dir(cwd)?;
157 std::env::set_current_dir(cwd)?;
157 Ok(initial)
158 Ok(initial)
158 })
159 })
159 .unwrap_or_else(|error| {
160 .unwrap_or_else(|error| {
160 exit(
161 exit(
161 &argv,
162 &argv,
162 &None,
163 &None,
163 &Ui::new_infallible(&Config::empty()),
164 &Ui::new_infallible(&Config::empty()),
164 OnUnsupported::Abort,
165 OnUnsupported::Abort,
165 Err(CommandError::abort(format!(
166 Err(CommandError::abort(format!(
166 "abort: {}: '{}'",
167 "abort: {}: '{}'",
167 error,
168 error,
168 cwd.display()
169 cwd.display()
169 ))),
170 ))),
170 false,
171 false,
171 )
172 )
172 })
173 })
173 });
174 });
174
175
175 let mut non_repo_config =
176 let mut non_repo_config =
176 Config::load_non_repo().unwrap_or_else(|error| {
177 Config::load_non_repo().unwrap_or_else(|error| {
177 // Normally this is decided based on config, but we don’t have that
178 // Normally this is decided based on config, but we don’t have that
178 // available. As of this writing config loading never returns an
179 // available. As of this writing config loading never returns an
179 // "unsupported" error but that is not enforced by the type system.
180 // "unsupported" error but that is not enforced by the type system.
180 let on_unsupported = OnUnsupported::Abort;
181 let on_unsupported = OnUnsupported::Abort;
181
182
182 exit(
183 exit(
183 &argv,
184 &argv,
184 &initial_current_dir,
185 &initial_current_dir,
185 &Ui::new_infallible(&Config::empty()),
186 &Ui::new_infallible(&Config::empty()),
186 on_unsupported,
187 on_unsupported,
187 Err(error.into()),
188 Err(error.into()),
188 false,
189 false,
189 )
190 )
190 });
191 });
191
192
192 non_repo_config
193 non_repo_config
193 .load_cli_args(early_args.config, early_args.color)
194 .load_cli_args(early_args.config, early_args.color)
194 .unwrap_or_else(|error| {
195 .unwrap_or_else(|error| {
195 exit(
196 exit(
196 &argv,
197 &argv,
197 &initial_current_dir,
198 &initial_current_dir,
198 &Ui::new_infallible(&non_repo_config),
199 &Ui::new_infallible(&non_repo_config),
199 OnUnsupported::from_config(&non_repo_config),
200 OnUnsupported::from_config(&non_repo_config),
200 Err(error.into()),
201 Err(error.into()),
201 non_repo_config
202 non_repo_config
202 .get_bool(b"ui", b"detailed-exit-code")
203 .get_bool(b"ui", b"detailed-exit-code")
203 .unwrap_or(false),
204 .unwrap_or(false),
204 )
205 )
205 });
206 });
206
207
207 if let Some(repo_path_bytes) = &early_args.repo {
208 if let Some(repo_path_bytes) = &early_args.repo {
208 lazy_static::lazy_static! {
209 lazy_static::lazy_static! {
209 static ref SCHEME_RE: regex::bytes::Regex =
210 static ref SCHEME_RE: regex::bytes::Regex =
210 // Same as `_matchscheme` in `mercurial/util.py`
211 // Same as `_matchscheme` in `mercurial/util.py`
211 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
212 regex::bytes::Regex::new("^[a-zA-Z0-9+.\\-]+:").unwrap();
212 }
213 }
213 if SCHEME_RE.is_match(repo_path_bytes) {
214 if SCHEME_RE.is_match(repo_path_bytes) {
214 exit(
215 exit(
215 &argv,
216 &argv,
216 &initial_current_dir,
217 &initial_current_dir,
217 &Ui::new_infallible(&non_repo_config),
218 &Ui::new_infallible(&non_repo_config),
218 OnUnsupported::from_config(&non_repo_config),
219 OnUnsupported::from_config(&non_repo_config),
219 Err(CommandError::UnsupportedFeature {
220 Err(CommandError::UnsupportedFeature {
220 message: format_bytes!(
221 message: format_bytes!(
221 b"URL-like --repository {}",
222 b"URL-like --repository {}",
222 repo_path_bytes
223 repo_path_bytes
223 ),
224 ),
224 }),
225 }),
225 // TODO: show a warning or combine with original error if
226 // TODO: show a warning or combine with original error if
226 // `get_bool` returns an error
227 // `get_bool` returns an error
227 non_repo_config
228 non_repo_config
228 .get_bool(b"ui", b"detailed-exit-code")
229 .get_bool(b"ui", b"detailed-exit-code")
229 .unwrap_or(false),
230 .unwrap_or(false),
230 )
231 )
231 }
232 }
232 }
233 }
233 let repo_arg = early_args.repo.unwrap_or_default();
234 let repo_arg = early_args.repo.unwrap_or_default();
234 let repo_path: Option<PathBuf> = {
235 let repo_path: Option<PathBuf> = {
235 if repo_arg.is_empty() {
236 if repo_arg.is_empty() {
236 None
237 None
237 } else {
238 } else {
238 let local_config = {
239 let local_config = {
239 if std::env::var_os("HGRCSKIPREPO").is_none() {
240 if std::env::var_os("HGRCSKIPREPO").is_none() {
240 // TODO: handle errors from find_repo_root
241 // TODO: handle errors from find_repo_root
241 if let Ok(current_dir_path) = Repo::find_repo_root() {
242 if let Ok(current_dir_path) = Repo::find_repo_root() {
242 let config_files = vec![
243 let config_files = vec![
243 ConfigSource::AbsPath(
244 ConfigSource::AbsPath(
244 current_dir_path.join(".hg/hgrc"),
245 current_dir_path.join(".hg/hgrc"),
245 ),
246 ),
246 ConfigSource::AbsPath(
247 ConfigSource::AbsPath(
247 current_dir_path.join(".hg/hgrc-not-shared"),
248 current_dir_path.join(".hg/hgrc-not-shared"),
248 ),
249 ),
249 ];
250 ];
250 // TODO: handle errors from
251 // TODO: handle errors from
251 // `load_from_explicit_sources`
252 // `load_from_explicit_sources`
252 Config::load_from_explicit_sources(config_files).ok()
253 Config::load_from_explicit_sources(config_files).ok()
253 } else {
254 } else {
254 None
255 None
255 }
256 }
256 } else {
257 } else {
257 None
258 None
258 }
259 }
259 };
260 };
260
261
261 let non_repo_config_val = {
262 let non_repo_config_val = {
262 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
263 let non_repo_val = non_repo_config.get(b"paths", &repo_arg);
263 match &non_repo_val {
264 match &non_repo_val {
264 Some(val) if !val.is_empty() => home::home_dir()
265 Some(val) if !val.is_empty() => home::home_dir()
265 .unwrap_or_else(|| PathBuf::from("~"))
266 .unwrap_or_else(|| PathBuf::from("~"))
266 .join(get_path_from_bytes(val))
267 .join(get_path_from_bytes(val))
267 .canonicalize()
268 .canonicalize()
268 // TODO: handle error and make it similar to python
269 // TODO: handle error and make it similar to python
269 // implementation maybe?
270 // implementation maybe?
270 .ok(),
271 .ok(),
271 _ => None,
272 _ => None,
272 }
273 }
273 };
274 };
274
275
275 let config_val = match &local_config {
276 let config_val = match &local_config {
276 None => non_repo_config_val,
277 None => non_repo_config_val,
277 Some(val) => {
278 Some(val) => {
278 let local_config_val = val.get(b"paths", &repo_arg);
279 let local_config_val = val.get(b"paths", &repo_arg);
279 match &local_config_val {
280 match &local_config_val {
280 Some(val) if !val.is_empty() => {
281 Some(val) if !val.is_empty() => {
281 // presence of a local_config assures that
282 // presence of a local_config assures that
282 // current_dir
283 // current_dir
283 // wont result in an Error
284 // wont result in an Error
284 let canpath = hg::utils::current_dir()
285 let canpath = hg::utils::current_dir()
285 .unwrap()
286 .unwrap()
286 .join(get_path_from_bytes(val))
287 .join(get_path_from_bytes(val))
287 .canonicalize();
288 .canonicalize();
288 canpath.ok().or(non_repo_config_val)
289 canpath.ok().or(non_repo_config_val)
289 }
290 }
290 _ => non_repo_config_val,
291 _ => non_repo_config_val,
291 }
292 }
292 }
293 }
293 };
294 };
294 config_val
295 config_val
295 .or_else(|| Some(get_path_from_bytes(&repo_arg).to_path_buf()))
296 .or_else(|| Some(get_path_from_bytes(&repo_arg).to_path_buf()))
296 }
297 }
297 };
298 };
298
299
299 let simple_exit =
300 let simple_exit =
300 |ui: &Ui, config: &Config, result: Result<(), CommandError>| -> ! {
301 |ui: &Ui, config: &Config, result: Result<(), CommandError>| -> ! {
301 exit(
302 exit(
302 &argv,
303 &argv,
303 &initial_current_dir,
304 &initial_current_dir,
304 ui,
305 ui,
305 OnUnsupported::from_config(config),
306 OnUnsupported::from_config(config),
306 result,
307 result,
307 // TODO: show a warning or combine with original error if
308 // TODO: show a warning or combine with original error if
308 // `get_bool` returns an error
309 // `get_bool` returns an error
309 non_repo_config
310 non_repo_config
310 .get_bool(b"ui", b"detailed-exit-code")
311 .get_bool(b"ui", b"detailed-exit-code")
311 .unwrap_or(false),
312 .unwrap_or(false),
312 )
313 )
313 };
314 };
314 let early_exit = |config: &Config, error: CommandError| -> ! {
315 let early_exit = |config: &Config, error: CommandError| -> ! {
315 simple_exit(&Ui::new_infallible(config), config, Err(error))
316 simple_exit(&Ui::new_infallible(config), config, Err(error))
316 };
317 };
317 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
318 let repo_result = match Repo::find(&non_repo_config, repo_path.to_owned())
318 {
319 {
319 Ok(repo) => Ok(repo),
320 Ok(repo) => Ok(repo),
320 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
321 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
321 // Not finding a repo is not fatal yet, if `-R` was not given
322 // Not finding a repo is not fatal yet, if `-R` was not given
322 Err(NoRepoInCwdError { cwd: at })
323 Err(NoRepoInCwdError { cwd: at })
323 }
324 }
324 Err(error) => early_exit(&non_repo_config, error.into()),
325 Err(error) => early_exit(&non_repo_config, error.into()),
325 };
326 };
326
327
327 let config = if let Ok(repo) = &repo_result {
328 let config = if let Ok(repo) = &repo_result {
328 repo.config()
329 repo.config()
329 } else {
330 } else {
330 &non_repo_config
331 &non_repo_config
331 };
332 };
332
333
333 let mut config_cow = Cow::Borrowed(config);
334 let mut config_cow = Cow::Borrowed(config);
334 config_cow.to_mut().apply_plain(PlainInfo::from_env());
335 config_cow.to_mut().apply_plain(PlainInfo::from_env());
335 if !ui::plain(Some("tweakdefaults"))
336 if !ui::plain(Some("tweakdefaults"))
336 && config_cow
337 && config_cow
337 .as_ref()
338 .as_ref()
338 .get_bool(b"ui", b"tweakdefaults")
339 .get_bool(b"ui", b"tweakdefaults")
339 .unwrap_or_else(|error| early_exit(config, error.into()))
340 .unwrap_or_else(|error| early_exit(config, error.into()))
340 {
341 {
341 config_cow.to_mut().tweakdefaults()
342 config_cow.to_mut().tweakdefaults()
342 };
343 };
343 let config = config_cow.as_ref();
344 let config = config_cow.as_ref();
344 let ui = Ui::new(config)
345 let ui = Ui::new(config)
345 .unwrap_or_else(|error| early_exit(config, error.into()));
346 .unwrap_or_else(|error| early_exit(config, error.into()));
346
347
347 if let Ok(true) = config.get_bool(b"rhg", b"fallback-immediately") {
348 if let Ok(true) = config.get_bool(b"rhg", b"fallback-immediately") {
348 exit(
349 exit(
349 &argv,
350 &argv,
350 &initial_current_dir,
351 &initial_current_dir,
351 &ui,
352 &ui,
352 OnUnsupported::Fallback {
353 OnUnsupported::Fallback {
353 executable: config
354 executable: config
354 .get(b"rhg", b"fallback-executable")
355 .get(b"rhg", b"fallback-executable")
355 .map(ToOwned::to_owned),
356 .map(ToOwned::to_owned),
356 },
357 },
357 Err(CommandError::unsupported(
358 Err(CommandError::unsupported(
358 "`rhg.fallback-immediately is true`",
359 "`rhg.fallback-immediately is true`",
359 )),
360 )),
360 false,
361 false,
361 )
362 )
362 }
363 }
363
364
364 let result = main_with_result(
365 let result = main_with_result(
365 argv.iter().map(|s| s.to_owned()).collect(),
366 argv.iter().map(|s| s.to_owned()).collect(),
366 &process_start_time,
367 &process_start_time,
367 &ui,
368 &ui,
368 repo_result.as_ref(),
369 repo_result.as_ref(),
369 config,
370 config,
370 );
371 );
371 simple_exit(&ui, config, result)
372 simple_exit(&ui, config, result)
372 }
373 }
373
374
374 fn main() -> ! {
375 fn main() -> ! {
375 rhg_main(std::env::args_os().collect())
376 rhg_main(std::env::args_os().collect())
376 }
377 }
377
378
378 fn exit_code(
379 fn exit_code(
379 result: &Result<(), CommandError>,
380 result: &Result<(), CommandError>,
380 use_detailed_exit_code: bool,
381 use_detailed_exit_code: bool,
381 ) -> i32 {
382 ) -> i32 {
382 match result {
383 match result {
383 Ok(()) => exit_codes::OK,
384 Ok(()) => exit_codes::OK,
384 Err(CommandError::Abort {
385 Err(CommandError::Abort {
385 detailed_exit_code, ..
386 detailed_exit_code, ..
386 }) => {
387 }) => {
387 if use_detailed_exit_code {
388 if use_detailed_exit_code {
388 *detailed_exit_code
389 *detailed_exit_code
389 } else {
390 } else {
390 exit_codes::ABORT
391 exit_codes::ABORT
391 }
392 }
392 }
393 }
393 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
394 Err(CommandError::Unsuccessful) => exit_codes::UNSUCCESSFUL,
394 // Exit with a specific code and no error message to let a potential
395 // Exit with a specific code and no error message to let a potential
395 // wrapper script fallback to Python-based Mercurial.
396 // wrapper script fallback to Python-based Mercurial.
396 Err(CommandError::UnsupportedFeature { .. }) => {
397 Err(CommandError::UnsupportedFeature { .. }) => {
397 exit_codes::UNIMPLEMENTED
398 exit_codes::UNIMPLEMENTED
398 }
399 }
399 Err(CommandError::InvalidFallback { .. }) => {
400 Err(CommandError::InvalidFallback { .. }) => {
400 exit_codes::INVALID_FALLBACK
401 exit_codes::INVALID_FALLBACK
401 }
402 }
402 }
403 }
403 }
404 }
404
405
405 fn exit<'a>(
406 fn exit<'a>(
406 original_args: &'a [OsString],
407 original_args: &'a [OsString],
407 initial_current_dir: &Option<PathBuf>,
408 initial_current_dir: &Option<PathBuf>,
408 ui: &Ui,
409 ui: &Ui,
409 mut on_unsupported: OnUnsupported,
410 mut on_unsupported: OnUnsupported,
410 result: Result<(), CommandError>,
411 result: Result<(), CommandError>,
411 use_detailed_exit_code: bool,
412 use_detailed_exit_code: bool,
412 ) -> ! {
413 ) -> ! {
413 if let (
414 if let (
414 OnUnsupported::Fallback { executable },
415 OnUnsupported::Fallback { executable },
415 Err(CommandError::UnsupportedFeature { message }),
416 Err(CommandError::UnsupportedFeature { message }),
416 ) = (&on_unsupported, &result)
417 ) = (&on_unsupported, &result)
417 {
418 {
418 let mut args = original_args.iter();
419 let mut args = original_args.iter();
419 let executable = match executable {
420 let executable = match executable {
420 None => {
421 None => {
421 exit_no_fallback(
422 exit_no_fallback(
422 ui,
423 ui,
423 OnUnsupported::Abort,
424 OnUnsupported::Abort,
424 Err(CommandError::abort(
425 Err(CommandError::abort(
425 "abort: 'rhg.on-unsupported=fallback' without \
426 "abort: 'rhg.on-unsupported=fallback' without \
426 'rhg.fallback-executable' set.",
427 'rhg.fallback-executable' set.",
427 )),
428 )),
428 false,
429 false,
429 );
430 );
430 }
431 }
431 Some(executable) => executable,
432 Some(executable) => executable,
432 };
433 };
433 let executable_path = get_path_from_bytes(executable);
434 let executable_path = get_path_from_bytes(executable);
434 let this_executable = args.next().expect("exepcted argv[0] to exist");
435 let this_executable = args.next().expect("exepcted argv[0] to exist");
435 if executable_path == *this_executable {
436 if executable_path == *this_executable {
436 // Avoid spawning infinitely many processes until resource
437 // Avoid spawning infinitely many processes until resource
437 // exhaustion.
438 // exhaustion.
438 let _ = ui.write_stderr(&format_bytes!(
439 let _ = ui.write_stderr(&format_bytes!(
439 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
440 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
440 points to `rhg` itself.\n",
441 points to `rhg` itself.\n",
441 executable
442 executable
442 ));
443 ));
443 on_unsupported = OnUnsupported::Abort
444 on_unsupported = OnUnsupported::Abort
444 } else {
445 } else {
445 log::debug!("falling back (see trace-level log)");
446 log::debug!("falling back (see trace-level log)");
446 log::trace!("{}", local_to_utf8(message));
447 log::trace!("{}", local_to_utf8(message));
447 if let Err(err) = which::which(executable_path) {
448 if let Err(err) = which::which(executable_path) {
448 exit_no_fallback(
449 exit_no_fallback(
449 ui,
450 ui,
450 OnUnsupported::Abort,
451 OnUnsupported::Abort,
451 Err(CommandError::InvalidFallback {
452 Err(CommandError::InvalidFallback {
452 path: executable.to_owned(),
453 path: executable.to_owned(),
453 err: err.to_string(),
454 err: err.to_string(),
454 }),
455 }),
455 use_detailed_exit_code,
456 use_detailed_exit_code,
456 )
457 )
457 }
458 }
458 // `args` is now `argv[1..]` since we’ve already consumed
459 // `args` is now `argv[1..]` since we’ve already consumed
459 // `argv[0]`
460 // `argv[0]`
460 let mut command = Command::new(executable_path);
461 let mut command = Command::new(executable_path);
461 command.args(args);
462 command.args(args);
462 if let Some(initial) = initial_current_dir {
463 if let Some(initial) = initial_current_dir {
463 command.current_dir(initial);
464 command.current_dir(initial);
464 }
465 }
465 // We don't use subprocess because proper signal handling is harder
466 // We don't use subprocess because proper signal handling is harder
466 // and we don't want to keep `rhg` around after a fallback anyway.
467 // and we don't want to keep `rhg` around after a fallback anyway.
467 // For example, if `rhg` is run in the background and falls back to
468 // For example, if `rhg` is run in the background and falls back to
468 // `hg` which, in turn, waits for a signal, we'll get stuck if
469 // `hg` which, in turn, waits for a signal, we'll get stuck if
469 // we're doing plain subprocess.
470 // we're doing plain subprocess.
470 //
471 //
471 // If `exec` returns, we can only assume our process is very broken
472 // If `exec` returns, we can only assume our process is very broken
472 // (see its documentation), so only try to forward the error code
473 // (see its documentation), so only try to forward the error code
473 // when exiting.
474 // when exiting.
474 let err = command.exec();
475 let err = command.exec();
475 std::process::exit(
476 std::process::exit(
476 err.raw_os_error().unwrap_or(exit_codes::ABORT),
477 err.raw_os_error().unwrap_or(exit_codes::ABORT),
477 );
478 );
478 }
479 }
479 }
480 }
480 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
481 exit_no_fallback(ui, on_unsupported, result, use_detailed_exit_code)
481 }
482 }
482
483
483 fn exit_no_fallback(
484 fn exit_no_fallback(
484 ui: &Ui,
485 ui: &Ui,
485 on_unsupported: OnUnsupported,
486 on_unsupported: OnUnsupported,
486 result: Result<(), CommandError>,
487 result: Result<(), CommandError>,
487 use_detailed_exit_code: bool,
488 use_detailed_exit_code: bool,
488 ) -> ! {
489 ) -> ! {
489 match &result {
490 match &result {
490 Ok(_) => {}
491 Ok(_) => {}
491 Err(CommandError::Unsuccessful) => {}
492 Err(CommandError::Unsuccessful) => {}
492 Err(CommandError::Abort { message, hint, .. }) => {
493 Err(CommandError::Abort { message, hint, .. }) => {
493 // Ignore errors when writing to stderr, we’re already exiting
494 // Ignore errors when writing to stderr, we’re already exiting
494 // with failure code so there’s not much more we can do.
495 // with failure code so there’s not much more we can do.
495 if !message.is_empty() {
496 if !message.is_empty() {
496 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
497 let _ = ui.write_stderr(&format_bytes!(b"{}\n", message));
497 }
498 }
498 if let Some(hint) = hint {
499 if let Some(hint) = hint {
499 let _ = ui.write_stderr(&format_bytes!(b"({})\n", hint));
500 let _ = ui.write_stderr(&format_bytes!(b"({})\n", hint));
500 }
501 }
501 }
502 }
502 Err(CommandError::UnsupportedFeature { message }) => {
503 Err(CommandError::UnsupportedFeature { message }) => {
503 match on_unsupported {
504 match on_unsupported {
504 OnUnsupported::Abort => {
505 OnUnsupported::Abort => {
505 let _ = ui.write_stderr(&format_bytes!(
506 let _ = ui.write_stderr(&format_bytes!(
506 b"unsupported feature: {}\n",
507 b"unsupported feature: {}\n",
507 message
508 message
508 ));
509 ));
509 }
510 }
510 OnUnsupported::AbortSilent => {}
511 OnUnsupported::AbortSilent => {}
511 OnUnsupported::Fallback { .. } => unreachable!(),
512 OnUnsupported::Fallback { .. } => unreachable!(),
512 }
513 }
513 }
514 }
514 Err(CommandError::InvalidFallback { path, err }) => {
515 Err(CommandError::InvalidFallback { path, err }) => {
515 let _ = ui.write_stderr(&format_bytes!(
516 let _ = ui.write_stderr(&format_bytes!(
516 b"abort: invalid fallback '{}': {}\n",
517 b"abort: invalid fallback '{}': {}\n",
517 path,
518 path,
518 err.as_bytes(),
519 err.as_bytes(),
519 ));
520 ));
520 }
521 }
521 }
522 }
522 std::process::exit(exit_code(&result, use_detailed_exit_code))
523 std::process::exit(exit_code(&result, use_detailed_exit_code))
523 }
524 }
524
525
525 macro_rules! subcommands {
526 macro_rules! subcommands {
526 ($( $command: ident )+) => {
527 ($( $command: ident )+) => {
527 mod commands {
528 mod commands {
528 $(
529 $(
529 pub mod $command;
530 pub mod $command;
530 )+
531 )+
531 }
532 }
532
533
533 fn add_subcommand_args(app: clap::Command) -> clap::Command {
534 fn add_subcommand_args(app: clap::Command) -> clap::Command {
534 app
535 app
535 $(
536 $(
536 .subcommand(commands::$command::args())
537 .subcommand(commands::$command::args())
537 )+
538 )+
538 }
539 }
539
540
540 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
541 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
541
542
542 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
543 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
543 match name {
544 match name {
544 $(
545 $(
545 stringify!($command) => Some(commands::$command::run),
546 stringify!($command) => Some(commands::$command::run),
546 )+
547 )+
547 _ => None,
548 _ => None,
548 }
549 }
549 }
550 }
550 };
551 };
551 }
552 }
552
553
553 subcommands! {
554 subcommands! {
554 cat
555 cat
555 debugdata
556 debugdata
556 debugrequirements
557 debugrequirements
557 debugignorerhg
558 debugignorerhg
558 debugrhgsparse
559 debugrhgsparse
559 files
560 files
560 root
561 root
561 config
562 config
562 status
563 status
563 }
564 }
564
565
565 pub struct CliInvocation<'a> {
566 pub struct CliInvocation<'a> {
566 ui: &'a Ui,
567 ui: &'a Ui,
567 subcommand_args: &'a ArgMatches,
568 subcommand_args: &'a ArgMatches,
568 config: &'a Config,
569 config: &'a Config,
569 /// References inside `Result` is a bit peculiar but allow
570 /// References inside `Result` is a bit peculiar but allow
570 /// `invocation.repo?` to work out with `&CliInvocation` since this
571 /// `invocation.repo?` to work out with `&CliInvocation` since this
571 /// `Result` type is `Copy`.
572 /// `Result` type is `Copy`.
572 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
573 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
573 }
574 }
574
575
575 struct NoRepoInCwdError {
576 struct NoRepoInCwdError {
576 cwd: PathBuf,
577 cwd: PathBuf,
577 }
578 }
578
579
579 /// CLI arguments to be parsed "early" in order to be able to read
580 /// CLI arguments to be parsed "early" in order to be able to read
580 /// configuration before using Clap. Ideally we would also use Clap for this,
581 /// configuration before using Clap. Ideally we would also use Clap for this,
581 /// see <https://github.com/clap-rs/clap/discussions/2366>.
582 /// see <https://github.com/clap-rs/clap/discussions/2366>.
582 ///
583 ///
583 /// These arguments are still declared when we do use Clap later, so that Clap
584 /// These arguments are still declared when we do use Clap later, so that Clap
584 /// does not return an error for their presence.
585 /// does not return an error for their presence.
585 struct EarlyArgs {
586 struct EarlyArgs {
586 /// Values of all `--config` arguments. (Possibly none)
587 /// Values of all `--config` arguments. (Possibly none)
587 config: Vec<Vec<u8>>,
588 config: Vec<Vec<u8>>,
588 /// Value of all the `--color` argument, if any.
589 /// Value of all the `--color` argument, if any.
589 color: Option<Vec<u8>>,
590 color: Option<Vec<u8>>,
590 /// Value of the `-R` or `--repository` argument, if any.
591 /// Value of the `-R` or `--repository` argument, if any.
591 repo: Option<Vec<u8>>,
592 repo: Option<Vec<u8>>,
592 /// Value of the `--cwd` argument, if any.
593 /// Value of the `--cwd` argument, if any.
593 cwd: Option<Vec<u8>>,
594 cwd: Option<Vec<u8>>,
594 }
595 }
595
596
596 impl EarlyArgs {
597 impl EarlyArgs {
597 fn parse<'a>(args: impl IntoIterator<Item = &'a OsString>) -> Self {
598 fn parse<'a>(args: impl IntoIterator<Item = &'a OsString>) -> Self {
598 let mut args = args.into_iter().map(get_bytes_from_os_str);
599 let mut args = args.into_iter().map(get_bytes_from_os_str);
599 let mut config = Vec::new();
600 let mut config = Vec::new();
600 let mut color = None;
601 let mut color = None;
601 let mut repo = None;
602 let mut repo = None;
602 let mut cwd = None;
603 let mut cwd = None;
603 // Use `while let` instead of `for` so that we can also call
604 // Use `while let` instead of `for` so that we can also call
604 // `args.next()` inside the loop.
605 // `args.next()` inside the loop.
605 while let Some(arg) = args.next() {
606 while let Some(arg) = args.next() {
606 if arg == b"--config" {
607 if arg == b"--config" {
607 if let Some(value) = args.next() {
608 if let Some(value) = args.next() {
608 config.push(value)
609 config.push(value)
609 }
610 }
610 } else if let Some(value) = arg.drop_prefix(b"--config=") {
611 } else if let Some(value) = arg.drop_prefix(b"--config=") {
611 config.push(value.to_owned())
612 config.push(value.to_owned())
612 }
613 }
613
614
614 if arg == b"--color" {
615 if arg == b"--color" {
615 if let Some(value) = args.next() {
616 if let Some(value) = args.next() {
616 color = Some(value)
617 color = Some(value)
617 }
618 }
618 } else if let Some(value) = arg.drop_prefix(b"--color=") {
619 } else if let Some(value) = arg.drop_prefix(b"--color=") {
619 color = Some(value.to_owned())
620 color = Some(value.to_owned())
620 }
621 }
621
622
622 if arg == b"--cwd" {
623 if arg == b"--cwd" {
623 if let Some(value) = args.next() {
624 if let Some(value) = args.next() {
624 cwd = Some(value)
625 cwd = Some(value)
625 }
626 }
626 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
627 } else if let Some(value) = arg.drop_prefix(b"--cwd=") {
627 cwd = Some(value.to_owned())
628 cwd = Some(value.to_owned())
628 }
629 }
629
630
630 if arg == b"--repository" || arg == b"-R" {
631 if arg == b"--repository" || arg == b"-R" {
631 if let Some(value) = args.next() {
632 if let Some(value) = args.next() {
632 repo = Some(value)
633 repo = Some(value)
633 }
634 }
634 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
635 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
635 repo = Some(value.to_owned())
636 repo = Some(value.to_owned())
636 } else if let Some(value) = arg.drop_prefix(b"-R") {
637 } else if let Some(value) = arg.drop_prefix(b"-R") {
637 repo = Some(value.to_owned())
638 repo = Some(value.to_owned())
638 }
639 }
639 }
640 }
640 Self {
641 Self {
641 config,
642 config,
642 color,
643 color,
643 repo,
644 repo,
644 cwd,
645 cwd,
645 }
646 }
646 }
647 }
647 }
648 }
648
649
649 /// What to do when encountering some unsupported feature.
650 /// What to do when encountering some unsupported feature.
650 ///
651 ///
651 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
652 /// See `HgError::UnsupportedFeature` and `CommandError::UnsupportedFeature`.
652 enum OnUnsupported {
653 enum OnUnsupported {
653 /// Print an error message describing what feature is not supported,
654 /// Print an error message describing what feature is not supported,
654 /// and exit with code 252.
655 /// and exit with code 252.
655 Abort,
656 Abort,
656 /// Silently exit with code 252.
657 /// Silently exit with code 252.
657 AbortSilent,
658 AbortSilent,
658 /// Try running a Python implementation
659 /// Try running a Python implementation
659 Fallback { executable: Option<Vec<u8>> },
660 Fallback { executable: Option<Vec<u8>> },
660 }
661 }
661
662
662 impl OnUnsupported {
663 impl OnUnsupported {
663 const DEFAULT: Self = OnUnsupported::Abort;
664 const DEFAULT: Self = OnUnsupported::Abort;
664
665
665 fn from_config(config: &Config) -> Self {
666 fn from_config(config: &Config) -> Self {
666 match config
667 match config
667 .get(b"rhg", b"on-unsupported")
668 .get(b"rhg", b"on-unsupported")
668 .map(|value| value.to_ascii_lowercase())
669 .map(|value| value.to_ascii_lowercase())
669 .as_deref()
670 .as_deref()
670 {
671 {
671 Some(b"abort") => OnUnsupported::Abort,
672 Some(b"abort") => OnUnsupported::Abort,
672 Some(b"abort-silent") => OnUnsupported::AbortSilent,
673 Some(b"abort-silent") => OnUnsupported::AbortSilent,
673 Some(b"fallback") => OnUnsupported::Fallback {
674 Some(b"fallback") => OnUnsupported::Fallback {
674 executable: config
675 executable: config
675 .get(b"rhg", b"fallback-executable")
676 .get(b"rhg", b"fallback-executable")
676 .map(|x| x.to_owned()),
677 .map(|x| x.to_owned()),
677 },
678 },
678 None => Self::DEFAULT,
679 None => Self::DEFAULT,
679 Some(_) => {
680 Some(_) => {
680 // TODO: warn about unknown config value
681 // TODO: warn about unknown config value
681 Self::DEFAULT
682 Self::DEFAULT
682 }
683 }
683 }
684 }
684 }
685 }
685 }
686 }
686
687
687 /// The `*` extension is an edge-case for config sub-options that apply to all
688 /// The `*` extension is an edge-case for config sub-options that apply to all
688 /// extensions. For now, only `:required` exists, but that may change in the
689 /// extensions. For now, only `:required` exists, but that may change in the
689 /// future.
690 /// future.
690 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[
691 const SUPPORTED_EXTENSIONS: &[&[u8]] = &[
691 b"blackbox",
692 b"blackbox",
692 b"share",
693 b"share",
693 b"sparse",
694 b"sparse",
694 b"narrow",
695 b"narrow",
695 b"*",
696 b"*",
696 b"strip",
697 b"strip",
697 b"rebase",
698 b"rebase",
698 ];
699 ];
699
700
700 fn check_extensions(config: &Config) -> Result<(), CommandError> {
701 fn check_extensions(config: &Config) -> Result<(), CommandError> {
701 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
702 if let Some(b"*") = config.get(b"rhg", b"ignored-extensions") {
702 // All extensions are to be ignored, nothing to do here
703 // All extensions are to be ignored, nothing to do here
703 return Ok(());
704 return Ok(());
704 }
705 }
705
706
706 let enabled: HashSet<&[u8]> = config
707 let enabled: HashSet<&[u8]> = config
707 .iter_section(b"extensions")
708 .iter_section(b"extensions")
708 .filter_map(|(extension, value)| {
709 .filter_map(|(extension, value)| {
709 if value == b"!" {
710 if value == b"!" {
710 // Filter out disabled extensions
711 // Filter out disabled extensions
711 return None;
712 return None;
712 }
713 }
713 // Ignore extension suboptions. Only `required` exists for now.
714 // Ignore extension suboptions. Only `required` exists for now.
714 // `rhg` either supports an extension or doesn't, so it doesn't
715 // `rhg` either supports an extension or doesn't, so it doesn't
715 // make sense to consider the loading of an extension.
716 // make sense to consider the loading of an extension.
716 let actual_extension =
717 let actual_extension =
717 extension.split_2(b':').unwrap_or((extension, b"")).0;
718 extension.split_2(b':').unwrap_or((extension, b"")).0;
718 Some(actual_extension)
719 Some(actual_extension)
719 })
720 })
720 .collect();
721 .collect();
721
722
722 let mut unsupported = enabled;
723 let mut unsupported = enabled;
723 for supported in SUPPORTED_EXTENSIONS {
724 for supported in SUPPORTED_EXTENSIONS {
724 unsupported.remove(supported);
725 unsupported.remove(supported);
725 }
726 }
726
727
727 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
728 if let Some(ignored_list) = config.get_list(b"rhg", b"ignored-extensions")
728 {
729 {
729 for ignored in ignored_list {
730 for ignored in ignored_list {
730 unsupported.remove(ignored.as_slice());
731 unsupported.remove(ignored.as_slice());
731 }
732 }
732 }
733 }
733
734
734 if unsupported.is_empty() {
735 if unsupported.is_empty() {
735 Ok(())
736 Ok(())
736 } else {
737 } else {
737 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
738 let mut unsupported: Vec<_> = unsupported.into_iter().collect();
738 // Sort the extensions to get a stable output
739 // Sort the extensions to get a stable output
739 unsupported.sort();
740 unsupported.sort();
740 Err(CommandError::UnsupportedFeature {
741 Err(CommandError::UnsupportedFeature {
741 message: format_bytes!(
742 message: format_bytes!(
742 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
743 b"extensions: {} (consider adding them to 'rhg.ignored-extensions' config)",
743 join(unsupported, b", ")
744 join(unsupported, b", ")
744 ),
745 ),
745 })
746 })
746 }
747 }
747 }
748 }
748
749
749 /// Array of tuples of (auto upgrade conf, feature conf, local requirement)
750 /// Array of tuples of (auto upgrade conf, feature conf, local requirement)
750 #[allow(clippy::type_complexity)]
751 #[allow(clippy::type_complexity)]
751 const AUTO_UPGRADES: &[((&str, &str), (&str, &str), &str)] = &[
752 const AUTO_UPGRADES: &[((&str, &str), (&str, &str), &str)] = &[
752 (
753 (
753 ("format", "use-share-safe.automatic-upgrade-of-mismatching-repositories"),
754 ("format", "use-share-safe.automatic-upgrade-of-mismatching-repositories"),
754 ("format", "use-share-safe"),
755 ("format", "use-share-safe"),
755 requirements::SHARESAFE_REQUIREMENT,
756 requirements::SHARESAFE_REQUIREMENT,
756 ),
757 ),
757 (
758 (
758 ("format", "use-dirstate-tracked-hint.automatic-upgrade-of-mismatching-repositories"),
759 ("format", "use-dirstate-tracked-hint.automatic-upgrade-of-mismatching-repositories"),
759 ("format", "use-dirstate-tracked-hint"),
760 ("format", "use-dirstate-tracked-hint"),
760 requirements::DIRSTATE_TRACKED_HINT_V1,
761 requirements::DIRSTATE_TRACKED_HINT_V1,
761 ),
762 ),
762 (
763 (
763 ("format", "use-dirstate-v2.automatic-upgrade-of-mismatching-repositories"),
764 ("format", "use-dirstate-v2.automatic-upgrade-of-mismatching-repositories"),
764 ("format", "use-dirstate-v2"),
765 ("format", "use-dirstate-v2"),
765 requirements::DIRSTATE_V2_REQUIREMENT,
766 requirements::DIRSTATE_V2_REQUIREMENT,
766 ),
767 ),
767 ];
768 ];
768
769
769 /// Mercurial allows users to automatically upgrade their repository.
770 /// Mercurial allows users to automatically upgrade their repository.
770 /// `rhg` does not have the ability to upgrade yet, so fallback if an upgrade
771 /// `rhg` does not have the ability to upgrade yet, so fallback if an upgrade
771 /// is needed.
772 /// is needed.
772 fn check_auto_upgrade(
773 fn check_auto_upgrade(
773 config: &Config,
774 config: &Config,
774 reqs: &HashSet<String>,
775 reqs: &HashSet<String>,
775 ) -> Result<(), CommandError> {
776 ) -> Result<(), CommandError> {
776 for (upgrade_conf, feature_conf, local_req) in AUTO_UPGRADES.iter() {
777 for (upgrade_conf, feature_conf, local_req) in AUTO_UPGRADES.iter() {
777 let auto_upgrade = config
778 let auto_upgrade = config
778 .get_bool(upgrade_conf.0.as_bytes(), upgrade_conf.1.as_bytes())?;
779 .get_bool(upgrade_conf.0.as_bytes(), upgrade_conf.1.as_bytes())?;
779
780
780 if auto_upgrade {
781 if auto_upgrade {
781 let want_it = config.get_bool(
782 let want_it = config.get_bool(
782 feature_conf.0.as_bytes(),
783 feature_conf.0.as_bytes(),
783 feature_conf.1.as_bytes(),
784 feature_conf.1.as_bytes(),
784 )?;
785 )?;
785 let have_it = reqs.contains(*local_req);
786 let have_it = reqs.contains(*local_req);
786
787
787 let action = match (want_it, have_it) {
788 let action = match (want_it, have_it) {
788 (true, false) => Some("upgrade"),
789 (true, false) => Some("upgrade"),
789 (false, true) => Some("downgrade"),
790 (false, true) => Some("downgrade"),
790 _ => None,
791 _ => None,
791 };
792 };
792 if let Some(action) = action {
793 if let Some(action) = action {
793 let message = format!(
794 let message = format!(
794 "automatic {} {}.{}",
795 "automatic {} {}.{}",
795 action, upgrade_conf.0, upgrade_conf.1
796 action, upgrade_conf.0, upgrade_conf.1
796 );
797 );
797 return Err(CommandError::unsupported(message));
798 return Err(CommandError::unsupported(message));
798 }
799 }
799 }
800 }
800 }
801 }
801 Ok(())
802 Ok(())
802 }
803 }
803
804
804 fn check_unsupported(
805 fn check_unsupported(
805 config: &Config,
806 config: &Config,
806 repo: Result<&Repo, &NoRepoInCwdError>,
807 repo: Result<&Repo, &NoRepoInCwdError>,
807 ) -> Result<(), CommandError> {
808 ) -> Result<(), CommandError> {
808 check_extensions(config)?;
809 check_extensions(config)?;
809
810
810 if std::env::var_os("HG_PENDING").is_some() {
811 if std::env::var_os("HG_PENDING").is_some() {
811 // TODO: only if the value is `== repo.working_directory`?
812 // TODO: only if the value is `== repo.working_directory`?
812 // What about relative v.s. absolute paths?
813 // What about relative v.s. absolute paths?
813 Err(CommandError::unsupported("$HG_PENDING"))?
814 Err(CommandError::unsupported("$HG_PENDING"))?
814 }
815 }
815
816
816 if let Ok(repo) = repo {
817 if let Ok(repo) = repo {
817 if repo.has_subrepos()? {
818 if repo.has_subrepos()? {
818 Err(CommandError::unsupported("sub-repositories"))?
819 Err(CommandError::unsupported("sub-repositories"))?
819 }
820 }
820 check_auto_upgrade(config, repo.requirements())?;
821 check_auto_upgrade(config, repo.requirements())?;
821 }
822 }
822
823
823 if config.has_non_empty_section(b"encode") {
824 if config.has_non_empty_section(b"encode") {
824 Err(CommandError::unsupported("[encode] config"))?
825 Err(CommandError::unsupported("[encode] config"))?
825 }
826 }
826
827
827 if config.has_non_empty_section(b"decode") {
828 if config.has_non_empty_section(b"decode") {
828 Err(CommandError::unsupported("[decode] config"))?
829 Err(CommandError::unsupported("[decode] config"))?
829 }
830 }
830
831
831 Ok(())
832 Ok(())
832 }
833 }
General Comments 0
You need to be logged in to leave comments. Login now