Svetlin StefanovRefactor git_config tests.714ef5c4 years agoHistory
1713 lines · 62.3 KB · Rust
RawDownload
use std::{    borrow::Cow,    collections::{HashMap, VecDeque},    convert::TryFrom,    fmt::Display,    path::{Path, PathBuf},}; use crate::{    file::{        error::GitConfigError,        section::{MutableSection, SectionBody},        value::{EntryData, MutableMultiValue, MutableValue},        Index, Size,    },    parser,    parser::{        parse_from_bytes, parse_from_path, parse_from_str, Error, Event, Key, ParsedSectionHeader, Parser,        SectionHeaderName,    },    values,}; /// The section ID is a monotonically increasing ID used to refer to sections./// This value does not imply any ordering between sections, as new sections/// with higher section IDs may be in between lower ID sections.////// We need to use a section id because `git-config` permits sections with/// identical names. As a result, we can't simply use the section name as a key/// in a map.////// This id guaranteed to be unique, but not guaranteed to be compact. In other/// words, it's possible that a section may have an ID of 3 but the next section/// has an ID of 5.#[derive(PartialEq, Eq, Hash, Copy, Clone, PartialOrd, Ord, Debug)]pub(super) struct SectionId(usize); /// Internal data structure for the section id lookup tree used by/// [`GitConfig`]. Note that order in Vec matters as it represents the order/// of section ids with the matched section and name, and is used for precedence/// management.#[derive(PartialEq, Eq, Clone, Debug)]pub(super) enum LookupTreeNode<'a> {    Terminal(Vec<SectionId>),    NonTerminal(HashMap<Cow<'a, str>, Vec<SectionId>>),} /// High level `git-config` reader and writer.////// This is the full-featured implementation that can deserialize, serialize,/// and edit `git-config` files without loss of whitespace or comments. As a/// result, it's lot more complex than it's read-only variant,/// [`ResolvedGitConfig`] that exposes a [`HashMap`]-like interface. Users that/// only need to read `git-config` files should use that instead.////// Internally, this uses various acceleration data structures to improve/// performance of the typical usage behavior of many lookups and relatively/// fewer insertions.////// # Multivar behavior////// `git` is flexible enough to allow users to set a key multiple times in/// any number of identically named sections. When this is the case, the key/// is known as a "multivar". In this case, `get_raw_value` follows the/// "last one wins" approach that `git-config` internally uses for multivar/// resolution.////// Concretely, the following config has a multivar, `a`, with the values/// of `b`, `c`, and `d`, while `e` is a single variable with the value/// `f g h`.////// ```text/// [core]///     a = b///     a = c/// [core]///     a = d///     e = f g h/// ```////// Calling methods that fetch or set only one value (such as [`get_raw_value`])/// key `a` with the above config will fetch `d` or replace `d`, since the last/// valid config key/value pair is `a = d`:////// ```/// # use git_config::file::GitConfig;/// # use std::borrow::Cow;/// # use std::convert::TryFrom;/// # let git_config = GitConfig::try_from("[core]a=b\n[core]\na=c\na=d").unwrap();/// assert_eq!(git_config.get_raw_value("core", None, "a"), Ok(Cow::Borrowed("d".as_bytes())));/// ```////// Consider the `multi` variants of the methods instead, if you want to work/// with all values instead.////// [`ResolvedGitConfig`]: crate::file::ResolvedGitConfig/// [`get_raw_value`]: Self::get_raw_value#[derive(PartialEq, Eq, Clone, Debug, Default)]pub struct GitConfig<'event> {    /// The list of events that occur before an actual section. Since a    /// `git-config` file prohibits global values, this vec is limited to only    /// comment, newline, and whitespace events.    frontmatter_events: SectionBody<'event>,    /// Section name and subsection name to section id lookup tree. This is    /// effectively a n-tree (opposed to a binary tree) that can have a height    /// of at most three (including an implicit root node).    pub(super) section_lookup_tree: HashMap<SectionHeaderName<'event>, Vec<LookupTreeNode<'event>>>,    /// SectionId to section mapping. The value of this HashMap contains actual    /// events.    ///    /// This indirection with the SectionId as the key is critical to flexibly    /// supporting `git-config` sections, as duplicated keys are permitted.    pub(super) sections: HashMap<SectionId, SectionBody<'event>>,    section_headers: HashMap<SectionId, ParsedSectionHeader<'event>>,    /// Internal monotonically increasing counter for section ids.    section_id_counter: usize,    /// Section order for output ordering.    section_order: VecDeque<SectionId>,} pub mod from_paths {    use std::borrow::Cow;     use quick_error::quick_error;     use crate::{parser, values::path::interpolate};     quick_error! {        #[derive(Debug)]        /// The error returned by [`GitConfig::from_paths()`][super::GitConfig::from_paths()] and [`GitConfig::from_env_paths()`][super::GitConfig::from_env_paths()].        #[allow(missing_docs)]        pub enum Error {            ParserOrIoError(err: parser::ParserOrIoError<'static>) {                display("Could not read config")                source(err)                from()            }            Interpolate(err: interpolate::Error) {                display("Could not interpolate path")                source(err)                from()            }            IncludeDepthExceeded { max_depth: u8 } {                display("The maximum allowed length {} of the file include chain built by following nested includes is exceeded", max_depth)            }            MissingConfigPath {                display("Include paths from environment variables must not be relative.")            }        }    }     /// Options when loading git config using [`GitConfig::from_paths()`][super::GitConfig::from_paths()].    #[derive(Clone)]    pub struct Options<'a> {        /// The location where gitoxide or git is installed        pub git_install_dir: Option<Cow<'a, std::path::Path>>,        /// The maximum allowed length of the file include chain built by following nested includes where base level is depth = 0.        pub max_depth: u8,        /// When max depth is exceeded while following nested included, return an error if true or silently stop following        /// includes.        ///        /// Setting this value to false allows to read configuration with cycles, which otherwise always results in an error.        pub error_on_max_depth_exceeded: bool,    }     impl<'a> Default for Options<'a> {        fn default() -> Self {            Options {                git_install_dir: None,                max_depth: 10,                error_on_max_depth_exceeded: true,            }        }    }} pub mod from_env {    use quick_error::quick_error;     use super::from_paths;    use crate::values::path::interpolate;     quick_error! {        #[derive(Debug)]        /// Represents the errors that may occur when calling [`GitConfig::from_env`][crate::file::GitConfig::from_env()].        #[allow(missing_docs)]        pub enum Error {            ParseError (err: String) {                display("GIT_CONFIG_COUNT was not a positive integer: {}", err)            }            InvalidKeyId (key_id: usize) {                display("GIT_CONFIG_KEY_{} was not set.", key_id)            }            InvalidKeyValue (key_id: usize, key_val: String) {                display("GIT_CONFIG_KEY_{} was set to an invalid value: {}", key_id, key_val)            }            InvalidValueId (value_id: usize) {                display("GIT_CONFIG_VALUE_{} was not set.", value_id)            }            PathInterpolationError (err: interpolate::Error) {                display("Could not interpolate path while loading a config file.")                source(err)                from()            }            FromPathsError (err: from_paths::Error) {                display("Could not load config from a file")                source(err)                from()            }        }    }} impl<'event> GitConfig<'event> {    /// Constructs an empty `git-config` file.    #[inline]    #[must_use]    pub fn new() -> Self {        Self::default()    }     /// Constructs a `git-config` file from the provided path.    ///    /// # Errors    ///    /// Returns an error if there was an IO error or if the file wasn't a valid    /// git-config file.    #[inline]    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, parser::ParserOrIoError<'static>> {        parse_from_path(path).map(Self::from)    }     /// Constructs a `git-config` file from the provided paths in the order provided.    /// This is neither zero-copy nor zero-alloc.    ///    /// # Errors    ///    /// Returns an error if there was an IO error or if a file wasn't a valid    /// git-config file.    ///    /// [`git-config`'s documentation]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-FILES    #[inline]    pub fn from_paths(paths: Vec<PathBuf>, options: &from_paths::Options) -> Result<Self, from_paths::Error> {        let mut target = Self::new();        for path in paths {            let mut config = Self::open(&path)?;            config.resolve_includes(Some(&path), options)?;            target.append(config);        }        Ok(target)    }     // TODO: add note indicating that probably a lot if not all information about the original files is currently lost,    //       so can't be written back. This will probably change a lot during refactor, so it's not too important now.    fn append(&mut self, mut other: Self) {        let mut section_indices: Vec<_> = other.section_headers.keys().cloned().collect();        // header keys are numeric and ascend in insertion order, hence sorting them gives the order        // in which they appear in the config file.        section_indices.sort();        for section_index in section_indices {            let section_header = other.section_headers.remove(&section_index).expect("present");            self.push_section(                section_header.name.0,                section_header.subsection_name,                other.sections.remove(&section_index).expect("present"),            );        }    }     pub(crate) fn resolve_includes(        &mut self,        config_path: Option<&std::path::Path>,        options: &from_paths::Options,    ) -> Result<(), from_paths::Error> {        fn resolve_includes_recursive(            target_config: &mut GitConfig,            target_config_path: Option<&Path>,            depth: u8,            options: &from_paths::Options,        ) -> Result<(), from_paths::Error> {            if depth == options.max_depth {                return if options.error_on_max_depth_exceeded {                    Err(from_paths::Error::IncludeDepthExceeded {                        max_depth: options.max_depth,                    })                } else {                    Ok(())                };            }             let mut paths_to_include = Vec::new();            for path in target_config                .multi_value::<values::Path>("include", None, "path")                .unwrap_or_default()            {                let path = path.interpolate(options.git_install_dir.as_deref())?;                let path: PathBuf = if path.is_relative() {                    target_config_path                        .ok_or(from_paths::Error::MissingConfigPath)?                        .parent()                        .expect("path is a config file which naturally lives in a directory")                        .join(path)                } else {                    path.into()                };                 if path.is_file() {                    paths_to_include.push(path);                }            }             for config_path in paths_to_include {                let mut include_config = GitConfig::open(&config_path)?;                resolve_includes_recursive(&mut include_config, Some(&config_path), depth + 1, options)?;                target_config.append(include_config);            }            Ok(())        }         resolve_includes_recursive(self, config_path, 0, options)    }     /// Constructs a `git-config` from the default cascading sequence.    /// This is neither zero-alloc nor zero-copy.    ///    /// See <https://git-scm.com/docs/git-config#FILES> for details.    pub fn from_env_paths(options: &from_paths::Options) -> Result<Self, from_paths::Error> {        use std::env;         let mut paths = vec![];         if env::var("GIT_CONFIG_NO_SYSTEM").is_err() {            if let Ok(git_config_system) = env::var("GIT_CONFIG_SYSTEM") {                paths.push(PathBuf::from(git_config_system))            } else {                // In git the fallback is set to a build time macro which defaults to /etc/gitconfig                paths.push(PathBuf::from("/etc/gitconfig"));            }        }         if let Ok(git_config_global) = env::var("GIT_CONFIG_GLOBAL") {            paths.push(PathBuf::from(git_config_global));        } else {            // Divergence from git-config(1)            // These two are supposed to share the same scope and override            // rather than append according to git-config(1) documentation.            if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") {                paths.push(PathBuf::from(xdg_config_home).join("git/config"));            } else if let Ok(home) = env::var("HOME") {                paths.push(PathBuf::from(home).join(".config/git/config"));            }             if let Ok(home) = env::var("HOME") {                paths.push(PathBuf::from(home).join(".gitconfig"));            }        }         if let Ok(git_dir) = env::var("GIT_DIR") {            paths.push(PathBuf::from(git_dir).join("config"));        }         Self::from_paths(paths, options)    }     /// Generates a config from the environment variables. This is neither    /// zero-copy nor zero-alloc. See [`git-config`'s documentation] on    /// environment variable for more information.    ///    /// # Errors    ///    /// Returns an error if `GIT_CONFIG_COUNT` set and is not a number, or if    /// there was an invalid key value pair.    ///    /// [`git-config`'s documentation]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-GITCONFIGCOUNT    pub fn from_env(options: &from_paths::Options) -> Result<Option<Self>, from_env::Error> {        use std::env;        let count: usize = match env::var("GIT_CONFIG_COUNT") {            Ok(v) => v.parse().map_err(|_| from_env::Error::ParseError(v))?,            Err(_) => return Ok(None),        };         let mut config = Self::new();        for i in 0..count {            let key = env::var(format!("GIT_CONFIG_KEY_{}", i)).map_err(|_| from_env::Error::InvalidKeyId(i))?;            let value = env::var(format!("GIT_CONFIG_VALUE_{}", i)).map_err(|_| from_env::Error::InvalidValueId(i))?;            if let Some((section_name, maybe_subsection)) = key.split_once('.') {                let (subsection, key) = if let Some((subsection, key)) = maybe_subsection.rsplit_once('.') {                    (Some(subsection), key)                } else {                    (None, maybe_subsection)                };                 let mut section = if let Ok(section) = config.section_mut(section_name, subsection) {                    section                } else {                    // Need to have config own the section and subsection names                    // else they get dropped at the end of the loop.                    config.new_section(                        section_name.to_string(),                        subsection.map(|subsection| Cow::Owned(subsection.to_string())),                    )                };                 section.push(                    Cow::<str>::Owned(key.to_string()).into(),                    Cow::Owned(value.into_bytes()),                );            } else {                return Err(from_env::Error::InvalidKeyValue(i, key.to_string()));            }        }         // This occurs when `GIT_CONFIG_COUNT` is set to zero.        if config.is_empty() {            Ok(None)        } else {            config.resolve_includes(None, options)?;            Ok(Some(config))        }    }     /// Returns an interpreted value given a section, an optional subsection and    /// key.    ///    /// It's recommended to use one of the values in the [`values`] module as    /// the conversion is already implemented, but this function is flexible and    /// will accept any type that implements [`TryFrom<&[u8]>`][`TryFrom`].    ///    /// Consider [`Self::multi_value`] if you want to get all values of a    /// multivar instead.    ///    /// # Examples    ///    /// ```    /// # use git_config::file::{GitConfig, GitConfigError};    /// # use git_config::values::{Integer, Boolean};    /// # use std::borrow::Cow;    /// # use std::convert::TryFrom;    /// let config = r#"    ///     [core]    ///         a = 10k    ///         c    /// "#;    /// let git_config = GitConfig::try_from(config)?;    /// // You can either use the turbofish to determine the type...    /// let a_value = git_config.value::<Integer>("core", None, "a")?;    /// // ... or explicitly declare the type to avoid the turbofish    /// let c_value: Boolean = git_config.value("core", None, "c")?;    /// # Ok::<(), Box<dyn std::error::Error>>(())    /// ```    ///    /// # Errors    ///    /// This function will return an error if the key is not in the requested    /// section and subsection, if the section and subsection do not exist, or    /// if there was an issue converting the type into the requested variant.    ///    /// [`values`]: crate::values    /// [`TryFrom`]: std::convert::TryFrom    #[inline]    pub fn value<'lookup, T: TryFrom<Cow<'event, [u8]>>>(        &'event self,        section_name: &'lookup str,        subsection_name: Option<&'lookup str>,        key: &'lookup str,    ) -> Result<T, GitConfigError<'lookup>> {        T::try_from(self.get_raw_value(section_name, subsection_name, key)?)            .map_err(|_| GitConfigError::FailedConversion)    }     /// Returns all interpreted values given a section, an optional subsection    /// and key.    ///    /// It's recommended to use one of the values in the [`values`] module as    /// the conversion is already implemented, but this function is flexible and    /// will accept any type that implements [`TryFrom<&[u8]>`][`TryFrom`].    ///    /// Consider [`Self::value`] if you want to get a single value    /// (following last-one-wins resolution) instead.    ///    /// # Examples    ///    /// ```    /// # use git_config::file::{GitConfig, GitConfigError};    /// # use git_config::values::{Integer, Bytes, Boolean, TrueVariant};    /// # use std::borrow::Cow;    /// # use std::convert::TryFrom;    /// let config = r#"    ///     [core]    ///         a = true    ///         c = g    ///     [core]    ///         a    ///         a = false    /// "#;    /// let git_config = GitConfig::try_from(config).unwrap();    /// // You can either use the turbofish to determine the type...    /// let a_value = git_config.multi_value::<Boolean>("core", None, "a")?;    /// assert_eq!(    ///     a_value,

Lines 1–500 of 1713.