changeset 50802:f8412da86d05

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`.
author Raphaël Gomès <rgomes@octobus.net>
date Thu, 06 Jul 2023 14:32:07 +0200
parents c51b178b0b7e
children 8ff187fbbfea
files rust/Cargo.lock rust/hg-core/Cargo.toml rust/hg-core/src/config/config.rs rust/hg-core/src/config/config_items.rs rust/hg-core/src/config/layer.rs rust/hg-core/src/config/mod.rs rust/rhg/src/commands/status.rs
diffstat 7 files changed, 835 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- a/rust/Cargo.lock	Mon Jan 23 18:08:11 2023 +0100
+++ b/rust/Cargo.lock	Thu Jul 06 14:32:07 2023 +0200
@@ -476,6 +476,12 @@
 
 [[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 @@
  "derive_more",
  "flate2",
  "format-bytes",
- "hashbrown",
+ "hashbrown 0.13.1",
  "home",
  "im-rc",
  "itertools",
@@ -535,9 +541,11 @@
  "regex",
  "same-file",
  "self_cell",
+ "serde",
  "sha-1 0.10.0",
  "tempfile",
  "thread_local",
+ "toml",
  "twox-hash",
  "zstd",
 ]
@@ -610,6 +618,16 @@
 ]
 
 [[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 @@
 ]
 
 [[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 @@
 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 @@
 
 [[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 @@
 ]
 
 [[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"
--- a/rust/hg-core/Cargo.toml	Mon Jan 23 18:08:11 2023 +0100
+++ b/rust/hg-core/Cargo.toml	Thu Jul 06 14:32:07 2023 +0200
@@ -26,10 +26,12 @@
 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 @@
 default-features = false
 
 [dev-dependencies]
-clap = { version = "4.0.24", features = ["derive"] }
+clap = { version = "~4.0", features = ["derive"] }
 pretty_assertions = "1.1.0"
--- a/rust/hg-core/src/config/config.rs	Mon Jan 23 18:08:11 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-core/src/config/config_items.rs	Thu Jul 06 14:32:07 2023 +0200
@@ -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<DefaultConfigItem>,
+    templates: FastHashMap<String, Vec<TemplateItem>>,
+    #[serde(rename = "template-applications")]
+    template_applications: Vec<TemplateApplication>,
+}
+
+/// 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<DefaultConfigItemType>,
+    /// 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 <pattern>, 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<isize>,
+    /// 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<toml::Value>,
+    #[serde(rename = "default-type")]
+    default_type: Option<String>,
+    #[serde(default)]
+    priority: isize,
+    #[serde(default)]
+    generic: bool,
+    #[serde(default)]
+    alias: Vec<(String, String)>,
+    #[serde(default)]
+    experimental: bool,
+    #[serde(default)]
+    documentation: String,
+}
+
+impl TryFrom<RawDefaultConfigItem> for DefaultConfigItem {
+    type Error = HgError;
+
+    fn try_from(value: RawDefaultConfigItem) -> Result<Self, Self::Error> {
+        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<Option<&'a str>, 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<bool> {
+    type Error = HgError;
+
+    fn try_from(value: &DefaultConfigItem) -> Result<Self, 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 '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<u32> {
+    type Error = HgError;
+
+    fn try_from(value: &DefaultConfigItem) -> Result<Self, 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 '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<u64> {
+    type Error = HgError;
+
+    fn try_from(value: &DefaultConfigItem) -> Result<Self, 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 '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<String>),
+    /// 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<DefaultConfigItemType>,
+    priority: Option<isize>,
+    #[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<toml::Value>,
+    #[serde(rename = "default-type")]
+    default_type: Option<String>,
+    #[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<RawTemplateItem> for TemplateItem {
+    type Error = HgError;
+
+    fn try_from(value: RawTemplateItem) -> Result<Self, Self::Error> {
+        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<String>,
+    default: Option<toml::Value>,
+) -> Result<Option<DefaultConfigItemType>, 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<String>,
+}
+
+/// 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<String, FastHashMap<String, DefaultConfigItem>>,
+}
+
+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<Self, HgError> {
+        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)
+        );
+    }
+}
--- a/rust/hg-core/src/config/layer.rs	Mon Jan 23 18:08:11 2023 +0100
+++ b/rust/hg-core/src/config/layer.rs	Thu Jul 06 14:32:07 2023 +0200
@@ -304,8 +304,9 @@
     CommandLineColor,
     /// From environment variables like `$PAGER` or `$EDITOR`
     Environment(Vec<u8>),
-    /* TODO defaults (configitems.py)
-     * TODO extensions
+    /// From configitems.toml
+    Defaults,
+    /* TODO extensions
      * TODO Python resources?
      * Others? */
 }
@@ -323,6 +324,9 @@
             ConfigOrigin::Tweakdefaults => {
                 write_bytes!(out, b"ui.tweakdefaults")
             }
+            ConfigOrigin::Defaults => {
+                write_bytes!(out, b"configitems.toml")
+            }
         }
     }
 }
--- a/rust/hg-core/src/config/mod.rs	Mon Jan 23 18:08:11 2023 +0100
+++ b/rust/hg-core/src/config/mod.rs	Thu Jul 06 14:32:07 2023 +0200
@@ -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::path::{Path, PathBuf};
 use std::str;
 
+lazy_static! {
+    static ref DEFAULT_CONFIG: Result<DefaultConfig, HgError> = {
+        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 @@
         self.plain = plain;
     }
 
+        /// Returns the default value for the given config item, if any.
+        pub fn get_default(
+            &self,
+            section: &[u8],
+            item: &[u8],
+        ) -> Result<Option<&DefaultConfigItem>, 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<T>,
-    ) -> Result<Option<T>, ConfigValueParseError> {
+    ) -> Result<Option<T>, HgError>
+    where
+        Option<T>: 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 @@
                     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 @@
         &self,
         section: &[u8],
         item: &[u8],
-    ) -> Result<Option<&str>, ConfigValueParseError> {
+    ) -> Result<Option<&str>, HgError> {
         self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
             str::from_utf8(value).ok()
         })
@@ -388,7 +426,7 @@
         &self,
         section: &[u8],
         item: &[u8],
-    ) -> Result<Option<u32>, ConfigValueParseError> {
+    ) -> Result<Option<u32>, HgError> {
         self.get_parse(section, item, "valid integer", |value| {
             str::from_utf8(value).ok()?.parse().ok()
         })
@@ -401,7 +439,7 @@
         &self,
         section: &[u8],
         item: &[u8],
-    ) -> Result<Option<u64>, ConfigValueParseError> {
+    ) -> Result<Option<u64>, HgError> {
         self.get_parse(section, item, "byte quantity", values::parse_byte_size)
     }
 
@@ -412,7 +450,7 @@
         &self,
         section: &[u8],
         item: &[u8],
-    ) -> Result<Option<bool>, ConfigValueParseError> {
+    ) -> Result<Option<bool>, HgError> {
         self.get_parse(section, item, "boolean", values::parse_bool)
     }
 
@@ -422,7 +460,7 @@
         &self,
         section: &[u8],
         item: &[u8],
-    ) -> Result<bool, ConfigValueParseError> {
+    ) -> Result<bool, HgError> {
         Ok(self.get_option(section, item)?.unwrap_or(false))
     }
 
--- a/rust/rhg/src/commands/status.rs	Mon Jan 23 18:08:11 2023 +0100
+++ b/rust/rhg/src/commands/status.rs	Thu Jul 06 14:32:07 2023 +0200
@@ -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 @@
                 }
             }
         }
-        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