rust: Parse system and user configuration
authorSimon Sapin <simon.sapin@octobus.net>
Thu, 04 Feb 2021 13:16:21 +0100
changeset 46483 2845892dd489
parent 46482 39128182f04e
child 46484 a6e4e4650bac
rust: Parse system and user configuration CLI `--config` argument parsing is still missing, as is per-repo config Differential Revision: https://phab.mercurial-scm.org/D9961
rust/Cargo.lock
rust/hg-core/Cargo.toml
rust/hg-core/src/config.rs
rust/hg-core/src/config/config.rs
rust/hg-core/src/config/layer.rs
rust/hg-core/src/errors.rs
rust/hg-core/src/utils.rs
--- a/rust/Cargo.lock	Mon Feb 01 13:32:00 2021 +0100
+++ b/rust/Cargo.lock	Thu Feb 04 13:16:21 2021 +0100
@@ -306,6 +306,7 @@
  "derive_more 0.99.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "flate2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)",
  "format-bytes 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "home 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "im-rc 15.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -337,6 +338,14 @@
 ]
 
 [[package]]
+name = "home"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
 name = "humantime"
 version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -963,6 +972,7 @@
 "checksum getrandom 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6"
 "checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 "checksum hermit-abi 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
+"checksum home 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654"
 "checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
 "checksum im-rc 15.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca8957e71f04a205cb162508f9326aea04676c8dfd0711220190d6b83664f3f"
 "checksum itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
--- a/rust/hg-core/Cargo.toml	Mon Feb 01 13:32:00 2021 +0100
+++ b/rust/hg-core/Cargo.toml	Thu Feb 04 13:16:21 2021 +0100
@@ -12,6 +12,7 @@
 bytes-cast = "0.1"
 byteorder = "1.3.4"
 derive_more = "0.99"
+home = "0.5"
 im-rc = "15.0.*"
 lazy_static = "1.4.0"
 memchr = "2.3.3"
--- a/rust/hg-core/src/config.rs	Mon Feb 01 13:32:00 2021 +0100
+++ b/rust/hg-core/src/config.rs	Thu Feb 04 13:16:21 2021 +0100
@@ -12,3 +12,4 @@
 mod config;
 mod layer;
 pub use config::Config;
+pub use layer::{ConfigError, ConfigParseError};
--- a/rust/hg-core/src/config/config.rs	Mon Feb 01 13:32:00 2021 +0100
+++ b/rust/hg-core/src/config/config.rs	Thu Feb 04 13:16:21 2021 +0100
@@ -11,8 +11,11 @@
 use crate::config::layer::{
     ConfigError, ConfigLayer, ConfigParseError, ConfigValue,
 };
-use std::path::PathBuf;
+use crate::utils::files::get_bytes_from_path;
+use std::env;
+use std::path::{Path, PathBuf};
 
+use crate::errors::{HgResultExt, IoResultExt};
 use crate::repo::Repo;
 
 /// Holds the config values for the current repository
