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