diff rust/hg-core/src/config/config.rs @ 46187:95d6f31e88db

hg-core: add basic config module The config module exposes a `Config` struct, unused for now. It only reads the config file local to the repository, but handles all valid patterns and includes/unsets. It is structured in layers instead of erasing by reverse order of precedence, allowing us to transparently know more about the config for debugging purposes, and potentially other things I haven't thought about yet. This change also introduces `format_bytes!` to `hg-core`. Differential Revision: https://phab.mercurial-scm.org/D9408
author Rapha?l Gom?s <rgomes@octobus.net>
date Tue, 29 Dec 2020 10:53:45 +0100
parents
children 1dcd9c9975ed
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-core/src/config/config.rs	Tue Dec 29 10:53:45 2020 +0100
@@ -0,0 +1,197 @@
+// config.rs
+//
+// Copyright 2020
+//      Valentin Gatien-Baron,
+//      Raphaël Gomès <rgomes@octobus.net>
+//
+// This software may be used and distributed according to the terms of the
+// GNU General Public License version 2 or any later version.
+
+use super::layer;
+use crate::config::layer::{ConfigError, ConfigLayer, ConfigValue};
+use std::path::PathBuf;
+
+use crate::operations::find_root;
+use crate::utils::files::read_whole_file;
+
+/// Holds the config values for the current repository
+/// TODO update this docstring once we support more sources
+pub struct Config {
+    layers: Vec<layer::ConfigLayer>,
+}
+
+impl std::fmt::Debug for Config {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        for (index, layer) in self.layers.iter().rev().enumerate() {
+            write!(
+                f,
+                "==== Layer {} (trusted: {}) ====\n{:?}",
+                index, layer.trusted, layer
+            )?;
+        }
+        Ok(())
+    }
+}
+
+pub enum ConfigSource {
+    /// Absolute path to a config file
+    AbsPath(PathBuf),
+    /// Already parsed (from the CLI, env, Python resources, etc.)
+    Parsed(layer::ConfigLayer),
+}
+
+pub fn parse_bool(v: &[u8]) -> Option<bool> {
+    match v.to_ascii_lowercase().as_slice() {
+        b"1" | b"yes" | b"true" | b"on" | b"always" => Some(true),
+        b"0" | b"no" | b"false" | b"off" | b"never" => Some(false),
+        _ => None,
+    }
+}
+
+impl Config {
+    /// Loads in order, which means that the precedence is the same
+    /// as the order of `sources`.
+    pub fn load_from_explicit_sources(
+        sources: Vec<ConfigSource>,
+    ) -> Result<Self, ConfigError> {
+        let mut layers = vec![];
+
+        for source in sources.into_iter() {
+            match source {
+                ConfigSource::Parsed(c) => layers.push(c),
+                ConfigSource::AbsPath(c) => {
+                    // TODO check if it should be trusted
+                    // mercurial/ui.py:427
+                    let data = match read_whole_file(&c) {
+                        Err(_) => continue, // same as the python code
+                        Ok(data) => data,
+                    };
+                    layers.extend(ConfigLayer::parse(&c, &data)?)
+                }
+            }
+        }
+
+        Ok(Config { layers })
+    }
+
+    /// Loads the local config. In a future version, this will also load the
+    /// `$HOME/.hgrc` and more to mirror the Python implementation.
+    pub fn load() -> Result<Self, ConfigError> {
+        let root = find_root().unwrap();
+        Ok(Self::load_from_explicit_sources(vec![
+            ConfigSource::AbsPath(root.join(".hg/hgrc")),
+        ])?)
+    }
+
+    /// Returns an `Err` if the first value found is not a valid boolean.
+    /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
+    /// found, or `None`.
+    pub fn get_option(
+        &self,
+        section: &[u8],
+        item: &[u8],
+    ) -> Result<Option<bool>, ConfigError> {
+        match self.get_inner(&section, &item) {
+            Some((layer, v)) => match parse_bool(&v.bytes) {
+                Some(b) => Ok(Some(b)),
+                None => Err(ConfigError::Parse {
+                    origin: layer.origin.to_owned(),
+                    line: v.line,
+                    bytes: v.bytes.to_owned(),
+                }),
+            },
+            None => Ok(None),
+        }
+    }
+
+    /// Returns the corresponding boolean in the config. Returns `Ok(false)`
+    /// if the value is not found, an `Err` if it's not a valid boolean.
+    pub fn get_bool(
+        &self,
+        section: &[u8],
+        item: &[u8],
+    ) -> Result<bool, ConfigError> {
+        Ok(self.get_option(section, item)?.unwrap_or(false))
+    }
+
+    /// Returns the raw value bytes of the first one found, or `None`.
+    pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
+        self.get_inner(section, item)
+            .map(|(_, value)| value.bytes.as_ref())
+    }
+
+    /// Returns the layer and the value of the first one found, or `None`.
+    fn get_inner(
+        &self,
+        section: &[u8],
+        item: &[u8],
+    ) -> Option<(&ConfigLayer, &ConfigValue)> {
+        for layer in self.layers.iter().rev() {
+            if !layer.trusted {
+                continue;
+            }
+            if let Some(v) = layer.get(&section, &item) {
+                return Some((&layer, v));
+            }
+        }
+        None
+    }
+
+    /// Get raw values bytes from all layers (even untrusted ones) in order
+    /// of precedence.
+    #[cfg(test)]
+    fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
+        let mut res = vec![];
+        for layer in self.layers.iter().rev() {
+            if let Some(v) = layer.get(&section, &item) {
+                res.push(v.bytes.as_ref());
+            }
+        }
+        res
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use pretty_assertions::assert_eq;
+    use std::fs::File;
+    use std::io::Write;
+
+    #[test]
+    fn test_include_layer_ordering() {
+        let tmpdir = tempfile::tempdir().unwrap();
+        let tmpdir_path = tmpdir.path();
+        let mut included_file =
+            File::create(&tmpdir_path.join("included.rc")).unwrap();
+
+        included_file.write_all(b"[section]\nitem=value1").unwrap();
+        let base_config_path = tmpdir_path.join("base.rc");
+        let mut config_file = File::create(&base_config_path).unwrap();
+        let data =
+            b"[section]\nitem=value0\n%include included.rc\nitem=value2";
+        config_file.write_all(data).unwrap();
+
+        let sources = vec![ConfigSource::AbsPath(base_config_path)];
+        let config = Config::load_from_explicit_sources(sources)
+            .expect("expected valid config");
+
+        dbg!(&config);
+
+        let (_, value) = config.get_inner(b"section", b"item").unwrap();
+        assert_eq!(
+            value,
+            &ConfigValue {
+                bytes: b"value2".to_vec(),
+                line: Some(4)
+            }
+        );
+
+        let value = config.get(b"section", b"item").unwrap();
+        assert_eq!(value, b"value2",);
+        assert_eq!(
+            config.get_all(b"section", b"item"),
+            [b"value2", b"value1", b"value0"]
+        );
+    }
+}