The vendored code doesn't follow modern rustfmt's conventions.
This commit runs rustfmt so it does.
- skip-blame rustfmt
| Alphare | |
| durin42 |
| hg-reviewers |
The vendored code doesn't follow modern rustfmt's conventions.
This commit runs rustfmt so it does.
| No Linters Available |
| No Unit Test Coverage |
We actually ran rustfmt, but it's an older version (rustfmt 1.3.0-stable (d334502 2019-06-09)) and I verified the code still formats under that particular version of rustfmt with empty rustfmt.toml. In other words, rustfmt itself seems to change over time.
| Path | Packages | |||
|---|---|---|---|---|
| M | rust/hg-core/src/configparser/c_api.rs (21 lines) | |||
| M | rust/hg-core/src/configparser/config.rs (357 lines) | |||
| M | rust/hg-core/src/configparser/hg.rs (233 lines) | |||
| M | rust/hg-core/src/configparser/lib.rs (4 lines) |
| Commit | Parents | Author | Summary | Date |
|---|---|---|---|---|
| e8ec811bf019 | 1eda551870ad | Gregory Szorc | Dec 7 2019, 10:01 PM |
| /* | /* | ||||
| * Copyright (c) Facebook, Inc. and its affiliates. | * Copyright (c) Facebook, Inc. and its affiliates. | ||||
| * | * | ||||
| * This software may be used and distributed according to the terms of the | * This software may be used and distributed according to the terms of the | ||||
| * GNU General Public License version 2. | * GNU General Public License version 2. | ||||
| */ | */ | ||||
| //! This module exports some symbols to allow calling the config parser from C/C++ | //! This module exports some symbols to allow calling the config parser from | ||||
| //! C/C++ | |||||
| use std::ffi::{CStr, OsStr}; | use std::ffi::{CStr, OsStr}; | ||||
| use std::os::raw::c_char; | use std::os::raw::c_char; | ||||
| use std::path::Path; | use std::path::Path; | ||||
| use std::ptr; | use std::ptr; | ||||
| use std::slice; | use std::slice; | ||||
| use bytes::Bytes; | use bytes::Bytes; | ||||
| } | } | ||||
| /// Attempt to load and parse the config file at the specified path. | /// Attempt to load and parse the config file at the specified path. | ||||
| /// If successful, returns a nullptr. | /// If successful, returns a nullptr. | ||||
| /// Returns a Bytes object containing the error reason on failure; the | /// Returns a Bytes object containing the error reason on failure; the | ||||
| /// error object is UTF-8 encoded text, and errors can span multiple lines. | /// error object is UTF-8 encoded text, and errors can span multiple lines. | ||||
| #[cfg(unix)] | #[cfg(unix)] | ||||
| #[no_mangle] | #[no_mangle] | ||||
| pub extern "C" fn hgrc_configset_load_path(cfg: *mut ConfigSet, path: *const c_char) -> *mut Bytes { | pub extern "C" fn hgrc_configset_load_path( | ||||
| cfg: *mut ConfigSet, | |||||
| path: *const c_char, | |||||
| ) -> *mut Bytes { | |||||
| debug_assert!(!path.is_null()); | debug_assert!(!path.is_null()); | ||||
| debug_assert!(!cfg.is_null()); | debug_assert!(!cfg.is_null()); | ||||
| use std::os::unix::ffi::OsStrExt; | use std::os::unix::ffi::OsStrExt; | ||||
| let path_cstr = unsafe { CStr::from_ptr(path) }; | let path_cstr = unsafe { CStr::from_ptr(path) }; | ||||
| let path_bytes = path_cstr.to_bytes(); | let path_bytes = path_cstr.to_bytes(); | ||||
| let path = Path::new(OsStr::from_bytes(&path_bytes)); | let path = Path::new(OsStr::from_bytes(&path_bytes)); | ||||
| let cfg = unsafe { &mut *cfg }; | let cfg = unsafe { &mut *cfg }; | ||||
| load_path(cfg, path) | load_path(cfg, path) | ||||
| } | } | ||||
| /// Load system config files | /// Load system config files | ||||
| #[no_mangle] | #[no_mangle] | ||||
| pub extern "C" fn hgrc_configset_load_system(cfg: *mut ConfigSet) -> *mut Bytes { | pub extern "C" fn hgrc_configset_load_system( | ||||
| cfg: *mut ConfigSet, | |||||
| ) -> *mut Bytes { | |||||
| debug_assert!(!cfg.is_null()); | debug_assert!(!cfg.is_null()); | ||||
| let cfg = unsafe { &mut *cfg }; | let cfg = unsafe { &mut *cfg }; | ||||
| // Forces datapath to be the empty string as it doesn't | // Forces datapath to be the empty string as it doesn't | ||||
| // appear to play a useful role in simply resolving config | // appear to play a useful role in simply resolving config | ||||
| // settings for Eden. | // settings for Eden. | ||||
| errors_to_bytes(cfg.load_system()) | errors_to_bytes(cfg.load_system()) | ||||
| } | } | ||||
| /// Load user config files | /// Load user config files | ||||
| #[no_mangle] | #[no_mangle] | ||||
| pub extern "C" fn hgrc_configset_load_user(cfg: *mut ConfigSet) -> *mut Bytes { | pub extern "C" fn hgrc_configset_load_user(cfg: *mut ConfigSet) -> *mut Bytes { | ||||
| debug_assert!(!cfg.is_null()); | debug_assert!(!cfg.is_null()); | ||||
| let cfg = unsafe { &mut *cfg }; | let cfg = unsafe { &mut *cfg }; | ||||
| errors_to_bytes(cfg.load_user()) | errors_to_bytes(cfg.load_user()) | ||||
| } | } | ||||
| /// Returns a Bytes object holding the configuration value for the corresponding | /// Returns a Bytes object holding the configuration value for the | ||||
| /// section name and key. If there is no matching section/key pair, returns nullptr. | /// corresponding section name and key. If there is no matching section/key | ||||
| /// pair, returns nullptr. | |||||
| #[no_mangle] | #[no_mangle] | ||||
| pub extern "C" fn hgrc_configset_get( | pub extern "C" fn hgrc_configset_get( | ||||
| cfg: *const ConfigSet, | cfg: *const ConfigSet, | ||||
| section: *const u8, | section: *const u8, | ||||
| section_len: usize, | section_len: usize, | ||||
| name: *const u8, | name: *const u8, | ||||
| name_len: usize, | name_len: usize, | ||||
| ) -> *mut Bytes { | ) -> *mut Bytes { | ||||
| } | } | ||||
| #[repr(C)] | #[repr(C)] | ||||
| pub struct ByteData { | pub struct ByteData { | ||||
| ptr: *const u8, | ptr: *const u8, | ||||
| len: usize, | len: usize, | ||||
| } | } | ||||
| /// Returns the data pointer and length for a Bytes object, suitable for constructing | /// Returns the data pointer and length for a Bytes object, suitable for | ||||
| /// a folly::ByteRange. | /// constructing a folly::ByteRange. | ||||
| #[no_mangle] | #[no_mangle] | ||||
| pub extern "C" fn hgrc_bytes_data(bytes: *const Bytes) -> ByteData { | pub extern "C" fn hgrc_bytes_data(bytes: *const Bytes) -> ByteData { | ||||
| debug_assert!(!bytes.is_null()); | debug_assert!(!bytes.is_null()); | ||||
| let bytes = unsafe { &*bytes }; | let bytes = unsafe { &*bytes }; | ||||
| ByteData { | ByteData { | ||||
| ptr: bytes.as_ptr(), | ptr: bytes.as_ptr(), | ||||
| len: bytes.len(), | len: bytes.len(), | ||||
| } | } | ||||
| struct Section { | struct Section { | ||||
| items: IndexMap<Bytes, Vec<ValueSource>>, | items: IndexMap<Bytes, Vec<ValueSource>>, | ||||
| } | } | ||||
| /// A config value with associated metadata like where it comes from. | /// A config value with associated metadata like where it comes from. | ||||
| #[derive(Clone, Debug)] | #[derive(Clone, Debug)] | ||||
| pub struct ValueSource { | pub struct ValueSource { | ||||
| value: Option<Bytes>, | value: Option<Bytes>, | ||||
| source: Bytes, // global, user, repo, "--config", or an extension name, etc. | source: Bytes, /* global, user, repo, "--config", or an extension name, | ||||
| * etc. */ | |||||
| location: Option<ValueLocation>, | location: Option<ValueLocation>, | ||||
| } | } | ||||
| /// The on-disk file name and byte offsets that provide the config value. | /// The on-disk file name and byte offsets that provide the config value. | ||||
| /// Useful if applications want to edit config values in-place. | /// Useful if applications want to edit config values in-place. | ||||
| #[derive(Clone, Debug)] | #[derive(Clone, Debug)] | ||||
| struct ValueLocation { | struct ValueLocation { | ||||
| path: Arc<PathBuf>, | path: Arc<PathBuf>, | ||||
| content: Bytes, | content: Bytes, | ||||
| location: Range<usize>, | location: Range<usize>, | ||||
| } | } | ||||
| /// Options that affects config setting functions like `load_path`, `parse`, | /// Options that affects config setting functions like `load_path`, `parse`, | ||||
| /// and `set`. | /// and `set`. | ||||
| #[derive(Default)] | #[derive(Default)] | ||||
| pub struct Options { | pub struct Options { | ||||
| source: Bytes, | source: Bytes, | ||||
| filters: Vec<Box<dyn Fn(Bytes, Bytes, Option<Bytes>) -> Option<(Bytes, Bytes, Option<Bytes>)>>>, | filters: Vec< | ||||
| Box< | |||||
| dyn Fn( | |||||
| Bytes, | |||||
| Bytes, | |||||
| Option<Bytes>, | |||||
| ) -> Option<(Bytes, Bytes, Option<Bytes>)>, | |||||
| >, | |||||
| >, | |||||
| } | } | ||||
| impl ConfigSet { | impl ConfigSet { | ||||
| /// Return an empty `ConfigSet`. | /// Return an empty `ConfigSet`. | ||||
| pub fn new() -> Self { | pub fn new() -> Self { | ||||
| Default::default() | Default::default() | ||||
| } | } | ||||
| /// Load config files at given path. The path is a file. | /// Load config files at given path. The path is a file. | ||||
| /// | /// | ||||
| /// If `path` is a directory, it is ignored. | /// If `path` is a directory, it is ignored. | ||||
| /// If `path` is a file, it will be loaded directly. | /// If `path` is a file, it will be loaded directly. | ||||
| /// | /// | ||||
| /// A config file can use `%include` to load other paths (directories or files). They will | /// A config file can use `%include` to load other paths (directories or | ||||
| /// be loaded recursively. Includes take effect in place, instead of deferred. For example, | /// files). They will be loaded recursively. Includes take effect in | ||||
| /// with the following two files: | /// place, instead of deferred. For example, with the following two | ||||
| /// files: | |||||
| /// | /// | ||||
| /// ```plain,ignore | /// ```plain,ignore | ||||
| /// # This is 1.rc | /// # This is 1.rc | ||||
| /// [section] | /// [section] | ||||
| /// x = 1 | /// x = 1 | ||||
| /// %include 2.rc | /// %include 2.rc | ||||
| /// y = 2 | /// y = 2 | ||||
| /// | /// | ||||
| /// # This is 2.rc | /// # This is 2.rc | ||||
| /// [section] | /// [section] | ||||
| /// x = 3 | /// x = 3 | ||||
| /// y = 4 | /// y = 4 | ||||
| /// ``` | /// ``` | ||||
| /// | /// | ||||
| /// After loading `1.rc`. `x` is set to 3 and `y` is set to 2. | /// After loading `1.rc`. `x` is set to 3 and `y` is set to 2. | ||||
| /// | /// | ||||
| /// Loading a file that is already parsed or being parsed by this `load_path` call is ignored, | /// Loading a file that is already parsed or being parsed by this | ||||
| /// to avoid infinite loop. A separate `load_path` call would not ignore files loaded by | /// `load_path` call is ignored, to avoid infinite loop. A separate | ||||
| /// other `load_path` calls. | /// `load_path` call would not ignore files loaded by other `load_path` | ||||
| /// | /// calls. | ||||
| /// Return a list of errors. An error pasing a file will stop that file from loading, without | /// | ||||
| /// affecting other files. | /// Return a list of errors. An error pasing a file will stop that file | ||||
| pub fn load_path<P: AsRef<Path>>(&mut self, path: P, opts: &Options) -> Vec<Error> { | /// from loading, without affecting other files. | ||||
| pub fn load_path<P: AsRef<Path>>( | |||||
| &mut self, | |||||
| path: P, | |||||
| opts: &Options, | |||||
| ) -> Vec<Error> { | |||||
| let mut visited = HashSet::new(); | let mut visited = HashSet::new(); | ||||
| let mut errors = Vec::new(); | let mut errors = Vec::new(); | ||||
| self.load_file(path.as_ref(), opts, &mut visited, &mut errors); | self.load_file(path.as_ref(), opts, &mut visited, &mut errors); | ||||
| errors | errors | ||||
| } | } | ||||
| /// Load content of an unnamed config file. The `ValueLocation`s of loaded config items will | /// Load content of an unnamed config file. The `ValueLocation`s of loaded | ||||
| /// have an empty `path`. | /// config items will have an empty `path`. | ||||
| /// | /// | ||||
| /// Return a list of errors. | /// Return a list of errors. | ||||
| pub fn parse<B: Into<Bytes>>(&mut self, content: B, opts: &Options) -> Vec<Error> { | pub fn parse<B: Into<Bytes>>( | ||||
| &mut self, | |||||
| content: B, | |||||
| opts: &Options, | |||||
| ) -> Vec<Error> { | |||||
| let mut visited = HashSet::new(); | let mut visited = HashSet::new(); | ||||
| let mut errors = Vec::new(); | let mut errors = Vec::new(); | ||||
| let buf = content.into(); | let buf = content.into(); | ||||
| self.load_file_content(Path::new(""), buf, opts, &mut visited, &mut errors); | self.load_file_content( | ||||
| Path::new(""), | |||||
| buf, | |||||
| opts, | |||||
| &mut visited, | |||||
| &mut errors, | |||||
| ); | |||||
| errors | errors | ||||
| } | } | ||||
| /// Get config sections. | /// Get config sections. | ||||
| pub fn sections(&self) -> Vec<Bytes> { | pub fn sections(&self) -> Vec<Bytes> { | ||||
| self.sections.keys().cloned().collect() | self.sections.keys().cloned().collect() | ||||
| } | } | ||||
| /// Get config names in the given section. Sorted by insertion order. | /// Get config names in the given section. Sorted by insertion order. | ||||
| pub fn keys<S: Into<Bytes>>(&self, section: S) -> Vec<Bytes> { | pub fn keys<S: Into<Bytes>>(&self, section: S) -> Vec<Bytes> { | ||||
| self.sections | self.sections | ||||
| .get(§ion.into()) | .get(§ion.into()) | ||||
| .map(|section| section.items.keys().cloned().collect()) | .map(|section| section.items.keys().cloned().collect()) | ||||
| .unwrap_or(Vec::new()) | .unwrap_or(Vec::new()) | ||||
| } | } | ||||
| /// Get config value for a given config. | /// Get config value for a given config. | ||||
| /// Return `None` if the config item does not exist or is unset. | /// Return `None` if the config item does not exist or is unset. | ||||
| pub fn get<S: Into<Bytes>, N: Into<Bytes>>(&self, section: S, name: N) -> Option<Bytes> { | pub fn get<S: Into<Bytes>, N: Into<Bytes>>( | ||||
| &self, | |||||
| section: S, | |||||
| name: N, | |||||
| ) -> Option<Bytes> { | |||||
| self.sections.get(§ion.into()).and_then(|section| { | self.sections.get(§ion.into()).and_then(|section| { | ||||
| section | section.items.get(&name.into()).and_then(|values| { | ||||
| .items | values.last().and_then(|value| value.value.clone()) | ||||
| .get(&name.into()) | }) | ||||
| .and_then(|values| values.last().and_then(|value| value.value.clone())) | |||||
| }) | }) | ||||
| } | } | ||||
| /// Get detailed sources of a given config, including overrides, and source information. | /// Get detailed sources of a given config, including overrides, and source | ||||
| /// The last item in the returned vector is the latest value that is considered effective. | /// information. The last item in the returned vector is the latest | ||||
| /// value that is considered effective. | |||||
| /// | /// | ||||
| /// Return an emtpy vector if the config does not exist. | /// Return an emtpy vector if the config does not exist. | ||||
| pub fn get_sources<S: Into<Bytes>, N: Into<Bytes>>( | pub fn get_sources<S: Into<Bytes>, N: Into<Bytes>>( | ||||
| &self, | &self, | ||||
| section: S, | section: S, | ||||
| name: N, | name: N, | ||||
| ) -> Vec<ValueSource> { | ) -> Vec<ValueSource> { | ||||
| self.sections | self.sections | ||||
| .get(§ion.into()) | .get(§ion.into()) | ||||
| .and_then(|section| section.items.get(&name.into()).map(|values| values.clone())) | .and_then(|section| { | ||||
| section.items.get(&name.into()).map(|values| values.clone()) | |||||
| }) | |||||
| .unwrap_or(Vec::new()) | .unwrap_or(Vec::new()) | ||||
| } | } | ||||
| /// Set a config item directly. `section`, `name` locates the config. `value` is the new value. | /// Set a config item directly. `section`, `name` locates the config. | ||||
| /// `source` is some annotation about who set it, ex. "reporc", "userrc", "--config", etc. | /// `value` is the new value. `source` is some annotation about who set | ||||
| /// it, ex. "reporc", "userrc", "--config", etc. | |||||
| pub fn set<T: Into<Bytes>, N: Into<Bytes>>( | pub fn set<T: Into<Bytes>, N: Into<Bytes>>( | ||||
| &mut self, | &mut self, | ||||
| section: T, | section: T, | ||||
| name: N, | name: N, | ||||
| value: Option<&[u8]>, | value: Option<&[u8]>, | ||||
| opts: &Options, | opts: &Options, | ||||
| ) { | ) { | ||||
| let section = section.into(); | let section = section.into(); | ||||
| let name = name.into(); | let name = name.into(); | ||||
| let value = value.map(|v| Bytes::from(v)); | let value = value.map(|v| Bytes::from(v)); | ||||
| self.set_internal(section, name, value, None, &opts) | self.set_internal(section, name, value, None, &opts) | ||||
| } | } | ||||
| fn set_internal( | fn set_internal( | ||||
| &mut self, | &mut self, | ||||
| section: Bytes, | section: Bytes, | ||||
| name: Bytes, | name: Bytes, | ||||
| value: Option<Bytes>, | value: Option<Bytes>, | ||||
| location: Option<ValueLocation>, | location: Option<ValueLocation>, | ||||
| opts: &Options, | opts: &Options, | ||||
| ) { | ) { | ||||
| let filtered = opts | let filtered = opts.filters.iter().fold( | ||||
| .filters | Some((section, name, value)), | ||||
| .iter() | move |acc, func| { | ||||
| .fold(Some((section, name, value)), move |acc, func| { | acc.and_then(|(section, name, value)| { | ||||
| acc.and_then(|(section, name, value)| func(section, name, value)) | func(section, name, value) | ||||
| }); | }) | ||||
| }, | |||||
| ); | |||||
| if let Some((section, name, value)) = filtered { | if let Some((section, name, value)) = filtered { | ||||
| self.sections | self.sections | ||||
| .entry(section) | .entry(section) | ||||
| .or_insert_with(|| Default::default()) | .or_insert_with(|| Default::default()) | ||||
| .items | .items | ||||
| .entry(name) | .entry(name) | ||||
| .or_insert_with(|| Vec::with_capacity(1)) | .or_insert_with(|| Vec::with_capacity(1)) | ||||
| .push(ValueSource { | .push(ValueSource { | ||||
| errors.push(Error::Io(path.to_path_buf(), error)); | errors.push(Error::Io(path.to_path_buf(), error)); | ||||
| return; | return; | ||||
| } | } | ||||
| buf.push(b'\n'); | buf.push(b'\n'); | ||||
| let buf = Bytes::from(buf); | let buf = Bytes::from(buf); | ||||
| self.load_file_content(path, buf, opts, visited, errors); | self.load_file_content(path, buf, opts, visited, errors); | ||||
| } | } | ||||
| Err(error) => errors.push(Error::Io(path.to_path_buf(), error)), | Err(error) => { | ||||
| errors.push(Error::Io(path.to_path_buf(), error)) | |||||
| } | |||||
| } | } | ||||
| } else { | } else { | ||||
| // On Windows, a UNC path `\\?\C:\foo\.\x` will fail to canonicalize | // On Windows, a UNC path `\\?\C:\foo\.\x` will fail to | ||||
| // because it contains `.`. That path can be constructed by using | // canonicalize because it contains `.`. That path can | ||||
| // `PathBuf::join` to concatenate a UNC path `\\?\C:\foo` with | // be constructed by using `PathBuf::join` to | ||||
| // a "normal" path `.\x`. | // concatenate a UNC path `\\?\C:\foo` with a "normal" | ||||
| // Try to fix it automatically by stripping the UNC prefix and retry | // path `.\x`. Try to fix it automatically by stripping | ||||
| // `canonicalize`. `C:\foo\.\x` would be canonicalized without errors. | // the UNC prefix and retry `canonicalize`. | ||||
| // `C:\foo\.\x` would be canonicalized without errors. | |||||
| #[cfg(windows)] | #[cfg(windows)] | ||||
| { | { | ||||
| if let Some(path_str) = path.to_str() { | if let Some(path_str) = path.to_str() { | ||||
| if path_str.starts_with(r"\\?\") { | if path_str.starts_with(r"\\?\") { | ||||
| let path = Path::new(&path_str[4..]); | let path = Path::new(&path_str[4..]); | ||||
| self.load_file(&path, opts, visited, errors); | self.load_file(&path, opts, visited, errors); | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| // If `path.canonicalize` reports an error. It's usually the path cannot | // If `path.canonicalize` reports an error. It's usually the path | ||||
| // be resolved (ex. does not exist). It is considered normal and is not | // cannot be resolved (ex. does not exist). It is considered | ||||
| // reported in `errors`. | // normal and is not reported in `errors`. | ||||
| } | } | ||||
| fn load_file_content( | fn load_file_content( | ||||
| &mut self, | &mut self, | ||||
| path: &Path, | path: &Path, | ||||
| buf: Bytes, | buf: Bytes, | ||||
| opts: &Options, | opts: &Options, | ||||
| visited: &mut HashSet<PathBuf>, | visited: &mut HashSet<PathBuf>, | ||||
| errors: &mut Vec<Error>, | errors: &mut Vec<Error>, | ||||
| ) { | ) { | ||||
| let mut section = Bytes::new(); | let mut section = Bytes::new(); | ||||
| let shared_path = Arc::new(path.to_path_buf()); // use Arc to do shallow copy | let shared_path = Arc::new(path.to_path_buf()); // use Arc to do shallow copy | ||||
| let skip_include = path.parent().is_none(); // skip handling %include if path is empty | let skip_include = path.parent().is_none(); // skip handling %include if path is empty | ||||
| // Utilities to avoid too much indentation. | // Utilities to avoid too much indentation. | ||||
| let handle_value = |this: &mut ConfigSet, | let handle_value = | ||||
| |this: &mut ConfigSet, | |||||
| pair: Pair, | pair: Pair, | ||||
| section: Bytes, | section: Bytes, | ||||
| name: Bytes, | name: Bytes, | ||||
| location: ValueLocation| { | location: ValueLocation| { | ||||
| let pairs = pair.into_inner(); | let pairs = pair.into_inner(); | ||||
| let mut lines = Vec::with_capacity(1); | let mut lines = Vec::with_capacity(1); | ||||
| for pair in pairs { | for pair in pairs { | ||||
| if Rule::line == pair.as_rule() { | if Rule::line == pair.as_rule() { | ||||
| lines.push(extract(&buf, pair.as_span())); | lines.push(extract(&buf, pair.as_span())); | ||||
| } | } | ||||
| } | } | ||||
| let value = match lines.len() { | let value = match lines.len() { | ||||
| 1 => lines[0].clone(), | 1 => lines[0].clone(), | ||||
| _ => Bytes::from(lines.join(&b'\n')), | _ => Bytes::from(lines.join(&b'\n')), | ||||
| }; | }; | ||||
| let (start, end) = strip_offsets(&value, 0, value.len()); | let (start, end) = strip_offsets(&value, 0, value.len()); | ||||
| let value = value.slice(start, end); | let value = value.slice(start, end); | ||||
| this.set_internal(section, name, value.into(), location.into(), opts) | this.set_internal( | ||||
| section, | |||||
| name, | |||||
| value.into(), | |||||
| location.into(), | |||||
| opts, | |||||
| ) | |||||
| }; | }; | ||||
| let handle_config_item = |this: &mut ConfigSet, pair: Pair, section: Bytes| { | let handle_config_item = | ||||
| |this: &mut ConfigSet, pair: Pair, section: Bytes| { | |||||
| let pairs = pair.into_inner(); | let pairs = pair.into_inner(); | ||||
| let mut name = Bytes::new(); | let mut name = Bytes::new(); | ||||
| for pair in pairs { | for pair in pairs { | ||||
| match pair.as_rule() { | match pair.as_rule() { | ||||
| Rule::config_name => name = extract(&buf, pair.as_span()), | Rule::config_name => { | ||||
| name = extract(&buf, pair.as_span()) | |||||
| } | |||||
| Rule::value => { | Rule::value => { | ||||
| let span = pair.as_span(); | let span = pair.as_span(); | ||||
| let location = ValueLocation { | let location = ValueLocation { | ||||
| path: shared_path.clone(), | path: shared_path.clone(), | ||||
| content: buf.clone(), | content: buf.clone(), | ||||
| location: span.start()..span.end(), | location: span.start()..span.end(), | ||||
| }; | }; | ||||
| return handle_value(this, pair, section, name, location); | return handle_value( | ||||
| this, pair, section, name, location, | |||||
| ); | |||||
| } | } | ||||
| _ => (), | _ => (), | ||||
| } | } | ||||
| } | } | ||||
| unreachable!(); | unreachable!(); | ||||
| }; | }; | ||||
| let handle_section = |pair: Pair, section: &mut Bytes| { | let handle_section = |pair: Pair, section: &mut Bytes| { | ||||
| let pairs = pair.into_inner(); | let pairs = pair.into_inner(); | ||||
| for pair in pairs { | for pair in pairs { | ||||
| match pair.as_rule() { | match pair.as_rule() { | ||||
| Rule::section_name => { | Rule::section_name => { | ||||
| *section = extract(&buf, pair.as_span()); | *section = extract(&buf, pair.as_span()); | ||||
| return; | return; | ||||
| } | } | ||||
| _ => (), | _ => (), | ||||
| } | } | ||||
| } | } | ||||
| unreachable!(); | unreachable!(); | ||||
| }; | }; | ||||
| let mut handle_include = |this: &mut ConfigSet, pair: Pair, errors: &mut Vec<Error>| { | let mut handle_include = | ||||
| |this: &mut ConfigSet, pair: Pair, errors: &mut Vec<Error>| { | |||||
| let pairs = pair.into_inner(); | let pairs = pair.into_inner(); | ||||
| for pair in pairs { | for pair in pairs { | ||||
| match pair.as_rule() { | match pair.as_rule() { | ||||
| Rule::line => { | Rule::line => { | ||||
| if !skip_include { | if !skip_include { | ||||
| let include_path = pair.as_str(); | let include_path = pair.as_str(); | ||||
| let full_include_path = | let full_include_path = path | ||||
| path.parent().unwrap().join(expand_path(include_path)); | .parent() | ||||
| this.load_file(&full_include_path, opts, visited, errors); | .unwrap() | ||||
| .join(expand_path(include_path)); | |||||
| this.load_file( | |||||
| &full_include_path, | |||||
| opts, | |||||
| visited, | |||||
| errors, | |||||
| ); | |||||
| } | } | ||||
| } | } | ||||
| _ => (), | _ => (), | ||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| let handle_unset = |this: &mut ConfigSet, pair: Pair, section: &Bytes| { | let handle_unset = | ||||
| |this: &mut ConfigSet, pair: Pair, section: &Bytes| { | |||||
| let unset_span = pair.as_span(); | let unset_span = pair.as_span(); | ||||
| let pairs = pair.into_inner(); | let pairs = pair.into_inner(); | ||||
| for pair in pairs { | for pair in pairs { | ||||
| match pair.as_rule() { | match pair.as_rule() { | ||||
| Rule::config_name => { | Rule::config_name => { | ||||
| let name = extract(&buf, pair.as_span()); | let name = extract(&buf, pair.as_span()); | ||||
| let location = ValueLocation { | let location = ValueLocation { | ||||
| path: shared_path.clone(), | path: shared_path.clone(), | ||||
| content: buf.clone(), | content: buf.clone(), | ||||
| location: unset_span.start()..unset_span.end(), | location: unset_span.start()..unset_span.end(), | ||||
| }; | }; | ||||
| return this.set_internal( | return this.set_internal( | ||||
| section.clone(), | section.clone(), | ||||
| name, | name, | ||||
| None, | None, | ||||
| location.into(), | location.into(), | ||||
| opts, | opts, | ||||
| ); | ); | ||||
| } | } | ||||
| _ => (), | _ => (), | ||||
| } | } | ||||
| } | } | ||||
| unreachable!(); | unreachable!(); | ||||
| }; | }; | ||||
| let mut handle_directive = | let mut handle_directive = | ||||
| |this: &mut ConfigSet, pair: Pair, section: &Bytes, errors: &mut Vec<Error>| { | |this: &mut ConfigSet, | ||||
| pair: Pair, | |||||
| section: &Bytes, | |||||
| errors: &mut Vec<Error>| { | |||||
| let pairs = pair.into_inner(); | let pairs = pair.into_inner(); | ||||
| for pair in pairs { | for pair in pairs { | ||||
| match pair.as_rule() { | match pair.as_rule() { | ||||
| Rule::include => handle_include(this, pair, errors), | Rule::include => handle_include(this, pair, errors), | ||||
| Rule::unset => handle_unset(this, pair, section), | Rule::unset => handle_unset(this, pair, section), | ||||
| _ => (), | _ => (), | ||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| let text = match str::from_utf8(&buf) { | let text = match str::from_utf8(&buf) { | ||||
| Ok(text) => text, | Ok(text) => text, | ||||
| Err(error) => return errors.push(Error::Utf8(path.to_path_buf(), error)), | Err(error) => { | ||||
| return errors.push(Error::Utf8(path.to_path_buf(), error)) | |||||
| } | |||||
| }; | }; | ||||
| let pairs = match ConfigParser::parse(Rule::file, &text) { | let pairs = match ConfigParser::parse(Rule::file, &text) { | ||||
| Ok(pairs) => pairs, | Ok(pairs) => pairs, | ||||
| Err(error) => { | Err(error) => { | ||||
| return errors.push(Error::Parse(path.to_path_buf(), format!("{}", error))); | return errors.push(Error::Parse( | ||||
| path.to_path_buf(), | |||||
| format!("{}", error), | |||||
| )); | |||||
| } | } | ||||
| }; | }; | ||||
| for pair in pairs { | for pair in pairs { | ||||
| match pair.as_rule() { | match pair.as_rule() { | ||||
| Rule::config_item => handle_config_item(self, pair, section.clone()), | Rule::config_item => { | ||||
| handle_config_item(self, pair, section.clone()) | |||||
| } | |||||
| Rule::section => handle_section(pair, &mut section), | Rule::section => handle_section(pair, &mut section), | ||||
| Rule::directive => handle_directive(self, pair, §ion, errors), | Rule::directive => { | ||||
| Rule::blank_line | Rule::comment_line | Rule::new_line | Rule::EOI => (), | handle_directive(self, pair, §ion, errors) | ||||
| } | |||||
| Rule::blank_line | |||||
| | Rule::comment_line | |||||
| | Rule::new_line | |||||
| | Rule::EOI => (), | |||||
| Rule::comment_start | Rule::comment_start | ||||
| | Rule::compound | | Rule::compound | ||||
| | Rule::config_name | | Rule::config_name | ||||
| | Rule::equal_sign | | Rule::equal_sign | ||||
| | Rule::file | | Rule::file | ||||
| | Rule::include | | Rule::include | ||||
| | Rule::left_bracket | | Rule::left_bracket | ||||
| } | } | ||||
| impl ValueSource { | impl ValueSource { | ||||
| /// Return the actual value stored in this config value, or `None` if uset. | /// Return the actual value stored in this config value, or `None` if uset. | ||||
| pub fn value(&self) -> &Option<Bytes> { | pub fn value(&self) -> &Option<Bytes> { | ||||
| &self.value | &self.value | ||||
| } | } | ||||
| /// Return the "source" information for the config value. It's usually who sets the config, | /// Return the "source" information for the config value. It's usually who | ||||
| /// like "--config", "user_hgrc", "system_hgrc", etc. | /// sets the config, like "--config", "user_hgrc", "system_hgrc", etc. | ||||
| pub fn source(&self) -> &Bytes { | pub fn source(&self) -> &Bytes { | ||||
| &self.source | &self.source | ||||
| } | } | ||||
| /// Return the file path and byte range for the exact config value, | /// Return the file path and byte range for the exact config value, | ||||
| /// or `None` if there is no such information. | /// or `None` if there is no such information. | ||||
| /// | /// | ||||
| /// If the value is `None`, the byte range is for the "%unset" statement. | /// If the value is `None`, the byte range is for the "%unset" statement. | ||||
| pub fn location(&self) -> Option<(PathBuf, Range<usize>)> { | pub fn location(&self) -> Option<(PathBuf, Range<usize>)> { | ||||
| match self.location { | match self.location { | ||||
| Some(ref src) => Some((src.path.as_ref().to_path_buf(), src.location.clone())), | Some(ref src) => { | ||||
| Some((src.path.as_ref().to_path_buf(), src.location.clone())) | |||||
| } | |||||
| None => None, | None => None, | ||||
| } | } | ||||
| } | } | ||||
| /// Return the file content. Or `None` if there is no such information. | /// Return the file content. Or `None` if there is no such information. | ||||
| pub fn file_content(&self) -> Option<Bytes> { | pub fn file_content(&self) -> Option<Bytes> { | ||||
| match self.location { | match self.location { | ||||
| Some(ref src) => Some(src.content.clone()), | Some(ref src) => Some(src.content.clone()), | ||||
| None => None, | None => None, | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| impl Options { | impl Options { | ||||
| /// Create a default `Options`. | /// Create a default `Options`. | ||||
| pub fn new() -> Self { | pub fn new() -> Self { | ||||
| Self::default() | Self::default() | ||||
| } | } | ||||
| /// Append a filter. A filter can decide to ignore a config item, or change its section, | /// Append a filter. A filter can decide to ignore a config item, or change | ||||
| /// config name, or even value. The filter function takes a tuple of `(section, name, value)` | /// its section, config name, or even value. The filter function takes | ||||
| /// and outputs `None` to prevent inserting that value, or `Some((section, name, value))` to | /// a tuple of `(section, name, value)` and outputs `None` to prevent | ||||
| /// inserting that value, or `Some((section, name, value))` to | |||||
| /// insert it with optionally different name or values. | /// insert it with optionally different name or values. | ||||
| /// | /// | ||||
| /// Filters inserted first will be executed first. | /// Filters inserted first will be executed first. | ||||
| pub fn append_filter( | pub fn append_filter( | ||||
| mut self, | mut self, | ||||
| filter: Box<dyn Fn(Bytes, Bytes, Option<Bytes>) -> Option<(Bytes, Bytes, Option<Bytes>)>>, | filter: Box< | ||||
| dyn Fn( | |||||
| Bytes, | |||||
| Bytes, | |||||
| Option<Bytes>, | |||||
| ) -> Option<(Bytes, Bytes, Option<Bytes>)>, | |||||
| >, | |||||
| ) -> Self { | ) -> Self { | ||||
| self.filters.push(filter); | self.filters.push(filter); | ||||
| self | self | ||||
| } | } | ||||
| /// Set `source` information. It is about who initialized the config loading. For example, | /// Set `source` information. It is about who initialized the config | ||||
| /// "user_hgrc" indicates it is from the user config file, "--config" indicates it is from the | /// loading. For example, "user_hgrc" indicates it is from the user | ||||
| /// global "--config" command line flag, "env" indicates it is translated from an environment | /// config file, "--config" indicates it is from the global "--config" | ||||
| /// command line flag, "env" indicates it is translated from an environment | |||||
| /// variable (ex. "PAGER"), etc. | /// variable (ex. "PAGER"), etc. | ||||
| pub fn source<B: Into<Bytes>>(mut self, source: B) -> Self { | pub fn source<B: Into<Bytes>>(mut self, source: B) -> Self { | ||||
| self.source = source.into(); | self.source = source.into(); | ||||
| self | self | ||||
| } | } | ||||
| } | } | ||||
| /// Convert a "source" string to an `Options`. | /// Convert a "source" string to an `Options`. | ||||
| impl<S: Into<Bytes>> From<S> for Options { | impl<S: Into<Bytes>> From<S> for Options { | ||||
| fn from(source: S) -> Options { | fn from(source: S) -> Options { | ||||
| Options::new().source(source.into()) | Options::new().source(source.into()) | ||||
| } | } | ||||
| } | } | ||||
| /// Remove space characters from both ends. Remove newline characters from the end. | /// Remove space characters from both ends. Remove newline characters from the | ||||
| /// `start` position is inclusive, `end` is exclusive. | /// end. `start` position is inclusive, `end` is exclusive. | ||||
| /// Return the stripped `start` and `end` offsets. | /// Return the stripped `start` and `end` offsets. | ||||
| #[inline] | #[inline] | ||||
| fn strip_offsets(buf: &Bytes, start: usize, end: usize) -> (usize, usize) { | fn strip_offsets(buf: &Bytes, start: usize, end: usize) -> (usize, usize) { | ||||
| let mut start = start; | let mut start = start; | ||||
| let mut end = end; | let mut end = end; | ||||
| while start < end && b" \t".contains(&buf[start]) { | while start < end && b" \t".contains(&buf[start]) { | ||||
| start += 1 | start += 1 | ||||
| } | } | ||||
| // Will be loaded. Shouldn't cause cycles. | // Will be loaded. Shouldn't cause cycles. | ||||
| write_file(dir.path().join("e.rc"), "[x]\ne=e\n%include f.rc"); | write_file(dir.path().join("e.rc"), "[x]\ne=e\n%include f.rc"); | ||||
| write_file( | write_file( | ||||
| dir.path().join("f.rc"), | dir.path().join("f.rc"), | ||||
| "[x]\nf=f\n%include e.rc\n%include rootrc", | "[x]\nf=f\n%include e.rc\n%include rootrc", | ||||
| ); | ); | ||||
| let mut cfg = ConfigSet::new(); | let mut cfg = ConfigSet::new(); | ||||
| let errors = cfg.load_path(dir.path().join("rootrc"), &"test_parse_include".into()); | let errors = cfg.load_path( | ||||
| dir.path().join("rootrc"), | |||||
| &"test_parse_include".into(), | |||||
| ); | |||||
| assert!(errors.is_empty()); | assert!(errors.is_empty()); | ||||
| assert_eq!(cfg.sections(), vec![Bytes::from("x"), Bytes::from("y")]); | assert_eq!(cfg.sections(), vec![Bytes::from("x"), Bytes::from("y")]); | ||||
| assert_eq!( | assert_eq!( | ||||
| cfg.keys("x"), | cfg.keys("x"), | ||||
| vec![ | vec![ | ||||
| Bytes::from("b"), | Bytes::from("b"), | ||||
| Bytes::from("a"), | Bytes::from("a"), | ||||
| "%include ./${FOO}1/$FOO/3.rc\n\ | "%include ./${FOO}1/$FOO/3.rc\n\ | ||||
| %include ./%FOO%2/%FOO%/4.rc\n", | %include ./%FOO%2/%FOO%/4.rc\n", | ||||
| ); | ); | ||||
| write_file(dir.path().join("f1/f/3.rc"), "[x]\na=1\n"); | write_file(dir.path().join("f1/f/3.rc"), "[x]\na=1\n"); | ||||
| write_file(dir.path().join("f2/f/4.rc"), "[y]\nb=2\n"); | write_file(dir.path().join("f2/f/4.rc"), "[y]\nb=2\n"); | ||||
| let mut cfg = ConfigSet::new(); | let mut cfg = ConfigSet::new(); | ||||
| let errors = cfg.load_path(dir.path().join("rootrc"), &"include_expand".into()); | let errors = | ||||
| cfg.load_path(dir.path().join("rootrc"), &"include_expand".into()); | |||||
| assert!(errors.is_empty()); | assert!(errors.is_empty()); | ||||
| assert_eq!(cfg.get("x", "a"), Some(Bytes::from("1"))); | assert_eq!(cfg.get("x", "a"), Some(Bytes::from("1"))); | ||||
| assert_eq!(cfg.get("y", "b"), Some(Bytes::from("2"))); | assert_eq!(cfg.get("y", "b"), Some(Bytes::from("2"))); | ||||
| } | } | ||||
| } | } | ||||
| pub const HGPLAIN: &str = "HGPLAIN"; | pub const HGPLAIN: &str = "HGPLAIN"; | ||||
| pub const HGPLAINEXCEPT: &str = "HGPLAINEXCEPT"; | pub const HGPLAINEXCEPT: &str = "HGPLAINEXCEPT"; | ||||
| pub const HGRCPATH: &str = "HGRCPATH"; | pub const HGRCPATH: &str = "HGRCPATH"; | ||||
| pub trait OptionsHgExt { | pub trait OptionsHgExt { | ||||
| /// Drop configs according to `$HGPLAIN` and `$HGPLAINEXCEPT`. | /// Drop configs according to `$HGPLAIN` and `$HGPLAINEXCEPT`. | ||||
| fn process_hgplain(self) -> Self; | fn process_hgplain(self) -> Self; | ||||
| /// Set read-only config items. `items` contains a list of tuple `(section, name)`. | /// Set read-only config items. `items` contains a list of tuple `(section, | ||||
| /// Setting those items to new value will be ignored. | /// name)`. Setting those items to new value will be ignored. | ||||
| fn readonly_items<S: Into<Bytes>, N: Into<Bytes>>(self, items: Vec<(S, N)>) -> Self; | fn readonly_items<S: Into<Bytes>, N: Into<Bytes>>( | ||||
| self, | |||||
| /// Set section remap. If a section name matches an entry key, it will be treated as if the | items: Vec<(S, N)>, | ||||
| /// name is the entry value. The remap wouldn't happen recursively. For example, with a | ) -> Self; | ||||
| /// `{"A": "B", "B": "C"}` map, section name "A" will be treated as "B", not "C". | |||||
| /// Set section remap. If a section name matches an entry key, it will be | |||||
| /// treated as if the name is the entry value. The remap wouldn't | |||||
| /// happen recursively. For example, with a `{"A": "B", "B": "C"}` map, | |||||
| /// section name "A" will be treated as "B", not "C". | |||||
| /// This is implemented via `append_filter`. | /// This is implemented via `append_filter`. | ||||
| fn remap_sections<K: Eq + Hash + Into<Bytes>, V: Into<Bytes>>( | fn remap_sections<K: Eq + Hash + Into<Bytes>, V: Into<Bytes>>( | ||||
| self, | self, | ||||
| remap: HashMap<K, V>, | remap: HashMap<K, V>, | ||||
| ) -> Self; | ) -> Self; | ||||
| /// Set section whitelist. Sections outside the whitelist won't be loaded. | /// Set section whitelist. Sections outside the whitelist won't be loaded. | ||||
| /// This is implemented via `append_filter`. | /// This is implemented via `append_filter`. | ||||
| fn whitelist_sections<B: Clone + Into<Bytes>>(self, sections: Vec<B>) -> Self; | fn whitelist_sections<B: Clone + Into<Bytes>>( | ||||
| self, | |||||
| sections: Vec<B>, | |||||
| ) -> Self; | |||||
| } | } | ||||
| pub trait ConfigSetHgExt { | pub trait ConfigSetHgExt { | ||||
| /// Load system config files if `$HGRCPATH` is not set. | /// Load system config files if `$HGRCPATH` is not set. | ||||
| /// Return errors parsing files. | /// Return errors parsing files. | ||||
| fn load_system(&mut self) -> Vec<Error>; | fn load_system(&mut self) -> Vec<Error>; | ||||
| /// Load user config files (and environment variables). If `$HGRCPATH` is | /// Load user config files (and environment variables). If `$HGRCPATH` is | ||||
| /// set, load files listed in that environment variable instead. | /// set, load files listed in that environment variable instead. | ||||
| /// Return errors parsing files. | /// Return errors parsing files. | ||||
| fn load_user(&mut self) -> Vec<Error>; | fn load_user(&mut self) -> Vec<Error>; | ||||
| /// Load a specified config file. Respect HGPLAIN environment variables. | /// Load a specified config file. Respect HGPLAIN environment variables. | ||||
| /// Return errors parsing files. | /// Return errors parsing files. | ||||
| fn load_hgrc(&mut self, path: impl AsRef<Path>, source: &'static str) -> Vec<Error>; | fn load_hgrc( | ||||
| &mut self, | |||||
| path: impl AsRef<Path>, | |||||
| source: &'static str, | |||||
| ) -> Vec<Error>; | |||||
| /// Get a config item. Convert to type `T`. | /// Get a config item. Convert to type `T`. | ||||
| fn get_opt<T: FromConfigValue>(&self, section: &str, name: &str) -> Result<Option<T>>; | fn get_opt<T: FromConfigValue>( | ||||
| &self, | |||||
| section: &str, | |||||
| name: &str, | |||||
| ) -> Result<Option<T>>; | |||||
| /// Get a config item. Convert to type `T`. | /// Get a config item. Convert to type `T`. | ||||
| /// | /// | ||||
| /// If the config item is not set, calculate it using `default_func`. | /// If the config item is not set, calculate it using `default_func`. | ||||
| fn get_or<T: FromConfigValue>( | fn get_or<T: FromConfigValue>( | ||||
| &self, | &self, | ||||
| section: &str, | section: &str, | ||||
| name: &str, | name: &str, | ||||
| default_func: impl Fn() -> T, | default_func: impl Fn() -> T, | ||||
| ) -> Result<T> { | ) -> Result<T> { | ||||
| Ok(self.get_opt(section, name)?.unwrap_or_else(default_func)) | Ok(self.get_opt(section, name)?.unwrap_or_else(default_func)) | ||||
| } | } | ||||
| /// Get a config item. Convert to type `T`. | /// Get a config item. Convert to type `T`. | ||||
| /// | /// | ||||
| /// If the config item is not set, return `T::default()`. | /// If the config item is not set, return `T::default()`. | ||||
| fn get_or_default<T: Default + FromConfigValue>(&self, section: &str, name: &str) -> Result<T> { | fn get_or_default<T: Default + FromConfigValue>( | ||||
| &self, | |||||
| section: &str, | |||||
| name: &str, | |||||
| ) -> Result<T> { | |||||
| self.get_or(section, name, Default::default) | self.get_or(section, name, Default::default) | ||||
| } | } | ||||
| } | } | ||||
| pub trait FromConfigValue: Sized { | pub trait FromConfigValue: Sized { | ||||
| fn try_from_bytes(bytes: &[u8]) -> Result<Self>; | fn try_from_bytes(bytes: &[u8]) -> Result<Self>; | ||||
| } | } | ||||
| let plain_exceptions: HashSet<String> = plain_except | let plain_exceptions: HashSet<String> = plain_except | ||||
| .unwrap_or_else(|_| "".to_string()) | .unwrap_or_else(|_| "".to_string()) | ||||
| .split(',') | .split(',') | ||||
| .map(|s| s.to_string()) | .map(|s| s.to_string()) | ||||
| .collect(); | .collect(); | ||||
| // [defaults] and [commands] are always blacklisted. | // [defaults] and [commands] are always blacklisted. | ||||
| let mut section_blacklist: HashSet<Bytes> = | let mut section_blacklist: HashSet<Bytes> = | ||||
| ["defaults", "commands"].iter().map(|&s| s.into()).collect(); | ["defaults", "commands"] | ||||
| .iter() | |||||
| .map(|&s| s.into()) | |||||
| .collect(); | |||||
| // [alias], [revsetalias], [templatealias] are blacklisted if they are outside | // [alias], [revsetalias], [templatealias] are blacklisted if | ||||
| // HGPLAINEXCEPT. | // they are outside HGPLAINEXCEPT. | ||||
| for &name in ["alias", "revsetalias", "templatealias"].iter() { | for &name in ["alias", "revsetalias", "templatealias"].iter() { | ||||
| if !plain_exceptions.contains(name) { | if !plain_exceptions.contains(name) { | ||||
| section_blacklist.insert(Bytes::from(name)); | section_blacklist.insert(Bytes::from(name)); | ||||
| } | } | ||||
| } | } | ||||
| // These configs under [ui] are always blacklisted. | // These configs under [ui] are always blacklisted. | ||||
| let mut ui_blacklist: HashSet<Bytes> = [ | let mut ui_blacklist: HashSet<Bytes> = [ | ||||
| "debug", | "debug", | ||||
| "fallbackencoding", | "fallbackencoding", | ||||
| "quiet", | "quiet", | ||||
| "slash", | "slash", | ||||
| "logtemplate", | "logtemplate", | ||||
| "statuscopies", | "statuscopies", | ||||
| "style", | "style", | ||||
| "traceback", | "traceback", | ||||
| "verbose", | "verbose", | ||||
| ] | ] | ||||
| .iter() | .iter() | ||||
| .map(|&s| s.into()) | .map(|&s| s.into()) | ||||
| .collect(); | .collect(); | ||||
| // exitcodemask is blacklisted if exitcode is outside HGPLAINEXCEPT. | // exitcodemask is blacklisted if exitcode is outside | ||||
| // HGPLAINEXCEPT. | |||||
| if !plain_exceptions.contains("exitcode") { | if !plain_exceptions.contains("exitcode") { | ||||
| ui_blacklist.insert("exitcodemask".into()); | ui_blacklist.insert("exitcodemask".into()); | ||||
| } | } | ||||
| (section_blacklist, ui_blacklist) | (section_blacklist, ui_blacklist) | ||||
| }; | }; | ||||
| let filter = move |section: Bytes, name: Bytes, value: Option<Bytes>| { | let filter = | ||||
| move |section: Bytes, name: Bytes, value: Option<Bytes>| { | |||||
| if section_blacklist.contains(§ion) | if section_blacklist.contains(§ion) | ||||
| || (section.as_ref() == b"ui" && ui_blacklist.contains(&name)) | || (section.as_ref() == b"ui" | ||||
| && ui_blacklist.contains(&name)) | |||||
| { | { | ||||
| None | None | ||||
| } else { | } else { | ||||
| Some((section, name, value)) | Some((section, name, value)) | ||||
| } | } | ||||
| }; | }; | ||||
| self.append_filter(Box::new(filter)) | self.append_filter(Box::new(filter)) | ||||
| } else { | } else { | ||||
| self | self | ||||
| } | } | ||||
| } | } | ||||
| /// Set section whitelist. Sections outside the whitelist won't be loaded. | /// Set section whitelist. Sections outside the whitelist won't be loaded. | ||||
| /// This is implemented via `append_filter`. | /// This is implemented via `append_filter`. | ||||
| fn whitelist_sections<B: Clone + Into<Bytes>>(self, sections: Vec<B>) -> Self { | fn whitelist_sections<B: Clone + Into<Bytes>>( | ||||
| self, | |||||
| sections: Vec<B>, | |||||
| ) -> Self { | |||||
| let whitelist: HashSet<Bytes> = sections | let whitelist: HashSet<Bytes> = sections | ||||
| .iter() | .iter() | ||||
| .cloned() | .cloned() | ||||
| .map(|section| section.into()) | .map(|section| section.into()) | ||||
| .collect(); | .collect(); | ||||
| let filter = move |section: Bytes, name: Bytes, value: Option<Bytes>| { | let filter = | ||||
| move |section: Bytes, name: Bytes, value: Option<Bytes>| { | |||||
| if whitelist.contains(§ion) { | if whitelist.contains(§ion) { | ||||
| Some((section, name, value)) | Some((section, name, value)) | ||||
| } else { | } else { | ||||
| None | None | ||||
| } | } | ||||
| }; | }; | ||||
| self.append_filter(Box::new(filter)) | self.append_filter(Box::new(filter)) | ||||
| } | } | ||||
| /// Set section remap. If a section name matches an entry key, it will be treated as if the | /// Set section remap. If a section name matches an entry key, it will be | ||||
| /// name is the entry value. The remap wouldn't happen recursively. For example, with a | /// treated as if the name is the entry value. The remap wouldn't | ||||
| /// `{"A": "B", "B": "C"}` map, section name "A" will be treated as "B", not "C". | /// happen recursively. For example, with a `{"A": "B", "B": "C"}` map, | ||||
| /// section name "A" will be treated as "B", not "C". | |||||
| /// This is implemented via `append_filter`. | /// This is implemented via `append_filter`. | ||||
| fn remap_sections<K, V>(self, remap: HashMap<K, V>) -> Self | fn remap_sections<K, V>(self, remap: HashMap<K, V>) -> Self | ||||
| where | where | ||||
| K: Eq + Hash + Into<Bytes>, | K: Eq + Hash + Into<Bytes>, | ||||
| V: Into<Bytes>, | V: Into<Bytes>, | ||||
| { | { | ||||
| let remap: HashMap<Bytes, Bytes> = remap | let remap: HashMap<Bytes, Bytes> = remap | ||||
| .into_iter() | .into_iter() | ||||
| .map(|(k, v)| (k.into(), v.into())) | .map(|(k, v)| (k.into(), v.into())) | ||||
| .collect(); | .collect(); | ||||
| let filter = move |section: Bytes, name: Bytes, value: Option<Bytes>| { | let filter = | ||||
| move |section: Bytes, name: Bytes, value: Option<Bytes>| { | |||||
| let section = remap.get(§ion).cloned().unwrap_or(section); | let section = remap.get(§ion).cloned().unwrap_or(section); | ||||
| Some((section, name, value)) | Some((section, name, value)) | ||||
| }; | }; | ||||
| self.append_filter(Box::new(filter)) | self.append_filter(Box::new(filter)) | ||||
| } | } | ||||
| fn readonly_items<S: Into<Bytes>, N: Into<Bytes>>(self, items: Vec<(S, N)>) -> Self { | fn readonly_items<S: Into<Bytes>, N: Into<Bytes>>( | ||||
| self, | |||||
| items: Vec<(S, N)>, | |||||
| ) -> Self { | |||||
| let readonly_items: HashSet<(Bytes, Bytes)> = items | let readonly_items: HashSet<(Bytes, Bytes)> = items | ||||
| .into_iter() | .into_iter() | ||||
| .map(|(section, name)| (section.into(), name.into())) | .map(|(section, name)| (section.into(), name.into())) | ||||
| .collect(); | .collect(); | ||||
| let filter = move |section: Bytes, name: Bytes, value: Option<Bytes>| { | let filter = | ||||
| move |section: Bytes, name: Bytes, value: Option<Bytes>| { | |||||
| if readonly_items.contains(&(section.clone(), name.clone())) { | if readonly_items.contains(&(section.clone(), name.clone())) { | ||||
| None | None | ||||
| } else { | } else { | ||||
| Some((section, name, value)) | Some((section, name, value)) | ||||
| } | } | ||||
| }; | }; | ||||
| self.append_filter(Box::new(filter)) | self.append_filter(Box::new(filter)) | ||||
| } | } | ||||
| } | } | ||||
| impl ConfigSetHgExt for ConfigSet { | impl ConfigSetHgExt for ConfigSet { | ||||
| fn load_system(&mut self) -> Vec<Error> { | fn load_system(&mut self) -> Vec<Error> { | ||||
| let opts = Options::new().source("system").process_hgplain(); | let opts = Options::new().source("system").process_hgplain(); | ||||
| let mut errors = Vec::new(); | let mut errors = Vec::new(); | ||||
| if env::var(HGRCPATH).is_err() { | if env::var(HGRCPATH).is_err() { | ||||
| #[cfg(unix)] | #[cfg(unix)] | ||||
| { | { | ||||
| errors.append(&mut self.load_path("/etc/mercurial/system.rc", &opts)); | |||||
| // TODO(T40519286): Remove this after the tupperware overrides move out of hgrc.d | |||||
| errors.append( | errors.append( | ||||
| &mut self.load_path("/etc/mercurial/hgrc.d/tupperware_overrides.rc", &opts), | &mut self.load_path("/etc/mercurial/system.rc", &opts), | ||||
| ); | |||||
| // TODO(T40519286): Remove this after the tupperware overrides | |||||
| // move out of hgrc.d | |||||
| errors.append(&mut self.load_path( | |||||
| "/etc/mercurial/hgrc.d/tupperware_overrides.rc", | |||||
| &opts, | |||||
| )); | |||||
| // TODO(quark): Remove this after packages using system.rc are | |||||
| // rolled out | |||||
| errors.append( | |||||
| &mut self | |||||
| .load_path("/etc/mercurial/hgrc.d/include.rc", &opts), | |||||
| ); | ); | ||||
| // TODO(quark): Remove this after packages using system.rc are rolled out | |||||
| errors.append(&mut self.load_path("/etc/mercurial/hgrc.d/include.rc", &opts)); | |||||
| } | } | ||||
| #[cfg(windows)] | #[cfg(windows)] | ||||
| { | { | ||||
| if let Ok(program_data_path) = env::var("PROGRAMDATA") { | if let Ok(program_data_path) = env::var("PROGRAMDATA") { | ||||
| use std::path::Path; | use std::path::Path; | ||||
| let hgrc_dir = Path::new(&program_data_path).join("Facebook\\Mercurial"); | let hgrc_dir = Path::new(&program_data_path) | ||||
| errors.append(&mut self.load_path(hgrc_dir.join("system.rc"), &opts)); | .join("Facebook\\Mercurial"); | ||||
| // TODO(quark): Remove this after packages using system.rc are rolled out | errors.append( | ||||
| errors.append(&mut self.load_path(hgrc_dir.join("hgrc"), &opts)); | &mut self.load_path(hgrc_dir.join("system.rc"), &opts), | ||||
| ); | |||||
| // TODO(quark): Remove this after packages using system.rc | |||||
| // are rolled out | |||||
| errors.append( | |||||
| &mut self.load_path(hgrc_dir.join("hgrc"), &opts), | |||||
| ); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| errors | errors | ||||
| } | } | ||||
| fn load_user(&mut self) -> Vec<Error> { | fn load_user(&mut self) -> Vec<Error> { | ||||
| let paths = rcpath.split(':'); | let paths = rcpath.split(':'); | ||||
| #[cfg(windows)] | #[cfg(windows)] | ||||
| let paths = rcpath.split(';'); | let paths = rcpath.split(';'); | ||||
| for path in paths { | for path in paths { | ||||
| errors.append(&mut self.load_path(expand_path(path), &opts)); | errors.append(&mut self.load_path(expand_path(path), &opts)); | ||||
| } | } | ||||
| } else { | } else { | ||||
| if let Some(home_dir) = dirs::home_dir() { | if let Some(home_dir) = dirs::home_dir() { | ||||
| errors.append(&mut self.load_path(home_dir.join(".hgrc"), &opts)); | errors.append( | ||||
| &mut self.load_path(home_dir.join(".hgrc"), &opts), | |||||
| ); | |||||
| #[cfg(windows)] | #[cfg(windows)] | ||||
| { | { | ||||
| errors.append(&mut self.load_path(home_dir.join("mercurial.ini"), &opts)); | errors.append( | ||||
| &mut self | |||||
| .load_path(home_dir.join("mercurial.ini"), &opts), | |||||
| ); | |||||
| } | } | ||||
| } | } | ||||
| if let Some(config_dir) = dirs::config_dir() { | if let Some(config_dir) = dirs::config_dir() { | ||||
| errors.append(&mut self.load_path(config_dir.join("hg/hgrc"), &opts)); | errors.append( | ||||
| &mut self.load_path(config_dir.join("hg/hgrc"), &opts), | |||||
| ); | |||||
| } | } | ||||
| } | } | ||||
| errors | errors | ||||
| } | } | ||||
| fn load_hgrc(&mut self, path: impl AsRef<Path>, source: &'static str) -> Vec<Error> { | fn load_hgrc( | ||||
| &mut self, | |||||
| path: impl AsRef<Path>, | |||||
| source: &'static str, | |||||
| ) -> Vec<Error> { | |||||
| let opts = Options::new().source(source).process_hgplain(); | let opts = Options::new().source(source).process_hgplain(); | ||||
| self.load_path(path, &opts) | self.load_path(path, &opts) | ||||
| } | } | ||||
| fn get_opt<T: FromConfigValue>(&self, section: &str, name: &str) -> Result<Option<T>> { | fn get_opt<T: FromConfigValue>( | ||||
| &self, | |||||
| section: &str, | |||||
| name: &str, | |||||
| ) -> Result<Option<T>> { | |||||
| ConfigSet::get(self, section, name) | ConfigSet::get(self, section, name) | ||||
| .map(|bytes| T::try_from_bytes(&bytes)) | .map(|bytes| T::try_from_bytes(&bytes)) | ||||
| .transpose() | .transpose() | ||||
| } | } | ||||
| } | } | ||||
| impl FromConfigValue for bool { | impl FromConfigValue for bool { | ||||
| fn try_from_bytes(bytes: &[u8]) -> Result<Self> { | fn try_from_bytes(bytes: &[u8]) -> Result<Self> { | ||||
| let value = std::str::from_utf8(bytes)?.to_lowercase(); | let value = std::str::from_utf8(bytes)?.to_lowercase(); | ||||
| match value.as_ref() { | match value.as_ref() { | ||||
| "1" | "yes" | "true" | "on" | "always" => Ok(true), | "1" | "yes" | "true" | "on" | "always" => Ok(true), | ||||
| "0" | "no" | "false" | "off" | "never" => Ok(false), | "0" | "no" | "false" | "off" | "never" => Ok(false), | ||||
| _ => Err(Error::Convert(format!("invalid bool: {}", value)).into()), | _ => { | ||||
| Err(Error::Convert(format!("invalid bool: {}", value)).into()) | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| impl FromConfigValue for i8 { | impl FromConfigValue for i8 { | ||||
| fn try_from_bytes(bytes: &[u8]) -> Result<Self> { | fn try_from_bytes(bytes: &[u8]) -> Result<Self> { | ||||
| let value = std::str::from_utf8(bytes)?.parse()?; | let value = std::str::from_utf8(bytes)?.parse()?; | ||||
| Ok(value) | Ok(value) | ||||
| fn try_from_bytes(bytes: &[u8]) -> Result<Self> { | fn try_from_bytes(bytes: &[u8]) -> Result<Self> { | ||||
| let value = std::str::from_utf8(bytes)?.parse()?; | let value = std::str::from_utf8(bytes)?.parse()?; | ||||
| Ok(value) | Ok(value) | ||||
| } | } | ||||
| } | } | ||||
| impl FromConfigValue for String { | impl FromConfigValue for String { | ||||
| fn try_from_bytes(bytes: &[u8]) -> Result<Self> { | fn try_from_bytes(bytes: &[u8]) -> Result<Self> { | ||||
| String::from_utf8(bytes.to_vec()) | String::from_utf8(bytes.to_vec()).map_err(|_| { | ||||
| .map_err(|_| Error::Convert(format!("{:?} is not utf8 encoded", bytes)).into()) | Error::Convert(format!("{:?} is not utf8 encoded", bytes)).into() | ||||
| }) | |||||
| } | } | ||||
| } | } | ||||
| /// Byte count specified with a unit. For example: `1.5 MB`. | /// Byte count specified with a unit. For example: `1.5 MB`. | ||||
| #[derive(Copy, Clone, Default)] | #[derive(Copy, Clone, Default)] | ||||
| pub struct ByteCount(u64); | pub struct ByteCount(u64); | ||||
| impl ByteCount { | impl ByteCount { | ||||
| ("t", 1 << 40), | ("t", 1 << 40), | ||||
| ("b", 1), | ("b", 1), | ||||
| ("", 1), | ("", 1), | ||||
| ]; | ]; | ||||
| let value = std::str::from_utf8(bytes)?.to_lowercase(); | let value = std::str::from_utf8(bytes)?.to_lowercase(); | ||||
| for (suffix, unit) in sizeunits.iter() { | for (suffix, unit) in sizeunits.iter() { | ||||
| if value.ends_with(suffix) { | if value.ends_with(suffix) { | ||||
| let number_str: &str = value[..value.len() - suffix.len()].trim(); | let number_str: &str = | ||||
| value[..value.len() - suffix.len()].trim(); | |||||
| let number: f64 = number_str.parse()?; | let number: f64 = number_str.parse()?; | ||||
| if number < 0.0 { | if number < 0.0 { | ||||
| return Err(Error::Convert(format!( | return Err(Error::Convert(format!( | ||||
| "byte size '{:?}' cannot be negative", | "byte size '{:?}' cannot be negative", | ||||
| value | value | ||||
| )) | )) | ||||
| .into()); | .into()); | ||||
| } | } | ||||
| let unit = *unit as f64; | let unit = *unit as f64; | ||||
| return Ok(ByteCount((number * unit) as u64)); | return Ok(ByteCount((number * unit) as u64)); | ||||
| } | } | ||||
| } | } | ||||
| Err(Error::Convert(format!("'{:?}' cannot be parsed as a byte size", value)).into()) | Err(Error::Convert(format!( | ||||
| "'{:?}' cannot be parsed as a byte size", | |||||
| value | |||||
| )) | |||||
| .into()) | |||||
| } | } | ||||
| } | } | ||||
| impl FromConfigValue for PathBuf { | impl FromConfigValue for PathBuf { | ||||
| fn try_from_bytes(bytes: &[u8]) -> Result<Self> { | fn try_from_bytes(bytes: &[u8]) -> Result<Self> { | ||||
| let st = std::str::from_utf8(&bytes)?; | let st = std::str::from_utf8(&bytes)?; | ||||
| Ok(expand_path(st)) | Ok(expand_path(st)) | ||||
| // elif s[offset:offset + 1] == '"' and parts[-1][-1:] == '\\': | // elif s[offset:offset + 1] == '"' and parts[-1][-1:] == '\\': | ||||
| // parts[-1] = parts[-1][:-1] + s[offset:offset + 1] | // parts[-1] = parts[-1][:-1] + s[offset:offset + 1] | ||||
| // return _parse_plain, parts, offset + 1 | // return _parse_plain, parts, offset + 1 | ||||
| // parts[-1] += s[offset:offset + 1] | // parts[-1] += s[offset:offset + 1] | ||||
| // return _parse_plain, parts, offset + 1 | // return _parse_plain, parts, offset + 1 | ||||
| // ``` | // ``` | ||||
| State::Plain => { | State::Plain => { | ||||
| let mut whitespace = false; | let mut whitespace = false; | ||||
| while offset < value.len() && b" \n\r\t,".contains(&value[offset]) { | while offset < value.len() | ||||
| && b" \n\r\t,".contains(&value[offset]) | |||||
| { | |||||
| whitespace = true; | whitespace = true; | ||||
| offset += 1; | offset += 1; | ||||
| } | } | ||||
| if offset >= value.len() { | if offset >= value.len() { | ||||
| break; | break; | ||||
| } | } | ||||
| if whitespace { | if whitespace { | ||||
| parts.push(Vec::new()); | parts.push(Vec::new()); | ||||
| // else: | // else: | ||||
| // return None, parts, offset | // return None, parts, offset | ||||
| // return _parse_plain, parts, offset | // return _parse_plain, parts, offset | ||||
| // ``` | // ``` | ||||
| State::Quote => { | State::Quote => { | ||||
| if offset < value.len() && value[offset] == b'"' { | if offset < value.len() && value[offset] == b'"' { | ||||
| parts.push(Vec::new()); | parts.push(Vec::new()); | ||||
| offset += 1; | offset += 1; | ||||
| while offset < value.len() && b" \n\r\t,".contains(&value[offset]) { | while offset < value.len() | ||||
| && b" \n\r\t,".contains(&value[offset]) | |||||
| { | |||||
| offset += 1; | offset += 1; | ||||
| } | } | ||||
| state = State::Plain; | state = State::Plain; | ||||
| continue; | continue; | ||||
| } | } | ||||
| while offset < value.len() && value[offset] != b'"' { | while offset < value.len() && value[offset] != b'"' { | ||||
| if value[offset] == b'\\' | if value[offset] == b'\\' | ||||
| && offset + 1 < value.len() | && offset + 1 < value.len() | ||||
| && value[offset + 1] == b'"' | && value[offset + 1] == b'"' | ||||
| { | { | ||||
| offset += 1; | offset += 1; | ||||
| parts.last_mut().unwrap().push(b'"'); | parts.last_mut().unwrap().push(b'"'); | ||||
| } else { | } else { | ||||
| parts.last_mut().unwrap().push(value[offset]); | parts.last_mut().unwrap().push(value[offset]); | ||||
| } | } | ||||
| offset += 1; | offset += 1; | ||||
| } | } | ||||
| if offset >= value.len() { | if offset >= value.len() { | ||||
| let mut real_parts: Vec<Vec<u8>> = parse_list_internal(parts.last().unwrap()) | let mut real_parts: Vec<Vec<u8>> = | ||||
| parse_list_internal(parts.last().unwrap()) | |||||
| .iter() | .iter() | ||||
| .map(|b| b.to_vec()) | .map(|b| b.to_vec()) | ||||
| .collect(); | .collect(); | ||||
| if real_parts.is_empty() { | if real_parts.is_empty() { | ||||
| parts.pop(); | parts.pop(); | ||||
| parts.push(vec![b'"']); | parts.push(vec![b'"']); | ||||
| } else { | } else { | ||||
| real_parts[0].insert(0, b'"'); | real_parts[0].insert(0, b'"'); | ||||
| parts.pop(); | parts.pop(); | ||||
| parts.append(&mut real_parts); | parts.append(&mut real_parts); | ||||
| } | } | ||||
| assert_eq!( | assert_eq!( | ||||
| parse_list(b"\"hello world\", \"how are you?\""), | parse_list(b"\"hello world\", \"how are you?\""), | ||||
| vec![b("hello world"), b("how are you?")] | vec![b("hello world"), b("how are you?")] | ||||
| ); | ); | ||||
| assert_eq!( | assert_eq!( | ||||
| parse_list(b"Do\"Not\"Separate"), | parse_list(b"Do\"Not\"Separate"), | ||||
| vec![b("Do\"Not\"Separate")] | vec![b("Do\"Not\"Separate")] | ||||
| ); | ); | ||||
| assert_eq!(parse_list(b"\"Do\"Separate"), vec![b("Do"), b("Separate")]); | assert_eq!( | ||||
| parse_list(b"\"Do\"Separate"), | |||||
| vec![b("Do"), b("Separate")] | |||||
| ); | |||||
| assert_eq!( | assert_eq!( | ||||
| parse_list(b"\"Do\\\"NotSeparate\""), | parse_list(b"\"Do\\\"NotSeparate\""), | ||||
| vec![b("Do\"NotSeparate")] | vec![b("Do\"NotSeparate")] | ||||
| ); | ); | ||||
| assert_eq!( | assert_eq!( | ||||
| parse_list(&b"string \"with extraneous\" quotation mark\""[..]), | parse_list(&b"string \"with extraneous\" quotation mark\""[..]), | ||||
| vec![ | vec![ | ||||
| b("string"), | b("string"), | ||||
| vec![b(""), b(" key = "), b("x\""), b("y"), b(""), b("\"")] | vec![b(""), b(" key = "), b("x\""), b("y"), b(""), b("\"")] | ||||
| ); | ); | ||||
| assert_eq!(parse_list(b",,,, "), Vec::<Bytes>::new()); | assert_eq!(parse_list(b",,,, "), Vec::<Bytes>::new()); | ||||
| assert_eq!( | assert_eq!( | ||||
| parse_list(b"\" just with starting quotation"), | parse_list(b"\" just with starting quotation"), | ||||
| vec![b("\""), b("just"), b("with"), b("starting"), b("quotation")] | vec![b("\""), b("just"), b("with"), b("starting"), b("quotation")] | ||||
| ); | ); | ||||
| assert_eq!( | assert_eq!( | ||||
| parse_list(&b"\"longer quotation\" with \"no ending quotation"[..]), | parse_list( | ||||
| &b"\"longer quotation\" with \"no ending quotation"[..] | |||||
| ), | |||||
| vec![ | vec![ | ||||
| b("longer quotation"), | b("longer quotation"), | ||||
| b("with"), | b("with"), | ||||
| b("\"no"), | b("\"no"), | ||||
| b("ending"), | b("ending"), | ||||
| b("quotation"), | b("quotation"), | ||||
| ] | ] | ||||
| ); | ); | ||||
| assert_eq!( | assert_eq!( | ||||
| parse_list(&b"this is \\\" \"not a quotation mark\""[..]), | parse_list(&b"this is \\\" \"not a quotation mark\""[..]), | ||||
| vec![b("this"), b("is"), b("\""), b("not a quotation mark")] | vec![b("this"), b("is"), b("\""), b("not a quotation mark")] | ||||
| ); | ); | ||||
| assert_eq!(parse_list(b"\n \n\nding\ndong"), vec![b("ding"), b("dong")]); | assert_eq!( | ||||
| parse_list(b"\n \n\nding\ndong"), | |||||
| vec![b("ding"), b("dong")] | |||||
| ); | |||||
| // Other manually written cases | // Other manually written cases | ||||
| assert_eq!(parse_list("a,b,,c"), vec![b("a"), b("b"), b("c")]); | assert_eq!(parse_list("a,b,,c"), vec![b("a"), b("b"), b("c")]); | ||||
| assert_eq!(parse_list("a b c"), vec![b("a"), b("b"), b("c")]); | assert_eq!(parse_list("a b c"), vec![b("a"), b("b"), b("c")]); | ||||
| assert_eq!( | assert_eq!( | ||||
| parse_list(" , a , , b, , c , "), | parse_list(" , a , , b, , c , "), | ||||
| vec![b("a"), b("b"), b("c")] | vec![b("a"), b("b"), b("c")] | ||||
| ); | ); | ||||
| /* | /* | ||||
| * Copyright (c) Facebook, Inc. and its affiliates. | * Copyright (c) Facebook, Inc. and its affiliates. | ||||
| * | * | ||||
| * This software may be used and distributed according to the terms of the | * This software may be used and distributed according to the terms of the | ||||
| * GNU General Public License version 2. | * GNU General Public License version 2. | ||||
| */ | */ | ||||
| //! # ConfigParser | //! # ConfigParser | ||||
| //! | //! | ||||
| //! ConfigParser is a utility to parse hgrc-like config files. | //! ConfigParser is a utility to parse hgrc-like config files. | ||||
| //! | //! | ||||
| //! ## Features | //! ## Features | ||||
| //! | //! | ||||
| //! - Parse valid hgrc-like config files efficiently. | //! - Parse valid hgrc-like config files efficiently. | ||||
| //! - Track source locations of config values. Keep multiple locations of | //! - Track source locations of config values. Keep multiple locations of a | ||||
| //! a same config if it is overridden. | //! same config if it is overridden. | ||||
| //! | //! | ||||
| //! ## Config Format | //! ## Config Format | ||||
| //! | //! | ||||
| //! hgrc files are similar to INI files: | //! hgrc files are similar to INI files: | ||||
| //! | //! | ||||
| //! ```plain,ignore | //! ```plain,ignore | ||||
| //! [section1] | //! [section1] | ||||
| //! name1 = value1 | //! name1 = value1 | ||||