@@ -50,6 +53,124 @@
 }
 
 impl Config {
+    /// Load system and user configuration from various files.
+    ///
+    /// This is also affected by some environment variables.
+    ///
+    /// TODO: add a parameter for `--config` CLI arguments
+    pub fn load() -> Result<Self, ConfigError> {
+        let mut config = Self { layers: Vec::new() };
+        let opt_rc_path = env::var_os("HGRCPATH");
+        // HGRCPATH replaces system config
+        if opt_rc_path.is_none() {
+            config.add_system_config()?
+        }
+        config.add_for_environment_variable("EDITOR", b"ui", b"editor");
+        config.add_for_environment_variable("VISUAL", b"ui", b"editor");
+        config.add_for_environment_variable("PAGER", b"pager", b"pager");
+        // HGRCPATH replaces user config
+        if opt_rc_path.is_none() {
+            config.add_user_config()?
+        }
+        if let Some(rc_path) = &opt_rc_path {
+            for path in env::split_paths(rc_path) {
+                if !path.as_os_str().is_empty() {
+                    if path.is_dir() {
+                        config.add_trusted_dir(&path)?
+                    } else {
+                        config.add_trusted_file(&path)?
+                    }
+                }
+            }
+        }
+        Ok(config)
+    }
+
+    fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
+        if let Some(entries) = std::fs::read_dir(path)
+            .for_file(path)
+            .io_not_found_as_none()?
+        {
+            for entry in entries {
+                let file_path = entry.for_file(path)?.path();
+                if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
+                    self.add_trusted_file(&file_path)?
+                }
+            }
+        }
+        Ok(())
+    }
+
+    fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
+        if let Some(data) =
+            std::fs::read(path).for_file(path).io_not_found_as_none()?
+        {
+            self.layers.extend(ConfigLayer::parse(path, &data)?)
+        }
+        Ok(())
+    }
+
+    fn add_for_environment_variable(
+        &mut self,
+        var: &str,
+        section: &[u8],
+        key: &[u8],
+    ) {
+        if let Some(value) = env::var_os(var) {
+            let origin = layer::ConfigOrigin::Environment(var.into());
+            let mut layer = ConfigLayer::new(origin);
+            layer.add(
+                section.to_owned(),
+                key.to_owned(),
+                // `value` is not a path but this works for any `OsStr`:
+                get_bytes_from_path(value),
+                None,
+            );
+            self.layers.push(layer)
+        }
+    }
+
+    #[cfg(unix)] // TODO: other platforms
+    fn add_system_config(&mut self) -> Result<(), ConfigError> {
+        let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
+            let etc = prefix.join("etc").join("mercurial");
+            self.add_trusted_file(&etc.join("hgrc"))?;
+            self.add_trusted_dir(&etc.join("hgrc.d"))
+        };
+        let root = Path::new("/");
+        // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
+        // instead? TODO: can this be a relative path?
+        let hg = crate::utils::current_exe()?;
+        // TODO: this order (per-installation then per-system) matches
+        // `systemrcpath()` in `mercurial/scmposix.py`, but
+        // `mercurial/helptext/config.txt` suggests it should be reversed
+        if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
+            if installation_prefix != root {
+                add_for_prefix(&installation_prefix)?
+            }
+        }
+        add_for_prefix(root)?;
+        Ok(())
+    }
+
+    #[cfg(unix)] // TODO: other plateforms
+    fn add_user_config(&mut self) -> Result<(), ConfigError> {
+        let opt_home = home::home_dir();
+        if let Some(home) = &opt_home {
+            self.add_trusted_file(&home.join(".hgrc"))?
+        }
+        let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
+        if !darwin {
+            if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
+                .map(PathBuf::from)
+                .or_else(|| opt_home.map(|home| home.join(".config")))
+            {
+                self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
+            }
+        }
+        Ok(())
+    }
+
     /// Loads in order, which means that the precedence is the same
     /// as the order of `sources`.
     pub fn load_from_explicit_sources(
--- a/rust/hg-core/src/config/layer.rs	Mon Feb 01 13:32:00 2021 +0100
+++ b/rust/hg-core/src/config/layer.rs	Thu Feb 04 13:16:21 2021 +0100
@@ -216,7 +216,7 @@
     pub fn to_bytes(&self) -> Vec<u8> {
         match self {
             ConfigOrigin::File(p) => get_bytes_from_path(p),
-            ConfigOrigin::Environment(e) => e.to_owned(),
+            ConfigOrigin::Environment(e) => format_bytes!(b"${}", e),
         }
     }
 }
--- a/rust/hg-core/src/errors.rs	Mon Feb 01 13:32:00 2021 +0100
+++ b/rust/hg-core/src/errors.rs	Thu Feb 04 13:16:21 2021 +0100
@@ -26,11 +26,13 @@
 /// Details about where an I/O error happened
 #[derive(Debug, derive_more::From)]
 pub enum IoErrorContext {
-    /// A filesystem operation returned `std::io::Error`
+    /// A filesystem operation for the given file
     #[from]
     File(std::path::PathBuf),
-    /// `std::env::current_dir` returned `std::io::Error`
+    /// `std::env::current_dir`
     CurrentDir,
+    /// `std::env::current_exe`
+    CurrentExe,
 }
 
 impl HgError {
@@ -69,6 +71,7 @@
         match self {
             IoErrorContext::File(path) => path.display().fmt(f),
             IoErrorContext::CurrentDir => f.write_str("current directory"),
+            IoErrorContext::CurrentExe => f.write_str("current executable"),
         }
     }
 }
--- a/rust/hg-core/src/utils.rs	Mon Feb 01 13:32:00 2021 +0100
+++ b/rust/hg-core/src/utils.rs	Thu Feb 04 13:16:21 2021 +0100
@@ -184,3 +184,10 @@
         context: IoErrorContext::CurrentDir,
     })
 }
+
+pub fn current_exe() -> Result<std::path::PathBuf, HgError> {
+    std::env::current_exe().map_err(|error| HgError::IoError {
+        error,
+        context: IoErrorContext::CurrentExe,
+    })
+}