rhg: Parse per-repository configuration
authorSimon Sapin <simon.sapin@octobus.net>
Thu, 04 Feb 2021 15:04:53 +0100
changeset 46486 d7685105e504
parent 46485 f031fe1c6ede
child 46494 d67732a4b58a
rhg: Parse per-repository configuration Differential Revision: https://phab.mercurial-scm.org/D9964
rust/hg-core/src/config/config.rs
rust/hg-core/src/config/layer.rs
rust/hg-core/src/repo.rs
rust/rhg/src/error.rs
--- a/rust/hg-core/src/config/config.rs	Thu Feb 04 14:29:47 2021 +0100
+++ b/rust/hg-core/src/config/config.rs	Thu Feb 04 15:04:53 2021 +0100
@@ -16,7 +16,6 @@
 use std::path::{Path, PathBuf};
 
 use crate::errors::{HgResultExt, IoResultExt};
-use crate::repo::Repo;
 
 /// Holds the config values for the current repository
 /// TODO update this docstring once we support more sources
@@ -196,12 +195,28 @@
         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_for_repo(repo: &Repo) -> Result<Self, ConfigError> {
-        Ok(Self::load_from_explicit_sources(vec![
-            ConfigSource::AbsPath(repo.hg_vfs().join("hgrc")),
-        ])?)
+    /// Loads the per-repository config into a new `Config` which is combined
+    /// with `self`.
+    pub(crate) fn combine_with_repo(
+        &self,
+        repo_config_files: &[PathBuf],
+    ) -> Result<Self, ConfigError> {
+        let (cli_layers, other_layers) = self
+            .layers
+            .iter()
+            .cloned()
+            .partition(ConfigLayer::is_from_command_line);
+
+        let mut repo_config = Self {
+            layers: other_layers,
+        };
+        for path in repo_config_files {
+            // TODO: check if this file should be trusted:
+            // `mercurial/ui.py:427`
+            repo_config.add_trusted_file(path)?;
+        }
+        repo_config.layers.extend(cli_layers);
+        Ok(repo_config)
     }
 
     /// Returns an `Err` if the first value found is not a valid boolean.
@@ -297,8 +312,6 @@
         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,
--- a/rust/hg-core/src/config/layer.rs	Thu Feb 04 14:29:47 2021 +0100
+++ b/rust/hg-core/src/config/layer.rs	Thu Feb 04 15:04:53 2021 +0100
@@ -51,6 +51,15 @@
         }
     }
 
