# HG changeset patch # User Raphaël Gomès # Date 2023-07-06 12:32:07 # Node ID f8412da86d0518ccfa03f90d3f97ec615b64e9ba # Parent c51b178b0b7e22b41c976d9279854f2c680ea5bc rust-config: add support for default config items Now that configitems.toml exists, we can read from it the default values for all core config items. We will add the devel-warning for use of undeclared config items in a later patch when we're done adding the missing entries for `rhg`. diff --git a/rust/Cargo.lock b/rust/Cargo.lock --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -476,6 +476,12 @@ dependencies = [ [[package]] name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038" @@ -517,7 +523,7 @@ dependencies = [ "derive_more", "flate2", "format-bytes", - "hashbrown", + "hashbrown 0.13.1", "home", "im-rc", "itertools", @@ -535,9 +541,11 @@ dependencies = [ "regex", "same-file", "self_cell", + "serde", "sha-1 0.10.0", "tempfile", "thread_local", + "toml", "twox-hash", "zstd", ] @@ -610,6 +618,16 @@ dependencies = [ ] [[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -749,6 +767,15 @@ dependencies = [ ] [[package]] +name = "nom8" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" +dependencies = [ + "memchr", +] + +[[package]] name = "num-integer" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1107,6 +1134,35 @@ source = "registry+https://github.com/ru checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" [[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + +[[package]] name = "sha-1" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1160,9 +1216,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a [[package]] name = "syn" -version = "1.0.103" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -1213,6 +1269,40 @@ dependencies = [ ] [[package]] +name = "toml" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb9d890e4dc9298b70f740f615f2e05b9db37dce531f6b24fb77ac993f9f217" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b" +dependencies = [ + "indexmap", + "nom8", + "serde", + "serde_spanned", + "toml_datetime", +] + +[[package]] name = "twox-hash" version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/rust/hg-core/Cargo.toml b/rust/hg-core/Cargo.toml --- a/rust/hg-core/Cargo.toml +++ b/rust/hg-core/Cargo.toml @@ -26,10 +26,12 @@ rand_distr = "0.4.3" rayon = "1.7.0" regex = "1.7.0" self_cell = "1.0" +serde = { version = "1.0", features = ["derive"] } sha-1 = "0.10.0" twox-hash = "1.6.3" same-file = "1.0.6" tempfile = "3.3.0" +toml = "0.6" thread_local = "1.1.4" crossbeam-channel = "0.5.6" log = "0.4.17" @@ -46,5 +48,5 @@ features = ["zlib"] default-features = false [dev-dependencies] -clap = { version = "4.0.24", features = ["derive"] } +clap = { version = "~4.0", features = ["derive"] } pretty_assertions = "1.1.0" diff --git a/rust/hg-core/src/config/config.rs b/rust/hg-core/src/config/config.rs deleted file mode 100644 --- a/rust/hg-core/src/config/config.rs +++ /dev/null @@ -1,1 +0,0 @@ - diff --git a/rust/hg-core/src/config/config_items.rs b/rust/hg-core/src/config/config_items.rs new file mode 100644 --- /dev/null +++ b/rust/hg-core/src/config/config_items.rs @@ -0,0 +1,669 @@ +//! Code for parsing default Mercurial config items. +use itertools::Itertools; +use serde::Deserialize; + +use crate::{errors::HgError, exit_codes, FastHashMap}; + +/// Corresponds to the structure of `mercurial/configitems.toml`. +#[derive(Debug, Deserialize)] +pub struct ConfigItems { + items: Vec, + templates: FastHashMap>, + #[serde(rename = "template-applications")] + template_applications: Vec, +} + +/// Corresponds to a config item declaration in `mercurial/configitems.toml`. +#[derive(Clone, Debug, PartialEq, Deserialize)] +#[serde(try_from = "RawDefaultConfigItem")] +pub struct DefaultConfigItem { + /// Section of the config the item is in (e.g. `[merge-tools]`) + section: String, + /// Name of the item (e.g. `meld.gui`) + name: String, + /// Default value (can be dynamic, see [`DefaultConfigItemType`]) + default: Option, + /// If the config option is generic (e.g. `merge-tools.*`), defines + /// the priority of this item relative to other generic items. + /// If we're looking for , then all generic items within the same + /// section will be sorted by order of priority, and the first regex match + /// against `name` is returned. + #[serde(default)] + priority: Option, + /// Aliases, if any. Each alias is a tuple of `(section, name)` for each + /// option that is aliased to this one. + #[serde(default)] + alias: Vec<(String, String)>, + /// Whether the config item is marked as experimental + #[serde(default)] + experimental: bool, + /// The (possibly empty) docstring for the item + #[serde(default)] + documentation: String, +} + +/// Corresponds to the raw (i.e. on disk) structure of config items. Used as +/// an intermediate step in deserialization. +#[derive(Clone, Debug, Deserialize)] +struct RawDefaultConfigItem { + section: String, + name: String, + default: Option, + #[serde(rename = "default-type")] + default_type: Option, + #[serde(default)] + priority: isize, + #[serde(default)] + generic: bool, + #[serde(default)] + alias: Vec<(String, String)>, + #[serde(default)] + experimental: bool, + #[serde(default)] + documentation: String, +} + +impl TryFrom for DefaultConfigItem { + type Error = HgError; + + fn try_from(value: RawDefaultConfigItem) -> Result { + Ok(Self { + section: value.section, + name: value.name, + default: raw_default_to_concrete( + value.default_type, + value.default, + )?, + priority: if value.generic { + Some(value.priority) + } else { + None + }, + alias: value.alias, + experimental: value.experimental, + documentation: value.documentation, + }) + } +} + +impl DefaultConfigItem { + fn is_generic(&self) -> bool { + self.priority.is_some() + } +} + +impl<'a> TryFrom<&'a DefaultConfigItem> for Option<&'a str> { + type Error = HgError; + + fn try_from( + value: &'a DefaultConfigItem, + ) -> Result, Self::Error> { + match &value.default { + Some(default) => { + let err = HgError::abort( + format!( + "programming error: wrong query on config item '{}.{}'", + value.section, + value.name + ), + exit_codes::ABORT, + Some(format!( + "asked for '&str', type of default is '{}'", + default.type_str() + )), + ); + match default { + DefaultConfigItemType::Primitive(toml::Value::String( + s, + )) => Ok(Some(s)), + _ => Err(err), + } + } + None => Ok(None), + } + } +} + +impl TryFrom<&DefaultConfigItem> for Option { + type Error = HgError; + + fn try_from(value: &DefaultConfigItem) -> Result { + match &value.default { + Some(default) => { + let err = HgError::abort( + format!( + "programming error: wrong query on config item '{}.{}'", + value.section, + value.name + ), + exit_codes::ABORT, + Some(format!( + "asked for 'bool', type of default is '{}'", + default.type_str() + )), + ); + match default { + DefaultConfigItemType::Primitive( + toml::Value::Boolean(b), + ) => Ok(Some(*b)), + _ => Err(err), + } + } + None => Ok(Some(false)), + } + } +} + +impl TryFrom<&DefaultConfigItem> for Option { + type Error = HgError; + + fn try_from(value: &DefaultConfigItem) -> Result { + match &value.default { + Some(default) => { + let err = HgError::abort( + format!( + "programming error: wrong query on config item '{}.{}'", + value.section, + value.name + ), + exit_codes::ABORT, + Some(format!( + "asked for 'u32', type of default is '{}'", + default.type_str() + )), + ); + match default { + DefaultConfigItemType::Primitive( + toml::Value::Integer(b), + ) => { + Ok(Some((*b).try_into().expect("TOML integer to u32"))) + } + _ => Err(err), + } + } + None => Ok(None), + } + } +} + +impl TryFrom<&DefaultConfigItem> for Option { + type Error = HgError; + + fn try_from(value: &DefaultConfigItem) -> Result { + match &value.default { + Some(default) => { + let err = HgError::abort( + format!( + "programming error: wrong query on config item '{}.{}'", + value.section, + value.name + ), + exit_codes::ABORT, + Some(format!( + "asked for 'u64', type of default is '{}'", + default.type_str() + )), + ); + match default { + DefaultConfigItemType::Primitive( + toml::Value::Integer(b), + ) => { + Ok(Some((*b).try_into().expect("TOML integer to u64"))) + } + _ => Err(err), + } + } + None => Ok(None), + } + } +} + +/// Allows abstracting over more complex default values than just primitives. +/// The former `configitems.py` contained some dynamic code that is encoded +/// in this enum. +#[derive(Debug, PartialEq, Clone, Deserialize)] +pub enum DefaultConfigItemType { + /// Some primitive type (string, integer, boolean) + Primitive(toml::Value), + /// A dynamic value that will be given by the code at runtime + Dynamic, + /// An lazily-returned array (possibly only relevant in the Python impl) + /// Example: `lambda: [b"zstd", b"zlib"]` + Lambda(Vec), + /// For now, a special case for `web.encoding` that points to the + /// `encoding.encoding` module in the Python impl so that local encoding + /// is correctly resolved at runtime + LazyModule(String), + ListType, +} + +impl DefaultConfigItemType { + pub fn type_str(&self) -> &str { + match self { + DefaultConfigItemType::Primitive(primitive) => { + primitive.type_str() + } + DefaultConfigItemType::Dynamic => "dynamic", + DefaultConfigItemType::Lambda(_) => "lambda", + DefaultConfigItemType::LazyModule(_) => "lazy_module", + DefaultConfigItemType::ListType => "list_type", + } + } +} + +/// Most of the fields are shared with [`DefaultConfigItem`]. +#[derive(Debug, Clone, Deserialize)] +#[serde(try_from = "RawTemplateItem")] +struct TemplateItem { + suffix: String, + default: Option, + priority: Option, + #[serde(default)] + alias: Vec<(String, String)>, + #[serde(default)] + experimental: bool, + #[serde(default)] + documentation: String, +} + +/// Corresponds to the raw (i.e. on disk) representation of a template item. +/// Used as an intermediate step in deserialization. +#[derive(Clone, Debug, Deserialize)] +struct RawTemplateItem { + suffix: String, + default: Option, + #[serde(rename = "default-type")] + default_type: Option, + #[serde(default)] + priority: isize, + #[serde(default)] + generic: bool, + #[serde(default)] + alias: Vec<(String, String)>, + #[serde(default)] + experimental: bool, + #[serde(default)] + documentation: String, +} + +impl TemplateItem { + fn into_default_item( + self, + application: TemplateApplication, + ) -> DefaultConfigItem { + DefaultConfigItem { + section: application.section, + name: application + .prefix + .map(|prefix| format!("{}.{}", prefix, self.suffix)) + .unwrap_or(self.suffix), + default: self.default, + priority: self.priority, + alias: self.alias, + experimental: self.experimental, + documentation: self.documentation, + } + } +} + +impl TryFrom for TemplateItem { + type Error = HgError; + + fn try_from(value: RawTemplateItem) -> Result { + Ok(Self { + suffix: value.suffix, + default: raw_default_to_concrete( + value.default_type, + value.default, + )?, + priority: if value.generic { + Some(value.priority) + } else { + None + }, + alias: value.alias, + experimental: value.experimental, + documentation: value.documentation, + }) + } +} + +/// Transforms the on-disk string-based representation of complex default types +/// to the concrete [`DefaultconfigItemType`]. +fn raw_default_to_concrete( + default_type: Option, + default: Option, +) -> Result, HgError> { + Ok(match default_type.as_deref() { + None => default.as_ref().map(|default| { + DefaultConfigItemType::Primitive(default.to_owned()) + }), + Some("dynamic") => Some(DefaultConfigItemType::Dynamic), + Some("list_type") => Some(DefaultConfigItemType::ListType), + Some("lambda") => match &default { + Some(default) => Some(DefaultConfigItemType::Lambda( + default.to_owned().try_into().map_err(|e| { + HgError::abort( + e.to_string(), + exit_codes::ABORT, + Some("Check 'mercurial/configitems.toml'".into()), + ) + })?, + )), + None => { + return Err(HgError::abort( + "lambda defined with no return value".to_string(), + exit_codes::ABORT, + Some("Check 'mercurial/configitems.toml'".into()), + )) + } + }, + Some("lazy_module") => match &default { + Some(default) => { + Some(DefaultConfigItemType::LazyModule(match default { + toml::Value::String(module) => module.to_owned(), + _ => { + return Err(HgError::abort( + "lazy_module module name should be a string" + .to_string(), + exit_codes::ABORT, + Some("Check 'mercurial/configitems.toml'".into()), + )) + } + })) + } + None => { + return Err(HgError::abort( + "lazy_module should have a default value".to_string(), + exit_codes::ABORT, + Some("Check 'mercurial/configitems.toml'".into()), + )) + } + }, + Some(invalid) => { + return Err(HgError::abort( + format!("invalid default_type '{}'", invalid), + exit_codes::ABORT, + Some("Check 'mercurial/configitems.toml'".into()), + )) + } + }) +} + +#[derive(Debug, Clone, Deserialize)] +struct TemplateApplication { + template: String, + section: String, + #[serde(default)] + prefix: Option, +} + +/// Represents the (dynamic) set of default core Mercurial config items from +/// `mercurial/configitems.toml`. +#[derive(Clone, Debug, Default)] +pub struct DefaultConfig { + /// Mapping of section -> (mapping of name -> item) + items: FastHashMap>, +} + +impl DefaultConfig { + pub fn empty() -> DefaultConfig { + Self { + items: Default::default(), + } + } + + /// Returns `Self`, given the contents of `mercurial/configitems.toml` + #[logging_timer::time("trace")] + pub fn from_contents(contents: &str) -> Result { + let mut from_file: ConfigItems = + toml::from_str(contents).map_err(|e| { + HgError::abort( + e.to_string(), + exit_codes::ABORT, + Some("Check 'mercurial/configitems.toml'".into()), + ) + })?; + + let mut flat_items = from_file.items; + + for application in from_file.template_applications.drain(..) { + match from_file.templates.get(&application.template) { + None => return Err( + HgError::abort( + format!( + "template application refers to undefined template '{}'", + application.template + ), + exit_codes::ABORT, + Some("Check 'mercurial/configitems.toml'".into()) + ) + ), + Some(template_items) => { + for template_item in template_items { + flat_items.push( + template_item + .clone() + .into_default_item(application.clone()), + ) + } + } + }; + } + + let items = flat_items.into_iter().fold( + FastHashMap::default(), + |mut acc, item| { + acc.entry(item.section.to_owned()) + .or_insert_with(|| { + let mut section = FastHashMap::default(); + section.insert(item.name.to_owned(), item.to_owned()); + section + }) + .insert(item.name.to_owned(), item); + acc + }, + ); + + Ok(Self { items }) + } + + /// Return the default config item that matches `section` and `item`. + pub fn get( + &self, + section: &[u8], + item: &[u8], + ) -> Option<&DefaultConfigItem> { + // Core items must be valid UTF-8 + let section = String::from_utf8_lossy(section); + let section_map = self.items.get(section.as_ref())?; + let item_name_lossy = String::from_utf8_lossy(item); + match section_map.get(item_name_lossy.as_ref()) { + Some(item) => Some(item), + None => { + for generic_item in section_map + .values() + .filter(|item| item.is_generic()) + .sorted_by_key(|item| match item.priority { + Some(priority) => (priority, &item.name), + _ => unreachable!(), + }) + { + if regex::bytes::Regex::new(&generic_item.name) + .expect("invalid regex in configitems") + .is_match(item) + { + return Some(generic_item); + } + } + None + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::config::config_items::{ + DefaultConfigItem, DefaultConfigItemType, + }; + + use super::DefaultConfig; + + #[test] + fn test_config_read() { + let contents = r#" +[[items]] +section = "alias" +name = "abcd.*" +default = 3 +generic = true +priority = -1 + +[[items]] +section = "alias" +name = ".*" +default-type = "dynamic" +generic = true + +[[items]] +section = "cmdserver" +name = "track-log" +default-type = "lambda" +default = [ "chgserver", "cmdserver", "repocache",] + +[[items]] +section = "chgserver" +name = "idletimeout" +default = 3600 + +[[items]] +section = "cmdserver" +name = "message-encodings" +default-type = "list_type" + +[[items]] +section = "web" +name = "encoding" +default-type = "lazy_module" +default = "encoding.encoding" + +[[items]] +section = "command-templates" +name = "graphnode" +alias = [["ui", "graphnodetemplate"]] +documentation = """This is a docstring. +This is another line \ +but this is not.""" + +[[items]] +section = "censor" +name = "policy" +default = "abort" +experimental = true + +[[template-applications]] +template = "diff-options" +section = "commands" +prefix = "revert.interactive" + +[[template-applications]] +template = "diff-options" +section = "diff" + +[templates] +[[templates.diff-options]] +suffix = "nodates" +default = false + +[[templates.diff-options]] +suffix = "showfunc" +default = false + +[[templates.diff-options]] +suffix = "unified" +"#; + let res = DefaultConfig::from_contents(contents); + let config = match res { + Ok(config) => config, + Err(e) => panic!("{}", e), + }; + let expected = DefaultConfigItem { + section: "censor".into(), + name: "policy".into(), + default: Some(DefaultConfigItemType::Primitive("abort".into())), + priority: None, + alias: vec![], + experimental: true, + documentation: "".into(), + }; + assert_eq!(config.get(b"censor", b"policy"), Some(&expected)); + + // Test generic priority. The `.*` pattern is wider than `abcd.*`, but + // `abcd.*` has priority, so it should match first. + let expected = DefaultConfigItem { + section: "alias".into(), + name: "abcd.*".into(), + default: Some(DefaultConfigItemType::Primitive(3.into())), + priority: Some(-1), + alias: vec![], + experimental: false, + documentation: "".into(), + }; + assert_eq!(config.get(b"alias", b"abcdsomething"), Some(&expected)); + + //... but if it doesn't, we should fallback to `.*` + let expected = DefaultConfigItem { + section: "alias".into(), + name: ".*".into(), + default: Some(DefaultConfigItemType::Dynamic), + priority: Some(0), + alias: vec![], + experimental: false, + documentation: "".into(), + }; + assert_eq!(config.get(b"alias", b"something"), Some(&expected)); + + let expected = DefaultConfigItem { + section: "chgserver".into(), + name: "idletimeout".into(), + default: Some(DefaultConfigItemType::Primitive(3600.into())), + priority: None, + alias: vec![], + experimental: false, + documentation: "".into(), + }; + assert_eq!(config.get(b"chgserver", b"idletimeout"), Some(&expected)); + + let expected = DefaultConfigItem { + section: "cmdserver".into(), + name: "track-log".into(), + default: Some(DefaultConfigItemType::Lambda(vec![ + "chgserver".into(), + "cmdserver".into(), + "repocache".into(), + ])), + priority: None, + alias: vec![], + experimental: false, + documentation: "".into(), + }; + assert_eq!(config.get(b"cmdserver", b"track-log"), Some(&expected)); + + let expected = DefaultConfigItem { + section: "command-templates".into(), + name: "graphnode".into(), + default: None, + priority: None, + alias: vec![("ui".into(), "graphnodetemplate".into())], + experimental: false, + documentation: + "This is a docstring.\nThis is another line but this is not." + .into(), + }; + assert_eq!( + config.get(b"command-templates", b"graphnode"), + Some(&expected) + ); + } +} diff --git a/rust/hg-core/src/config/layer.rs b/rust/hg-core/src/config/layer.rs --- a/rust/hg-core/src/config/layer.rs +++ b/rust/hg-core/src/config/layer.rs @@ -304,8 +304,9 @@ pub enum ConfigOrigin { CommandLineColor, /// From environment variables like `$PAGER` or `$EDITOR` Environment(Vec), - /* TODO defaults (configitems.py) - * TODO extensions + /// From configitems.toml + Defaults, + /* TODO extensions * TODO Python resources? * Others? */ } @@ -323,6 +324,9 @@ impl DisplayBytes for ConfigOrigin { ConfigOrigin::Tweakdefaults => { write_bytes!(out, b"ui.tweakdefaults") } + ConfigOrigin::Defaults => { + write_bytes!(out, b"configitems.toml") + } } } } diff --git a/rust/hg-core/src/config/mod.rs b/rust/hg-core/src/config/mod.rs --- a/rust/hg-core/src/config/mod.rs +++ b/rust/hg-core/src/config/mod.rs @@ -9,14 +9,19 @@ //! Mercurial config parsing and interfaces. +pub mod config_items; mod layer; mod plain_info; mod values; pub use layer::{ConfigError, ConfigOrigin, ConfigParseError}; +use lazy_static::lazy_static; pub use plain_info::PlainInfo; +use self::config_items::DefaultConfig; +use self::config_items::DefaultConfigItem; use self::layer::ConfigLayer; use self::layer::ConfigValue; +use crate::errors::HgError; use crate::errors::{HgResultExt, IoResultExt}; use crate::utils::files::get_bytes_from_os_str; use format_bytes::{write_bytes, DisplayBytes}; @@ -26,6 +31,14 @@ use std::fmt; use std::path::{Path, PathBuf}; use std::str; +lazy_static! { + static ref DEFAULT_CONFIG: Result = { + DefaultConfig::from_contents(include_str!( + "../../../../mercurial/configitems.toml" + )) + }; +} + /// Holds the config values for the current repository /// TODO update this docstring once we support more sources #[derive(Clone)] @@ -347,13 +360,32 @@ impl Config { self.plain = plain; } + /// Returns the default value for the given config item, if any. + pub fn get_default( + &self, + section: &[u8], + item: &[u8], + ) -> Result, HgError> { + let default_config = DEFAULT_CONFIG.as_ref().map_err(|e| { + HgError::abort( + e.to_string(), + crate::exit_codes::ABORT, + Some("`mercurial/configitems.toml` is not valid".into()), + ) + })?; + Ok(default_config.get(section, item)) + } + fn get_parse<'config, T: 'config>( &'config self, section: &[u8], item: &[u8], expected_type: &'static str, parse: impl Fn(&'config [u8]) -> Option, - ) -> Result, ConfigValueParseError> { + ) -> Result, HgError> + where + Option: TryFrom<&'config DefaultConfigItem, Error = HgError>, + { match self.get_inner(section, item) { Some((layer, v)) => match parse(&v.bytes) { Some(b) => Ok(Some(b)), @@ -364,9 +396,15 @@ impl Config { section: section.to_owned(), item: item.to_owned(), expected_type, - })), + }) + .into()), }, - None => Ok(None), + None => match self.get_default(section, item)? { + Some(default) => Ok(default.try_into()?), + None => { + Ok(None) + } + }, } } @@ -376,7 +414,7 @@ impl Config { &self, section: &[u8], item: &[u8], - ) -> Result, ConfigValueParseError> { + ) -> Result, HgError> { self.get_parse(section, item, "ASCII or UTF-8 string", |value| { str::from_utf8(value).ok() }) @@ -388,7 +426,7 @@ impl Config { &self, section: &[u8], item: &[u8], - ) -> Result, ConfigValueParseError> { + ) -> Result, HgError> { self.get_parse(section, item, "valid integer", |value| { str::from_utf8(value).ok()?.parse().ok() }) @@ -401,7 +439,7 @@ impl Config { &self, section: &[u8], item: &[u8], - ) -> Result, ConfigValueParseError> { + ) -> Result, HgError> { self.get_parse(section, item, "byte quantity", values::parse_byte_size) } @@ -412,7 +450,7 @@ impl Config { &self, section: &[u8], item: &[u8], - ) -> Result, ConfigValueParseError> { + ) -> Result, HgError> { self.get_parse(section, item, "boolean", values::parse_bool) } @@ -422,7 +460,7 @@ impl Config { &self, section: &[u8], item: &[u8], - ) -> Result { + ) -> Result { Ok(self.get_option(section, item)?.unwrap_or(false)) } diff --git a/rust/rhg/src/commands/status.rs b/rust/rhg/src/commands/status.rs --- a/rust/rhg/src/commands/status.rs +++ b/rust/rhg/src/commands/status.rs @@ -7,7 +7,8 @@ use crate::error::CommandError; use crate::ui::{ - format_pattern_file_warning, print_narrow_sparse_warnings, Ui, + format_pattern_file_warning, print_narrow_sparse_warnings, relative_paths, + RelativePaths, Ui, }; use crate::utils::path_utils::RelativizePaths; use clap::Arg; @@ -360,13 +361,26 @@ pub fn run(invocation: &crate::CliInvoca } } } - let relative_paths = config + + let relative_status = config .get_option(b"commands", b"status.relative")? - .unwrap_or(config.get_bool(b"ui", b"relative-paths")?); + .expect("commands.status.relative should have a default value"); + + let relativize_paths = relative_status || { + // TODO should be dependent on whether patterns are passed once + // we support those. + // See in Python code with `getuipathfn` usage in `commands.py`. + let legacy_relative_behavior = false; + match relative_paths(invocation.config)? { + RelativePaths::Legacy => legacy_relative_behavior, + RelativePaths::Bool(v) => v, + } + }; + let output = DisplayStatusPaths { ui, no_status, - relativize: if relative_paths { + relativize: if relativize_paths { Some(RelativizePaths::new(repo)?) } else { None