+    /// Returns whether this layer comes from `--config` CLI arguments
+    pub(crate) fn is_from_command_line(&self) -> bool {
+        if let ConfigOrigin::CommandLine = self.origin {
+            true
+        } else {
+            false
+        }
+    }
+
     /// Add an entry to the config, overwriting the old one if already present.
     pub fn add(
         &mut self,
@@ -97,11 +106,13 @@
             if let Some(m) = INCLUDE_RE.captures(&bytes) {
                 let filename_bytes = &m[1];
                 // `Path::parent` only fails for the root directory,
-                // which `src` can’t be since we’ve managed to open it as a file.
+                // which `src` can’t be since we’ve managed to open it as a
+                // file.
                 let dir = src
                     .parent()
                     .expect("Path::parent fail on a file we’ve read");
-                // `Path::join` with an absolute argument correctly ignores the base path
+                // `Path::join` with an absolute argument correctly ignores the
+                // base path
                 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
                 let data = std::fs::read(&filename).for_file(&filename)?;
                 layers.push(current_layer);
@@ -200,9 +211,11 @@
 
 #[derive(Clone, Debug)]
 pub enum ConfigOrigin {
-    /// The value comes from a configuration file
+    /// From a configuration file
     File(PathBuf),
-    /// The value comes from the environment like `$PAGER` or `$EDITOR`
+    /// From a `--config` CLI argument
+    CommandLine,
+    /// From environment variables like `$PAGER` or `$EDITOR`
     Environment(Vec<u8>),
     /* TODO cli
      * TODO defaults (configitems.py)
@@ -216,6 +229,7 @@
     pub fn to_bytes(&self) -> Vec<u8> {
         match self {
             ConfigOrigin::File(p) => get_bytes_from_path(p),
+            ConfigOrigin::CommandLine => b"--config".to_vec(),
             ConfigOrigin::Environment(e) => format_bytes!(b"${}", e),
         }
     }
--- a/rust/hg-core/src/repo.rs	Thu Feb 04 14:29:47 2021 +0100
+++ b/rust/hg-core/src/repo.rs	Thu Feb 04 15:04:53 2021 +0100
@@ -1,4 +1,4 @@
-use crate::config::Config;
+use crate::config::{Config, ConfigError, ConfigParseError};
 use crate::errors::{HgError, IoResultExt};
 use crate::requirements;
 use crate::utils::files::get_path_from_bytes;
@@ -12,17 +12,29 @@
     dot_hg: PathBuf,
     store: PathBuf,
     requirements: HashSet<String>,
+    config: Config,
 }
 
 #[derive(Debug, derive_more::From)]
-pub enum RepoFindError {
-    NotFoundInCurrentDirectoryOrAncestors {
+pub enum RepoError {
+    NotFound {
         current_directory: PathBuf,
     },
     #[from]
+    ConfigParseError(ConfigParseError),
+    #[from]
     Other(HgError),
 }
 
+impl From<ConfigError> for RepoError {
+    fn from(error: ConfigError) -> Self {
+        match error {
+            ConfigError::Parse(error) => error.into(),
+            ConfigError::Other(error) => error.into(),
+        }
+    }
+}
+
 /// Filesystem access abstraction for the contents of a given "base" diretory
 #[derive(Clone, Copy)]
 pub(crate) struct Vfs<'a> {
@@ -32,7 +44,7 @@
 impl Repo {
     /// Search the current directory and its ancestores for a repository:
     /// a working directory that contains a `.hg` sub-directory.
-    pub fn find(config: &Config) -> Result<Self, RepoFindError> {
+    pub fn find(config: &Config) -> Result<Self, RepoError> {
         let current_directory = crate::utils::current_dir()?;
         // ancestors() is inclusive: it first yields `current_directory` as-is.
         for ancestor in current_directory.ancestors() {
@@ -40,18 +52,20 @@
                 return Ok(Self::new_at_path(ancestor.to_owned(), config)?);
             }
         }
-        Err(RepoFindError::NotFoundInCurrentDirectoryOrAncestors {
-            current_directory,
-        })
+        Err(RepoError::NotFound { current_directory })
     }
 
     /// To be called after checking that `.hg` is a sub-directory
     fn new_at_path(
         working_directory: PathBuf,
         config: &Config,
-    ) -> Result<Self, HgError> {
+    ) -> Result<Self, RepoError> {
         let dot_hg = working_directory.join(".hg");
 
+        let mut repo_config_files = Vec::new();
+        repo_config_files.push(dot_hg.join("hgrc"));
+        repo_config_files.push(dot_hg.join("hgrc-not-shared"));
+
         let hg_vfs = Vfs { base: &dot_hg };
         let mut reqs = requirements::load_if_exists(hg_vfs)?;
         let relative =
@@ -89,7 +103,8 @@
                 return Err(HgError::corrupted(format!(
                     ".hg/sharedpath points to nonexistent directory {}",
                     shared_path.display()
-                )));
+                ))
+                .into());
             }
 
             store_path = shared_path.join("store");
@@ -99,12 +114,15 @@
                     .contains(requirements::SHARESAFE_REQUIREMENT);
 
             if share_safe && !source_is_share_safe {
-                return Err(match config.get(b"safe-mismatch", b"source-not-safe") {
+                return Err(match config
+                    .get(b"safe-mismatch", b"source-not-safe")
+                {
                     Some(b"abort") | None => HgError::abort(
-                        "share source does not support share-safe requirement"
+                        "share source does not support share-safe requirement",
                     ),
-                    _ => HgError::unsupported("share-safe downgrade")
-                });
+                    _ => HgError::unsupported("share-safe downgrade"),
+                }
+                .into());
             } else if source_is_share_safe && !share_safe {
                 return Err(
                     match config.get(b"safe-mismatch", b"source-safe") {
@@ -113,16 +131,24 @@
                             functionality while the current share does not",
                         ),
                         _ => HgError::unsupported("share-safe upgrade"),
-                    },
+                    }
+                    .into(),
                 );
             }
+
+            if share_safe {
+                repo_config_files.insert(0, shared_path.join("hgrc"))
+            }
         }
 
+        let repo_config = config.combine_with_repo(&repo_config_files)?;
+
         let repo = Self {
             requirements: reqs,
             working_directory,
             store: store_path,
             dot_hg,
+            config: repo_config,
         };
 
         requirements::check(&repo)?;
@@ -138,6 +164,10 @@
         &self.requirements
     }
 
+    pub fn config(&self) -> &Config {
+        &self.config
+    }
+
     /// For accessing repository files (in `.hg`), except for the store
     /// (`.hg/store`).
     pub(crate) fn hg_vfs(&self) -> Vfs<'_> {
--- a/rust/rhg/src/error.rs	Thu Feb 04 14:29:47 2021 +0100
+++ b/rust/rhg/src/error.rs	Thu Feb 04 15:04:53 2021 +0100
@@ -3,7 +3,7 @@
 use format_bytes::format_bytes;
 use hg::config::{ConfigError, ConfigParseError};
 use hg::errors::HgError;
-use hg::repo::RepoFindError;
+use hg::repo::RepoError;
 use hg::revlog::revlog::RevlogError;
 use hg::utils::files::get_bytes_from_path;
 use std::convert::From;
@@ -51,18 +51,17 @@
     }
 }
 
-impl From<RepoFindError> for CommandError {
-    fn from(error: RepoFindError) -> Self {
+impl From<RepoError> for CommandError {
+    fn from(error: RepoError) -> Self {
         match error {
-            RepoFindError::NotFoundInCurrentDirectoryOrAncestors {
-                current_directory,
-            } => CommandError::Abort {
+            RepoError::NotFound { current_directory } => CommandError::Abort {
                 message: format_bytes!(
                     b"no repository found in '{}' (.hg not found)!",
                     get_bytes_from_path(current_directory)
                 ),
             },
-            RepoFindError::Other(error) => error.into(),
+            RepoError::ConfigParseError(error) => error.into(),
+            RepoError::Other(error) => error.into(),
         }
     }
 }
@@ -70,33 +69,35 @@
 impl From<ConfigError> for CommandError {
     fn from(error: ConfigError) -> Self {
         match error {
-            ConfigError::Parse(ConfigParseError {
-                origin,
-                line,
-                bytes,
-            }) => {
-                let line_message = if let Some(line_number) = line {
-                    format_bytes!(
-                        b" at line {}",
-                        line_number.to_string().into_bytes()
-                    )
-                } else {
-                    Vec::new()
-                };
-                CommandError::Abort {
-                    message: format_bytes!(
-                        b"config parse error in {}{}: '{}'",
-                        origin.to_bytes(),
-                        line_message,
-                        bytes
-                    ),
-                }
-            }
+            ConfigError::Parse(error) => error.into(),
             ConfigError::Other(error) => error.into(),
         }
     }
 }
 
+impl From<ConfigParseError> for CommandError {
+    fn from(error: ConfigParseError) -> Self {
+        let ConfigParseError {
+            origin,
+            line,
+            bytes,
+        } = error;
+        let line_message = if let Some(line_number) = line {
+            format_bytes!(b" at line {}", line_number.to_string().into_bytes())
+        } else {
+            Vec::new()
+        };
+        CommandError::Abort {
+            message: format_bytes!(
+                b"config parse error in {}{}: '{}'",
+                origin.to_bytes(),
+                line_message,
+                bytes
+            ),
+        }
+    }
+}
+
 impl From<(RevlogError, &str)> for CommandError {
     fn from((err, rev): (RevlogError, &str)) -> CommandError {
         match err {