/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::path::PathBuf;
use std::time::Duration;

use minibytes::Text;
use util::path::expand_path;

use crate::Error;
use crate::Result;

pub trait FromConfigValue: Sized {
    fn try_from_str(s: &str) -> Result<Self>;
}

impl FromConfigValue for bool {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.to_lowercase();
        match value.as_ref() {
            "1" | "yes" | "true" | "on" | "always" => Ok(true),
            "0" | "no" | "false" | "off" | "never" => Ok(false),
            _ => Err(Error::Convert(format!("invalid bool: {}", value)).into()),
        }
    }
}

impl FromConfigValue for i8 {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.parse()?;
        Ok(value)
    }
}

impl FromConfigValue for i16 {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.parse()?;
        Ok(value)
    }
}

impl FromConfigValue for i32 {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.parse()?;
        Ok(value)
    }
}

impl FromConfigValue for i64 {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.parse()?;
        Ok(value)
    }
}

impl FromConfigValue for isize {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.parse()?;
        Ok(value)
    }
}

impl FromConfigValue for u8 {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.parse()?;
        Ok(value)
    }
}

impl FromConfigValue for u16 {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.parse()?;
        Ok(value)
    }
}

impl FromConfigValue for u32 {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.parse()?;
        Ok(value)
    }
}

impl FromConfigValue for u64 {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.parse()?;
        Ok(value)
    }
}

impl FromConfigValue for usize {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.parse()?;
        Ok(value)
    }
}

impl FromConfigValue for f32 {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.parse()?;
        Ok(value)
    }
}

impl FromConfigValue for f64 {
    fn try_from_str(s: &str) -> Result<Self> {
        let value = s.parse()?;
        Ok(value)
    }
}

impl FromConfigValue for String {
    fn try_from_str(s: &str) -> Result<Self> {
        Ok(s.to_string())
    }
}

/// Byte count specified with a unit. For example: `1.5 MB`.
#[derive(Copy, Clone, Default)]
pub struct ByteCount(u64);

impl ByteCount {
    /// Get the value of bytes. For example, `1K` has a value of `1024`.
    pub fn value(self) -> u64 {
        self.0
    }
}

impl From<u64> for ByteCount {
    fn from(value: u64) -> ByteCount {
        ByteCount(value)
    }
}

impl FromConfigValue for ByteCount {
    fn try_from_str(s: &str) -> Result<Self> {
        // This implementation matches mercurial/util.py:sizetoint
        let sizeunits = [
            ("kb", 1u64 << 10),
            ("mb", 1 << 20),
            ("gb", 1 << 30),
            ("tb", 1 << 40),
            ("k", 1 << 10),
            ("m", 1 << 20),
            ("g", 1 << 30),
            ("t", 1 << 40),
            ("b", 1),
            ("", 1),
        ];

        let value = s.to_lowercase();
        for (suffix, unit) in sizeunits.iter() {
            if value.ends_with(suffix) {
                let number_str: &str = value[..value.len() - suffix.len()].trim();
                let number: f64 = number_str.parse()?;
                if number < 0.0 {
                    return Err(Error::Convert(format!(
                        "byte size '{:?}' cannot be negative",
                        value
                    ))
                    .into());
                }
                let unit = *unit as f64;
                return Ok(ByteCount((number * unit) as u64));
            }
        }

        Err(Error::Convert(format!("'{:?}' cannot be parsed as a byte size", value)).into())
    }
}

impl FromConfigValue for PathBuf {
    fn try_from_str(s: &str) -> Result<Self> {
        Ok(expand_path(s))
    }
}

impl FromConfigValue for Duration {
    fn try_from_str(s: &str) -> Result<Self> {
        Ok(Duration::from_secs_f64(s.parse()?))
    }
}

impl<T: FromConfigValue> FromConfigValue for Vec<T> {
    fn try_from_str(s: &str) -> Result<Self> {
        let items = parse_list(s);
        items.into_iter().map(|s| T::try_from_str(&s)).collect()
    }
}

impl<T: FromConfigValue> FromConfigValue for Option<T> {
    fn try_from_str(s: &str) -> Result<Self> {
        T::try_from_str(s).map(Option::Some)
    }
}

/// Parse a configuration value as a list of comma/space separated strings.
/// It is ported from `mercurial.config.parselist`.
///
/// The function never complains about syntax and always returns some result.
///
/// Example:
///
/// ```
/// use configmodel::convert::parse_list;
///
/// assert_eq!(
///     parse_list("this,is \"a small\" ,test"),
///     vec!["this".to_string(), "is".to_string(), "a small".to_string(), "test".to_string()]
/// );
/// ```
pub fn parse_list<B: AsRef<str>>(value: B) -> Vec<Text> {
    let mut value = value.as_ref();

    while [" ", ",", "\n"].iter().any(|b| value.starts_with(b)) {
        value = &value[1..]
    }

    parse_list_internal(value)
        .into_iter()
        .map(Text::from)
        .collect()
}

fn parse_list_internal(value: &str) -> Vec<String> {
    // This code was translated verbatim from reliable Python code, so does not
    // use idiomatic Rust. Take great care in modifications.

    let mut value = value;

    value = value.trim_end_matches(|c| " ,\n".contains(c));

    if value.is_empty() {
        return Vec::new();
    }

    #[derive(Copy, Clone)]
    enum State {
        Plain,
        Quote,
    }

    let mut offset = 0;
    let mut parts: Vec<String> = vec![String::new()];
    let mut state = State::Plain;
    let value: Vec<char> = value.chars().collect();

    loop {
        match state {
            State::Plain => {
                let mut whitespace = false;
                while offset < value.len() && " \n\r\t,".contains(value[offset]) {
                    whitespace = true;
                    offset += 1;
                }
                if offset >= value.len() {
                    break;
                }
                if whitespace {
                    parts.push(Default::default());
                }
                if value[offset] == '"' {
                    let branch = {
                        match parts.last() {
                            None => 1,
                            Some(last) => {
                                if last.is_empty() {
                                    1
                                } else if last.ends_with('\\') {
                                    2
                                } else {
                                    3
                                }
                            }
                        }
                    }; // manual NLL, to drop reference on "parts".
                    if branch == 1 {
                        // last.is_empty()
                        state = State::Quote;
                        offset += 1;
                        continue;
                    } else if branch == 2 {
                        // last.ends_with(b"\\")
                        let last = parts.last_mut().unwrap();
                        last.pop();
                        last.push(value[offset]);
                        offset += 1;
                        continue;
                    }
                }
                let last = parts.last_mut().unwrap();
                last.push(value[offset]);
                offset += 1;
            }

            State::Quote => {
                if offset < value.len() && value[offset] == '"' {
                    parts.push(Default::default());
                    offset += 1;
                    while offset < value.len() && " \n\r\t,".contains(value[offset]) {
                        offset += 1;
                    }
                    state = State::Plain;
                    continue;
                }
                while offset < value.len() && value[offset] != '"' {
                    if value[offset] == '\\' && offset + 1 < value.len() && value[offset + 1] == '"'
                    {
                        offset += 1;
                        parts.last_mut().unwrap().push('"');
                    } else {
                        parts.last_mut().unwrap().push(value[offset]);
                    }
                    offset += 1;
                }
                if offset >= value.len() {
                    let mut real_parts: Vec<String> = parse_list_internal(parts.last().unwrap());
                    if real_parts.is_empty() {
                        parts.pop();
                        parts.push("\"".to_string());
                    } else {
                        real_parts[0].insert(0, '"');
                        parts.pop();
                        parts.append(&mut real_parts);
                    }
                    break;
                }
                offset += 1;
                while offset < value.len() && " ,".contains(value[offset]) {
                    offset += 1;
                }
                if offset < value.len() {
                    if offset + 1 == value.len() && value[offset] == '"' {
                        parts.last_mut().unwrap().push('"');
                        offset += 1;
                    } else {
                        parts.push(Default::default());
                    }
                } else {
                    break;
                }
                state = State::Plain;
            }
        }
    }

    parts
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_list() {
        fn b<B: AsRef<str>>(bytes: B) -> Text {
            Text::copy_from_slice(bytes.as_ref())
        }

        // From test-ui-config.py
        assert_eq!(parse_list("foo"), vec![b("foo")]);
        assert_eq!(
            parse_list("foo bar baz"),
            vec![b("foo"), b("bar"), b("baz")]
        );
        assert_eq!(parse_list("alice, bob"), vec![b("alice"), b("bob")]);
        assert_eq!(
            parse_list("foo bar baz alice, bob"),
            vec![b("foo"), b("bar"), b("baz"), b("alice"), b("bob")]
        );
        assert_eq!(
            parse_list("abc d\"ef\"g \"hij def\""),
            vec![b("abc"), b("d\"ef\"g"), b("hij def")]
        );
        assert_eq!(
            parse_list("\"hello world\", \"how are you?\""),
            vec![b("hello world"), b("how are you?")]
        );
        assert_eq!(
            parse_list("Do\"Not\"Separate"),
            vec![b("Do\"Not\"Separate")]
        );
        assert_eq!(parse_list("\"Do\"Separate"), vec![b("Do"), b("Separate")]);
        assert_eq!(
            parse_list("\"Do\\\"NotSeparate\""),
            vec![b("Do\"NotSeparate")]
        );
        assert_eq!(
            parse_list("string \"with extraneous\" quotation mark\""),
            vec![
                b("string"),
                b("with extraneous"),
                b("quotation"),
                b("mark\""),
            ]
        );
        assert_eq!(parse_list("x, y"), vec![b("x"), b("y")]);
        assert_eq!(parse_list("\"x\", \"y\""), vec![b("x"), b("y")]);
        assert_eq!(
            parse_list("\"\"\" key = \"x\", \"y\" \"\"\""),
            vec![b(""), b(" key = "), b("x\""), b("y"), b(""), b("\"")]
        );
        assert_eq!(parse_list(",,,,     "), Vec::<Text>::new());
        assert_eq!(
            parse_list("\" just with starting quotation"),
            vec![b("\""), b("just"), b("with"), b("starting"), b("quotation")]
        );
        assert_eq!(
            parse_list("\"longer quotation\" with \"no ending quotation"),
            vec![
                b("longer quotation"),
                b("with"),
                b("\"no"),
                b("ending"),
                b("quotation"),
            ]
        );
        assert_eq!(
            parse_list("this is \\\" \"not a quotation mark\""),
            vec![b("this"), b("is"), b("\""), b("not a quotation mark")]
        );
        assert_eq!(parse_list("\n \n\nding\ndong"), vec![b("ding"), b("dong")]);

        // Other manually written cases
        assert_eq!(parse_list("a,b,,c"), vec![b("a"), b("b"), b("c")]);
        assert_eq!(parse_list("a b  c"), vec![b("a"), b("b"), b("c")]);
        assert_eq!(
            parse_list(" , a , , b,  , c , "),
            vec![b("a"), b("b"), b("c")]
        );
        assert_eq!(parse_list("a,\"b,c\" d"), vec![b("a"), b("b,c"), b("d")]);
        assert_eq!(parse_list("a,\",c"), vec![b("a"), b("\""), b("c")]);
        assert_eq!(parse_list("a,\" c\" \""), vec![b("a"), b(" c\"")]);
        assert_eq!(
            parse_list("a,\" c\" \" d"),
            vec![b("a"), b(" c"), b("\""), b("d")]
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::ffi::CString;
use std::fmt;
use std::io;
use std::path::PathBuf;
use std::str;

use thiserror::Error;

/// The error type for parsing config files.
#[derive(Error, Debug)]
pub enum Error {
    /// Unable to convert to a type.
    #[error("{0}")]
    Convert(String),

    /// Unable to parse a file due to syntax.
    #[error("{0:?}:\n{1}")]
    ParseFile(PathBuf, String),

    /// Unable to parse a flag due to syntax.
    #[error("malformed --config option: '{0}' (use --config section.name=value)")]
    ParseFlag(String),

    /// Unable to read a file due to IO errors.
    #[error("{0:?}: {1}")]
    Io(PathBuf, #[source] io::Error),

    /// Config file contains invalid UTF-8.
    #[error("{0:?}: {1}")]
    Utf8(PathBuf, #[source] str::Utf8Error),

    #[error("{0:?}: {1}")]
    Utf8Path(CString, #[source] str::Utf8Error),

    #[error(transparent)]
    ParseInt(#[from] std::num::ParseIntError),

    #[error(transparent)]
    ParseFloat(#[from] std::num::ParseFloatError),

    #[error("{0}")]
    General(String),

    #[error("config {0}.{1} is not set")]
    NotSet(String, String),

    #[error("{0}")]
    Other(#[source] anyhow::Error),
}

impl From<String> for Error {
    fn from(s: String) -> Self {
        Self::General(s)
    }
}

#[derive(Error, Debug)]
pub struct Errors(pub Vec<Error>);

impl fmt::Display for Errors {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for error in self.0.iter() {
            write!(f, "{}\n", error)?;
        }
        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! # configmodel
//!
//! Provides a trait definition for config reading.

pub mod config;
pub mod convert;
pub mod error;

pub use config::Config;
pub use config::ConfigExt;
pub use config::ValueLocation;
pub use config::ValueSource;
pub use error::Error;
pub type Result<T> = std::result::Result<T, Error>;

// Re-export
pub use minibytes::Text;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Builtin hgrc templates

static GIT_RC: &str = r#"
[commands]
new-pull=true

[extensions]
commitcloud=!
github=
infinitepush=!
remotenames=
treemanifest=
autopullhoisthotfix=!

[commitcloud]
automigrate=false

[remotenames]
autopullhoistpattern=re:tags/\S+$
autopullpattern=re:^[A-Za-z0-9._/-]+/\S+$
disallowedto=^remote/
disallowhint=please don't specify 'remote/' prefix in remote bookmark's name
hoist=remote
publicheads=remote/master,remote/main
rename.default=remote
selectivepulldefault=main,master
selectivepull=true

[smartlog]
names=main,master
repos=remote/

[experimental]
copytrace=off

[tweakdefaults]
defaultdest=

%include /etc/mercurial/git_overrides.rc
%include %PROGRAMDATA%/Facebook/Mercurial/git_overrides.rc

# Don't add more config here. Add above the %includes.
# We trim everything starting from %include in tests.

"#;

pub(crate) fn get(name: &str) -> Option<&'static str> {
    if name == "builtin:git.rc" {
        if std::env::var("TESTTMP").is_ok() {
            Some(&GIT_RC[..GIT_RC.find("%include").unwrap_or(GIT_RC.len())])
        } else {
            Some(GIT_RC)
        }
    } else {
        None
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::borrow::Cow;
use std::collections::HashSet;
use std::fs;
use std::hash::Hash;
use std::path::Path;
use std::path::PathBuf;
use std::str;
use std::sync::Arc;

use configmodel::Config;
pub use configmodel::ValueLocation;
pub use configmodel::ValueSource;
use hgrc_parser::parse;
use hgrc_parser::Instruction;
use indexmap::IndexMap;
use indexmap::IndexSet;
use minibytes::Text;
use util::path::expand_path;

use crate::error::Error;

/// Collection of config sections loaded from various sources.
#[derive(Clone, Default)]
pub struct ConfigSet {
    name: Text,
    sections: IndexMap<Text, Section>,
    // Canonicalized files that were loaded, including files with errors
    files: Vec<PathBuf>,
    // Secondary, immutable config to try out if `sections` does not
    // contain the requested config.
    secondary: Option<Arc<dyn Config>>,
}

/// Internal representation of a config section.
#[derive(Clone, Default, Debug)]
struct Section {
    items: IndexMap<Text, Vec<ValueSource>>,
}

/// Options that affects config setting functions like `load_path`, `parse`,
/// and `set`.
#[derive(Clone, Default)]
pub struct Options {
    source: Text,
    filters: Vec<Arc<Box<dyn Fn(Text, Text, Option<Text>) -> Option<(Text, Text, Option<Text>)>>>>,
}

impl Config for ConfigSet {
    /// Get config names under a section, sorted by insertion order.
    ///
    /// keys("foo") returns keys in section "foo".
    fn keys(&self, section: &str) -> Vec<Text> {
        let self_keys = self
            .sections
            .get(section)
            .map(|section| section.items.keys().cloned().collect())
            .unwrap_or_default();
        if let Some(secondary) = &self.secondary {
            let secondary_keys = secondary.keys(section);
            let result = merge_cow_list(Cow::Owned(secondary_keys), Cow::Owned(self_keys));
            result.into_owned()
        } else {
            self_keys
        }
    }

    /// Get config value for a given config.
    /// Return `None` if the config item does not exist.
    /// Return `Some(None)` if the config is is unset.
    fn get_considering_unset(&self, section: &str, name: &str) -> Option<Option<Text>> {
        let self_value = (|| -> Option<Option<Text>> {
            let section = self.sections.get(section)?;
            let value_sources: &Vec<ValueSource> = section.items.get(name)?;
            let value = value_sources.last()?.value.clone();
            Some(value)
        })();
        if let (None, Some(secondary)) = (&self_value, &self.secondary) {
            return secondary.get_considering_unset(section, name);
        }
        self_value
    }

    /// Get config sections.
    fn sections(&self) -> Cow<[Text]> {
        let sections = self.sections.keys().cloned().collect();
        let self_sections: Cow<[Text]> = Cow::Owned(sections);
        if let Some(secondary) = &self.secondary {
            let secondary_sections = secondary.sections();
            merge_cow_list(secondary_sections, self_sections)
        } else {
            self_sections
        }
    }

    /// Get detailed sources of a given config, including overrides, and source information.
    /// The last item in the returned vector is the latest value that is considered effective.
    ///
    /// Return an emtpy vector if the config does not exist.
    fn get_sources(&self, section: &str, name: &str) -> Cow<[ValueSource]> {
        let self_sources: Cow<[ValueSource]> = match self
            .sections
            .get(section)
            .and_then(|section| section.items.get(name))
        {
            None => Cow::Owned(Vec::new()),
            Some(sources) => Cow::Borrowed(sources),
        };
        if let Some(secondary) = &self.secondary {
            let secondary_sources = secondary.get_sources(section, name);
            if secondary_sources.is_empty() {
                self_sources
            } else if self_sources.is_empty() {
                secondary_sources
            } else {
                let sources: Vec<ValueSource> = secondary_sources
                    .into_owned()
                    .into_iter()
                    .chain(self_sources.into_owned())
                    .collect();
                Cow::Owned(sources)
            }
        } else {
            self_sources
        }
    }

    /// Get on-disk files loaded for this `Config`.
    fn files(&self) -> Cow<[PathBuf]> {
        let self_files: Cow<[PathBuf]> = Cow::Borrowed(&self.files);
        if let Some(secondary) = &self.secondary {
            let secondary_files = secondary.files();
            // file load order: secondary first
            merge_cow_list(secondary_files, self_files)
        } else {
            self_files
        }
    }

    fn layer_name(&self) -> Text {
        if self.name.is_empty() {
            Text::from_static("ConfigSet")
        } else {
            self.name.clone()
        }
    }

    fn layers(&self) -> Vec<Arc<dyn Config>> {
        if let Some(secondary) = &self.secondary {
            secondary.layers()
        } else {
            Vec::new()
        }
    }
}

/// Merge two lists. Preserve order (a is before b). Remove duplicated items.
/// Assumes `a` and `b` do not have duplicated items respectively.
fn merge_cow_list<'a, T: Clone + Hash + Eq>(a: Cow<'a, [T]>, b: Cow<'a, [T]>) -> Cow<'a, [T]> {
    if a.is_empty() {
        b
    } else if b.is_empty() {
        a
    } else {
        let result: IndexSet<T> = a.into_owned().into_iter().chain(b.into_owned()).collect();
        let result: Vec<T> = result.into_iter().collect();
        Cow::Owned(result)
    }
}

impl ConfigSet {
    /// Return an empty `ConfigSet`.
    pub fn new() -> Self {
        Default::default()
    }

    /// Attach a secondary config as fallback for items missing from the
    /// main config.
    ///
    /// The secondary config is immutable, is cheaper to clone, and provides
    /// the layers information.
    ///
    /// If a secondary config was already attached, it will be completed
    /// replaced by this call.
    pub fn secondary(&mut self, secondary: Arc<dyn Config>) -> &mut Self {
        self.secondary = Some(secondary);
        self
    }

    /// Update the name of the `ConfigSet`.
    pub fn named(&mut self, name: &str) -> &mut Self {
        self.name = Text::copy_from_slice(name);
        self
    }

    /// Load config files at given path. The path is a file.
    ///
    /// If `path` is a directory, it is ignored.
    /// If `path` is a file, it will be loaded directly.
    ///
    /// A config file can use `%include` to load other paths (directories or files). They will
    /// be loaded recursively. Includes take effect in place, instead of deferred. For example,
    /// with the following two files:
    ///
    /// ```plain,ignore
    /// # This is 1.rc
    /// [section]
    /// x = 1
    /// %include 2.rc
    /// y = 2
    ///
    /// # This is 2.rc
    /// [section]
    /// x = 3
    /// y = 4
    /// ```
    ///
    /// After loading `1.rc`. `x` is set to 3 and `y` is set to 2.
    ///
    /// Loading a file that is already parsed or being parsed by this `load_path` call is ignored,
    /// to avoid infinite loop. A separate `load_path` call would not ignore files loaded by
    /// other `load_path` calls.
    ///
    /// Return a list of errors. An error pasing a file will stop that file from loading, without
    /// affecting other files.
    pub fn load_path<P: AsRef<Path>>(&mut self, path: P, opts: &Options) -> Vec<Error> {
        let mut visited = HashSet::new();
        let mut errors = Vec::new();
        self.load_file(path.as_ref(), opts, &mut visited, &mut errors);
        errors
    }

    /// Load content of an unnamed config file. The `ValueLocation`s of loaded config items will
    /// have an empty `path`.
    ///
    /// Return a list of errors.
    pub fn parse<B: Into<Text>>(&mut self, content: B, opts: &Options) -> Vec<Error> {
        let mut visited = HashSet::new();
        let mut errors = Vec::new();
        let buf = content.into();
        self.load_file_content(Path::new(""), buf, opts, &mut visited, &mut errors);
        errors
    }

    /// Set a config item directly. `section`, `name` locates the config. `value` is the new value.
    /// `source` is some annotation about who set it, ex. "reporc", "userrc", "--config", etc.
    pub fn set(
        &mut self,
        section: impl AsRef<str>,
        name: impl AsRef<str>,
        value: Option<impl AsRef<str>>,
        opts: &Options,
    ) {
        let section = Text::copy_from_slice(section.as_ref());
        let name = Text::copy_from_slice(name.as_ref());
        let value = value.map(|v| Text::copy_from_slice(v.as_ref()));
        self.set_internal(section, name, value, None, &opts)
    }

    fn set_internal(
        &mut self,
        section: Text,
        name: Text,
        value: Option<Text>,
        location: Option<ValueLocation>,
        opts: &Options,
    ) {
        if let Some((section, name, value)) = opts.filter(section, name, value) {
            self.sections
                .entry(section)
                .or_insert_with(Default::default)
                .items
                .entry(name)
                .or_insert_with(|| Vec::with_capacity(1))
                .push(ValueSource {
                    value,
                    location,
                    source: opts.source.clone(),
                })
        }
    }

    fn load_file(
        &mut self,
        path: &Path,
        opts: &Options,
        visited: &mut HashSet<PathBuf>,
        errors: &mut Vec<Error>,
    ) {
        if let Ok(path) = path.canonicalize() {
            let path = &path;
            debug_assert!(path.is_absolute());

            if !visited.insert(path.to_path_buf()) {
                // skip - visited before
                return;
            }

            self.files.push(path.to_path_buf());

            match fs::read_to_string(path) {
                Ok(mut text) => {
                    text.push('\n');
                    let text = Text::from(text);
                    self.load_file_content(path, text, opts, visited, errors);
                }
                Err(error) => errors.push(Error::Io(path.to_path_buf(), error)),
            }
        } else {
            // On Windows, a UNC path `\\?\C:\foo\.\x` will fail to canonicalize
            // because it contains `.`. That path can be constructed by using
            // `PathBuf::join` to concatenate a UNC path `\\?\C:\foo` with
            // a "normal" path `.\x`.
            // Try to fix it automatically by stripping the UNC prefix and retry
            // `canonicalize`. `C:\foo\.\x` would be canonicalized without errors.
            #[cfg(windows)]
            {
                if let Some(path_str) = path.to_str() {
                    if path_str.starts_with(r"\\?\") {
                        let path = Path::new(&path_str[4..]);
                        self.load_file(&path, opts, visited, errors);
                    }
                }
            }
        }

        // If `path.canonicalize` reports an error. It's usually the path cannot
        // be resolved (ex. does not exist). It is considered normal and is not
        // reported in `errors`.
    }

    fn load_file_content(
        &mut self,
        path: &Path,
        buf: Text,
        opts: &Options,
        visited: &mut HashSet<PathBuf>,
        errors: &mut Vec<Error>,
    ) {
        tracing::debug!(
            "load {} from path '{}' ({} bytes)",
            path.display(),
            opts.source.as_ref(),
            buf.len()
        );

        let shared_path = Arc::new(path.to_path_buf()); // use Arc to do shallow copy
        let skip_include = path.parent().is_none(); // skip handling %include if path is empty

        let insts = match parse(&buf) {
            Ok(insts) => insts,
            Err(error) => {
                return errors.push(Error::ParseFile(path.to_path_buf(), format!("{}", error)));
            }
        };

        for inst in insts {
            match inst {
                Instruction::SetConfig {
                    section,
                    name,
                    value,
                    span,
                } => {
                    let section = buf.slice_to_bytes(section);
                    let name = buf.slice_to_bytes(name);
                    let value = Some(buf.slice_to_bytes(&value));
                    let location = ValueLocation {
                        path: shared_path.clone(),
                        content: buf.clone(),
                        location: span,
                    };
                    self.set_internal(section, name, value, location.into(), opts);
                }
                Instruction::UnsetConfig {
                    section,
                    name,
                    span,
                } => {
                    let section = buf.slice_to_bytes(section);
                    let name = buf.slice_to_bytes(name);
                    let location = ValueLocation {
                        path: shared_path.clone(),
                        content: buf.clone(),
                        location: span,
                    };
                    self.set_internal(section.clone(), name, None, location.into(), opts);
                }
                Instruction::Include {
                    path: include_path,
                    span: _,
                } => {
                    if !skip_include {
                        if let Some(content) = crate::builtin::get(include_path) {
                            let text = Text::from(content);
                            let path = Path::new(include_path);
                            self.load_file_content(path, text, opts, visited, errors);
                        } else {
                            let full_include_path =
                                path.parent().unwrap().join(expand_path(include_path));
                            self.load_file(&full_include_path, opts, visited, errors);
                        }
                    }
                }
            }
        }
    }

    pub fn files(&self) -> &[PathBuf] {
        &self.files
    }

    pub fn to_string(&self) -> String {
        let mut result = String::new();

        for section in self.sections().iter() {
            result.push('[');
            result.push_str(section.as_ref());
            result.push_str("]\n");

            for key in self.keys(section).iter() {
                let value = self.get_considering_unset(section, key);
                #[cfg(test)]
                {
                    let values = self.get_sources(section, key);
                    assert_eq!(values.last().map(|v| v.value().clone()), value);
                }
                if let Some(value) = value {
                    if let Some(value) = value {
                        result.push_str(key);
                        result.push('=');
                        // When a newline delimited list is loaded, the whitespace around each
                        // entry is trimmed. In order for the serialized config to be parsable, we
                        // need some indentation after each newline. Since this whitespace will be
                        // stripped on load, it shouldn't hurt anything.
                        let value = value.replace("\n", "\n ");
                        result.push_str(&value);
                        result.push('\n');
                    } else {
                        // None indicates the value was unset.
                        result.push_str("%unset ");
                        result.push_str(key);
                        result.push('\n');
                    }
                }
            }

            result.push_str("\n");
        }

        result
    }

    /// Drop configs from sources that are outside `allowed_locations` or
    /// `allowed_configs`.
    ///
    /// This function is being removed but we need logging to understand its
    /// side-effect.
    pub fn ensure_location_supersets(
        &mut self,
        allowed_locations: Option<HashSet<&str>>,
        allowed_configs: Option<HashSet<(&str, &str)>>,
    ) {
        for (sname, section) in self.sections.iter_mut() {
            for (kname, values) in section.items.iter_mut() {
                let values_copy = values.clone();
                let mut removals = 0;
                // value with a larger index takes precedence.
                for (index, value) in values_copy.iter().enumerate() {
                    // Convert the index into the original index.
                    let index = index - removals;

                    // Get the filename of the value's rc location
                    let path: PathBuf = match value.location() {
                        None => continue,
                        Some((path, _)) => path,
                    };
                    let location: Option<String> = path
                        .file_name()
                        .and_then(|f| f.to_str())
                        .map(|s| s.to_string());
                    // If only certain locations are allowed, and this isn't one of them, remove
                    // it. If location is None, it came from inmemory, so don't filter it.
                    if let Some(location) = location {
                        if crate::builtin::get(location.as_str()).is_none()
                            && allowed_locations
                                .as_ref()
                                .map(|a| a.contains(location.as_str()))
                                == Some(false)
                            && allowed_configs
                                .as_ref()
                                .map(|a| a.contains(&(sname, kname)))
                                != Some(true)
                        {
                            tracing::trace!(
                                target: "configset::validate",
                                "dropping {}.{} set by {} ({})",
                                sname.as_ref(),
                                kname.as_ref(),
                                path.display().to_string(),
                                value
                                    .value()
                                    .as_ref()
                                    .map(|v| v.as_ref())
                                    .unwrap_or_default(),
                            );
                            values.remove(index);
                            removals += 1;
                            continue;
                        }
                    }
                }

                // If the removal changes the config, log it as mismatched.
                if let (Some(before_remove), Some(after_remove)) =
                    (values_copy.last(), values.last())
                {
                    if before_remove.value != after_remove.value {
                        let source = match before_remove.location() {
                            None => before_remove.source().to_string(),
                            Some(l) => l.0.display().to_string(),
                        };
                        tracing::info!(
                             target: "config_mismatch",
                             config=&format!("{sname}.{kname}"),
                             expected=after_remove.value.clone().unwrap_or_default().as_ref(),
                             actual=before_remove.value.clone().unwrap_or_default().as_ref(),
                             source=source,
                        );
                    }
                }
            }
        }
    }
}

impl Options {
    /// Create a default `Options`.
    pub fn new() -> Self {
        Self::default()
    }

    /// Append a filter. A filter can decide to ignore a config item, or change its section,
    /// config name, or even value. The filter function takes a tuple of `(section, name, value)`
    /// and outputs `None` to prevent inserting that value, or `Some((section, name, value))` to
    /// insert it with optionally different name or values.
    ///
    /// Filters inserted first will be executed first.
    pub fn append_filter(
        mut self,
        filter: Box<dyn Fn(Text, Text, Option<Text>) -> Option<(Text, Text, Option<Text>)>>,
    ) -> Self {
        self.filters.push(Arc::new(filter));
        self
    }

    /// Set `source` information. It is about who initialized the config loading.  For example,
    /// "user_hgrc" indicates it is from the user config file, "--config" indicates it is from the
    /// global "--config" command line flag, "env" indicates it is translated from an environment
    /// variable (ex.  "PAGER"), etc.
    pub fn source<B: Into<Text>>(mut self, source: B) -> Self {
        self.source = source.into();
        self
    }

    /// Pass `(section, name, value)` through chain of filters, yielding mutated
    /// result or `None`, if any filter returned `None`.
    pub fn filter(
        &self,
        section: Text,
        name: Text,
        value: Option<Text>,
    ) -> Option<(Text, Text, Option<Text>)> {
        self.filters
            .iter()
            .fold(Some((section, name, value)), move |acc, func| {
                acc.and_then(|(s, n, v)| func(s, n, v))
            })
    }
}

/// Convert a "source" string to an `Options`.
impl<S: Into<Text>> From<S> for Options {
    fn from(source: S) -> Options {
        Options::new().source(source.into())
    }
}

#[cfg(test)]
pub(crate) mod tests {
    use std::io::Write;

    use configmodel::ConfigExt;
    use tempdir::TempDir;

    use super::*;
    use crate::convert::ByteCount;

    #[test]
    fn test_empty() {
        let cfg = ConfigSet::new();
        assert!(cfg.sections().is_empty());
        assert!(cfg.keys("foo").is_empty());
        assert!(cfg.get("foo", "bar").is_none());
        assert!(cfg.get_sources("foo", "bar").is_empty());
    }

    #[test]
    fn test_set() {
        let mut cfg = ConfigSet::new();
        cfg.set("y", "b", Some("1"), &"set1".into());
        cfg.set("y", "b", Some("2"), &"set2".into());
        cfg.set("y", "a", Some("3"), &"set3".into());
        cfg.set("z", "p", Some("4"), &"set4".into());
        cfg.set("z", "p", None::<Text>, &"set5".into());
        assert_eq!(cfg.sections(), vec![Text::from("y"), Text::from("z")]);
        assert_eq!(cfg.keys("y"), vec![Text::from("b"), Text::from("a")]);
        assert_eq!(cfg.get("y", "b"), Some(Text::from("2")));
        assert_eq!(cfg.get("y", "a"), Some(Text::from("3")));
        assert_eq!(cfg.get("z", "p"), None);

        let sources = cfg.get_sources("z", "p");
        assert_eq!(sources.len(), 2);
        assert_eq!(sources[0].value(), &Some(Text::from("4")));
        assert_eq!(sources[1].value(), &None);
        assert_eq!(sources[0].source(), &"set4");
        assert_eq!(sources[1].source(), &"set5");
        assert_eq!(sources[0].location(), None);
        assert_eq!(sources[1].location(), None);
        assert_eq!(sources[1].file_content(), None);
    }

    #[test]
    fn test_keys() {
        let mut cfg = ConfigSet::new();
        cfg.set("foo", "other", Some(""), &"".into());
        cfg.set("foo", "bar", Some(""), &"".into());
        cfg.set("foo", "bar.baz", Some(""), &"".into());
        cfg.set("foo", "bar.qux", Some(""), &"".into());
        cfg.set("foo", "bar.qux.more", Some(""), &"".into());

        assert_eq!(
            cfg.keys("foo"),
            vec!["other", "bar", "bar.baz", "bar.qux", "bar.qux.more"]
        );
    }

    #[test]
    fn test_clone() {
        let mut cfg = ConfigSet::new();
        assert!(cfg.clone().sections().is_empty());
        cfg.set("x", "a", Some("1"), &"set1".into());
        assert_eq!(cfg.clone().sections(), vec![Text::from("x")]);
        assert_eq!(cfg.clone().get("x", "a"), Some("1".into()));
    }

    #[test]
    fn test_parse_basic() {
        let mut cfg = ConfigSet::new();
        cfg.parse(
            "[y]\n\
             a = 0\n\
             b=1\n\
             # override a to 2\n\
             a  =  2 \n\
             \n\
             [x]\n\
             m = this\n \
             value has\r\n \
             multi lines\n\
             ; comment again\n\
             n =\n",
            &"test_parse_basic".into(),
        );

        assert_eq!(cfg.sections(), vec![Text::from("y"), Text::from("x")]);
        assert_eq!(cfg.keys("y"), vec![Text::from("a"), Text::from("b")]);
        assert_eq!(cfg.keys("x"), vec![Text::from("m"), Text::from("n")]);

        assert_eq!(cfg.get("y", "a"), Some(Text::from("2")));
        assert_eq!(cfg.get("y", "b"), Some(Text::from("1")));
        assert_eq!(cfg.get("x", "n"), Some(Text::new()));
        assert_eq!(
            cfg.get("x", "m"),
            Some(Text::from(&"this\nvalue has\nmulti lines"[..]))
        );

        let sources = cfg.get_sources("y", "a");
        assert_eq!(sources.len(), 2);
        assert_eq!(sources[0].value(), &Some(Text::from("0")));
        assert_eq!(sources[1].value(), &Some(Text::from("2")));
        assert_eq!(sources[0].source(), &"test_parse_basic");
        assert_eq!(sources[1].source(), &"test_parse_basic");
        assert_eq!(sources[0].location().unwrap(), (PathBuf::new(), 8..9));
        assert_eq!(sources[1].location().unwrap(), (PathBuf::new(), 38..39));
        assert_eq!(sources[1].file_content().unwrap().len(), 100);
    }

    #[test]
    fn test_parse_spaces() {
        let mut cfg = ConfigSet::new();

        cfg.parse(
            "# space after section name\n\
             [a]    \n\
             # empty lines\n    \n\t\n\n\
             x=1\n\
             # space in config name\n\
             y y \t =2\n\
             # space in multi-line config value, with trailing spaces\n\
             z=\t \n 3 3 \n  \n  4  \n\t5  \n  \n\
             # empty values\n\
             e1 =\n\
             e2 = \n\
             e3 =\n  \n\
             \n\
             # space in section name\n\
             [ b c\t]\n\
             # space in unset\n\
             y y =\n\
             %unset  y y \n\
             # no space at EOF\n\
             x=4",
            &"".into(),
        );

        assert_eq!(cfg.get("a", "x"), Some("1".into()));
        assert_eq!(cfg.get("a", "y y"), Some("2".into()));
        assert_eq!(cfg.get("a", "z"), Some("\n3 3\n\n4\n5".into()));
        assert_eq!(cfg.get("a", "e1"), Some("".into()));
        assert_eq!(cfg.get("a", "e2"), Some("".into()));
        assert_eq!(cfg.get("a", "e3"), Some("".into()));
        assert_eq!(cfg.get("b c", "y y"), None);
        assert_eq!(cfg.get("b c", "x"), Some("4".into()));
    }

    #[test]
    fn test_corner_cases() {
        let mut cfg = ConfigSet::new();
        let errors = cfg.parse(
            "# section looks like a config assignment\n\
             [a=b]\n\
             # comments look like config assignments\n\
             # a = b\n\
             ; a = b\n\
             # multiple equal signs in a config assignment\n\
             c = d = e\n\
             #",
            &"".into(),
        );

        assert_eq!(format!("{:?}", errors), "[]");
        assert_eq!(cfg.get("a=b", "c"), Some("d = e".into()));
        assert_eq!(cfg.get("a=b", "a"), None);
        assert_eq!(cfg.get("a=b", "# a"), None);
        assert_eq!(cfg.get("a=b", "; a"), None);
    }

    #[test]
    fn test_parse_errors() {
        let mut cfg = ConfigSet::new();
        let errors = cfg.parse("=foo", &"test_parse_errors".into());
        assert_eq!(format!("{}", errors[0]), "\"\":\nline 1: empty config name");

        let errors = cfg.parse(" a=b", &"test_parse_errors".into());
        assert_eq!(
            format!("{}", errors[0]),
            "\"\":\nline 1: indented line is not part of a multi-line config"
        );

        let errors = cfg.parse("%unset =foo", &"test_parse_errors".into());
        assert_eq!(
            format!("{}", errors[0]),
            "\"\":\nline 1: config name cannot include '='"
        );

        let errors = cfg.parse("[", &"test_parse_errors".into());
        assert_eq!(
            format!("{}", errors[0]),
            "\"\":\nline 1: missing ']' for section header"
        );

        let errors = cfg.parse("[]", &"test_parse_errors".into());
        assert_eq!(
            format!("{}", errors[0]),
            "\"\":\nline 1: empty section name"
        );

        let errors = cfg.parse("[a]]", &"test_parse_errors".into());
        assert_eq!(
            format!("{}", errors[0]),
            "\"\":\nline 1: extra content after section header"
        );

        let errors = cfg.parse("# foo\n[y", &"test_parse_errors".into());
        assert_eq!(
            format!("{}", errors[0]),
            "\"\":\nline 2: missing ']' for section header"
        );

        let mut cfg = ConfigSet::new();
        let errors = cfg.parse("\n\n%unknown", &"test_parse_errors".into());
        assert_eq!(
            format!("{}", errors[0]),
            "\"\":\nline 3: unknown directive (expect '%include' or '%unset')"
        );

        let mut cfg = ConfigSet::new();
        let errors = cfg.parse("[section]\nabc", &"test_parse_errors".into());
        assert_eq!(
            format!("{}", errors[0]),
            "\"\":\nline 2: expect '[section]' or 'name = value'"
        );
    }

    #[test]
    fn test_parse_unset() {
        let mut cfg = ConfigSet::new();
        cfg.parse(
            "[x]\n\
             a = 1\n\
             %unset b\n\
             b = 2\n\
             %unset  a \n\
             c = 3\n\
             d = 4\n\
             [y]\n\
             %unset  c\n\
             [x]\n\
             %unset  d ",
            &"test_parse_unset".into(),
        );

        assert_eq!(cfg.get("x", "a"), None);
        assert_eq!(cfg.get("x", "b"), Some(Text::from("2")));
        assert_eq!(cfg.get("x", "c"), Some(Text::from("3")));
        assert_eq!(cfg.get("x", "d"), None);
        assert_eq!(cfg.get_considering_unset("x", "d"), Some(None));
        assert_eq!(cfg.get_considering_unset("x", "e"), None);

        let sources = cfg.get_sources("x", "a");
        assert_eq!(sources.len(), 2);
        assert_eq!(sources[0].location().unwrap(), (PathBuf::new(), 8..9));
        assert_eq!(sources[1].location().unwrap(), (PathBuf::new(), 33..34));
    }

    #[test]
    fn test_filters() {
        fn exclude_list_section_x(
            section: Text,
            name: Text,
            value: Option<Text>,
        ) -> Option<(Text, Text, Option<Text>)> {
            if section.as_ref() == "x" {
                None
            } else {
                Some((section, name, value))
            }
        }

        fn swap_name_value(
            section: Text,
            name: Text,
            value: Option<Text>,
        ) -> Option<(Text, Text, Option<Text>)> {
            Some((section, value.unwrap(), name.into()))
        }

        fn rename_section_to_z(
            _section: Text,
            name: Text,
            value: Option<Text>,
        ) -> Option<(Text, Text, Option<Text>)> {
            Some(("z".into(), name, value))
        }

        let mut cfg = ConfigSet::new();
        let opts = Options::new()
            .append_filter(Box::new(exclude_list_section_x))
            .append_filter(Box::new(swap_name_value))
            .append_filter(Box::new(rename_section_to_z));
        cfg.parse(
            "[x]\n\
             a=1\n\
             [y]\n\
             b=c",
            &opts,
        );
        assert_eq!(cfg.get("x", "a"), None);
        assert_eq!(cfg.get("y", "b"), None);
        assert_eq!(cfg.get("z", "c"), Some(Text::from("b")));
    }

    pub(crate) fn write_file(path: PathBuf, content: &str) {
        fs::create_dir_all(path.parent().unwrap()).unwrap();
        let mut f = fs::File::create(path).unwrap();
        f.write_all(content.as_bytes()).unwrap();
    }

    #[test]
    fn test_parse_include() {
        let dir = TempDir::new("test_parse_include").unwrap();
        write_file(
            dir.path().join("rootrc"),
            "[x]\n\
             b=1\n\
             a=1\n\
             %include dir/abc.rc\n\
             %include dir/y.rc\n\
             %include dir/loop.rc\n\
             %include b.rc\n\
             [y]\n\
             b=1\n\
             [x]\n\
             %unset f",
        );

        write_file(dir.path().join("dir/abc.rc"), "[x]\na=2\nb=2");
        write_file(dir.path().join("dir/y.rc"), "[y]\ny=1\n%include ../e.rc");
        write_file(dir.path().join("dir/loop.rc"), "%include ../rootrc");

        // Won't be loaded before it's not inside dir/ directly.
        write_file(dir.path().join("dir/unused/unused.rc"), "[unused]\na=1");

        // Won't be loaded before it does not have ".rc" extension.
        write_file(dir.path().join("dir/unusedrc"), "[unused]\na=1");

        // Will be loaded. `%include` shouldn't cause cycles.
        write_file(
            dir.path().join("b.rc"),
            "[x]\nb=4\n\
             %include dir/abc.rc\n\
             %include dir/y.rc\n\
             %include dir/loop.rc",
        );

        // Will be loaded. Shouldn't cause cycles.
        write_file(dir.path().join("e.rc"), "[x]\ne=e\n%include f.rc");
        write_file(
            dir.path().join("f.rc"),
            "[x]\nf=f\n%include e.rc\n%include rootrc",
        );

        let mut cfg = ConfigSet::new();
        let errors = cfg.load_path(dir.path().join("rootrc"), &"test_parse_include".into());
        assert!(errors.is_empty());

        assert_eq!(cfg.sections(), vec![Text::from("x"), Text::from("y")]);
        assert_eq!(
            cfg.keys("x"),
            vec![
                Text::from("b"),
                Text::from("a"),
                Text::from("e"),
                Text::from("f"),
            ]
        );
        assert_eq!(cfg.get("x", "a"), Some(Text::from("2")));
        assert_eq!(cfg.get("x", "b"), Some(Text::from("4")));
        assert_eq!(cfg.get("x", "e"), Some(Text::from("e")));
        assert_eq!(cfg.get("x", "f"), None);
        assert_eq!(cfg.get("y", "b"), Some(Text::from("1")));
    }

    #[test]
    fn test_parse_include_builtin() {
        let dir = TempDir::new("test_parse_include").unwrap();
        write_file(dir.path().join("rootrc"), "%include builtin:git.rc\n");

        let mut cfg = ConfigSet::new();
        let errors = cfg.load_path(
            dir.path().join("rootrc"),
            &"test_parse_include_builtin".into(),
        );
        assert!(errors.is_empty());

        assert_eq!(cfg.get("remotenames", "hoist"), Some(Text::from("remote")));
    }

    #[test]
    fn test_parse_include_expand() {
        use std::env;
        env::set_var("FOO", "f");

        let dir = TempDir::new("test_parse_include_expand").unwrap();
        write_file(
            dir.path().join("rootrc"),
            "%include ./${FOO}1/$FOO/3.rc\n\
             %include ./%FOO%2/%FOO%/4.rc\n",
        );

        write_file(dir.path().join("f1/f/3.rc"), "[x]\na=1\n");
        write_file(dir.path().join("f2/f/4.rc"), "[y]\nb=2\n");

        let mut cfg = ConfigSet::new();
        let errors = cfg.load_path(dir.path().join("rootrc"), &"include_expand".into());
        assert!(errors.is_empty());

        assert_eq!(cfg.get("x", "a"), Some(Text::from("1")));
        assert_eq!(cfg.get("y", "b"), Some(Text::from("2")));
    }

    #[test]
    fn test_named() {
        let mut cfg = ConfigSet::new();
        assert_eq!(cfg.layer_name(), "ConfigSet");
        cfg.named("foo");
        assert_eq!(cfg.layer_name(), "foo");
    }

    #[test]
    fn test_serialize() {
        let mut cfg = ConfigSet::new();
        let errors = cfg.parse(
            "[section_one]
normal=normal_value
space key=space value
newline=new \n line
unset_me=foo
%unset unset_me

[section_two]
empty=
list=value1,value2,value3
space_list=value1.a value1.b
    value2.a value2.b
",
            &"".into(),
        );
        assert!(errors.is_empty(), "cfg.parse had errors {:?}", errors);

        let serialized = cfg.to_string();
        assert_eq!(
            serialized,
            r#"[section_one]
normal=normal_value
space key=space value
newline=new
 line
%unset unset_me

[section_two]
empty=
list=value1,value2,value3
space_list=value1.a value1.b
 value2.a value2.b

"#
        );

        // Verify it round trips
        let mut cfg2 = ConfigSet::new();
        let errors = cfg2.parse(serialized, &"".into());
        assert!(errors.is_empty(), "cfg2.parse had errors {:?}", errors);
        assert_eq!(cfg.sections(), cfg2.sections());
    }

    #[test]
    fn test_allowed_locations() {
        let mut cfg = ConfigSet::new();

        fn set(
            cfg: &mut ConfigSet,
            section: &'static str,
            key: &'static str,
            value: &'static str,
            location: &'static str,
        ) {
            cfg.set_internal(
                Text::from_static(section),
                Text::from_static(key),
                Some(Text::from_static(value)),
                Some(ValueLocation {
                    path: Arc::new(Path::new(location).to_owned()),
                    content: Text::from_static(""),
                    location: 0..1,
                }),
                &Options::new().source(Text::from_static("source")),
            );
        }

        set(&mut cfg, "section1", "key1", "value1", "subset1");
        set(&mut cfg, "section2", "key2", "value2", "subset2");

        let mut allow_list = HashSet::new();
        allow_list.insert("subset1");

        cfg.ensure_location_supersets(Some(allow_list.clone()), None);
        assert_eq!(
            cfg.get("section1", "key1"),
            Some(Text::from_static("value1"))
        );
        assert_eq!(cfg.get("section2", "key2"), None);

        // Check that allow_configs allows the config through, even if allow_locations did not.
        let mut allow_configs = HashSet::new();
        allow_configs.insert(("section2", "key2"));

        set(&mut cfg, "section2", "key2", "value2", "subset2");
        cfg.ensure_location_supersets(Some(allow_list), Some(allow_configs));
        assert_eq!(
            cfg.get("section1", "key1"),
            Some(Text::from_static("value1"))
        );
        assert_eq!(
            cfg.get("section2", "key2"),
            Some(Text::from_static("value2"))
        );
    }

    #[test]
    fn test_secondary() {
        let mut cfg1 = ConfigSet::new();
        let mut cfg2 = ConfigSet::new();

        cfg1.parse(
            r#"[b]
x = 1
[d]
y = 1
%unset x
[a]
x = 1
y = 1
"#,
            &"test1".into(),
        );
        cfg2.parse(
            r#"[a]
z = 2
x = 2
[d]
x = 2
%unset z
%unset y
"#,
            &"test2".into(),
        );

        let mut config = cfg1.clone();
        config.secondary(Arc::new(cfg2));

        // section order: a, d (cfg2), b
        // name order: a.z, a.x (cfg2), a.y (cfg1); d.x, d.z, d.y (cfg2); b.x (cfg1)
        // override: cfg1 overrides cfg2; d.x, d.y, a.x
        // %unset in cfg1 and cfg2 is preserved
        assert_eq!(
            config.to_string(),
            "[a]\nz=2\nx=1\ny=1\n\n[d]\n%unset x\n%unset z\ny=1\n\n[b]\nx=1\n\n"
        );

        assert_eq!(config.sections().into_owned(), ["a", "d", "b"]);
        assert_eq!(config.keys("a"), ["z", "x", "y"]);
        assert_eq!(config.keys("d"), ["x", "z", "y"]);
        assert_eq!(config.keys("b"), ["x"]);
        assert_eq!(config.get("a", "x").unwrap(), "1");
        assert_eq!(config.get("d", "x"), None);
        assert_eq!(config.get("d", "y").unwrap(), "1");
        assert_eq!(config.get("d", "k"), None);
        assert_eq!(config.get_considering_unset("d", "x"), Some(None));
        assert_eq!(config.get_considering_unset("d", "k"), None);
        assert_eq!(
            config
                .get_sources("a", "x")
                .iter()
                .map(|s| s.source.to_string())
                .collect::<Vec<_>>(),
            ["test2", "test1"]
        );
    }

    #[test]
    fn test_verifier_removal() {
        let mut cfg = ConfigSet::new();

        fn set(
            cfg: &mut ConfigSet,
            section: &'static str,
            key: &'static str,
            value: &'static str,
            location: &'static str,
        ) {
            cfg.set_internal(
                Text::from_static(section),
                Text::from_static(key),
                Some(Text::from_static(value)),
                Some(ValueLocation {
                    path: Arc::new(Path::new(location).to_owned()),
                    content: Text::from_static(""),
                    location: 0..1,
                }),
                &Options::new().source(Text::from_static("source")),
            );
        }

        // This test verifies that allowed location removal and subset removal interact nicely
        // together.
        set(&mut cfg, "section", "key", "value", "subset");
        set(&mut cfg, "section", "key", "value2", "super");

        let mut allowed_locations = HashSet::new();
        allowed_locations.insert("super");

        cfg.ensure_location_supersets(Some(allowed_locations), None);
    }

    #[test]
    fn test_get_or() {
        let mut cfg = ConfigSet::new();
        cfg.parse(
            "[foo]\n\
             bool1 = yes\n\
             bool2 = unknown\n\
             bools = 1, TRUE, On, aLwAys, 0, false, oFF, never\n\
             int1 = -33\n\
             list1 = x y z\n\
             list3 = 2, 3, 1\n\
             byte1 = 1.5 KB\n\
             byte2 = 500\n\
             byte3 = 0.125M\n\
             float = 1.42\n\
             ",
            &"test".into(),
        );

        assert_eq!(cfg.get_or("foo", "bar", || 3).unwrap(), 3);
        assert_eq!(cfg.get_or("foo", "bool1", || false).unwrap(), true);
        assert_eq!(
            format!("{}", cfg.get_or("foo", "bool2", || true).unwrap_err()),
            "invalid bool: unknown"
        );
        assert_eq!(cfg.get_or("foo", "int1", || 42).unwrap(), -33);
        assert_eq!(
            cfg.get_or("foo", "list1", || vec!["x".to_string()])
                .unwrap(),
            vec!["x", "y", "z"]
        );
        assert_eq!(
            cfg.get_or("foo", "list3", || vec![0]).unwrap(),
            vec![2, 3, 1]
        );

        assert_eq!(cfg.get_or_default::<bool>("foo", "bool1").unwrap(), true);
        assert_eq!(
            cfg.get_or_default::<Vec<bool>>("foo", "bools").unwrap(),
            vec![true, true, true, true, false, false, false, false]
        );

        assert_eq!(
            cfg.get_or_default::<ByteCount>("foo", "byte1")
                .unwrap()
                .value(),
            1536
        );
        assert_eq!(
            cfg.get_or_default::<ByteCount>("foo", "byte2")
                .unwrap()
                .value(),
            500
        );
        assert_eq!(
            cfg.get_or_default::<ByteCount>("foo", "byte3")
                .unwrap()
                .value(),
            131072
        );
        assert_eq!(
            cfg.get_or("foo", "missing", || ByteCount::from(3))
                .unwrap()
                .value(),
            3
        );
        assert_eq!(cfg.get_or("foo", "float", || 42f32).unwrap(), 1.42f32);
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! # ConfigSet
//!
//! `ConfigSet` provides a mutable Config abstraction that has the ability to
//! load files in hgrc-format.
//!
//! If you only need to take a config object and read values from it, use
//! `configmodel::Config` and `configmodel::ConfigExt` instead.
//!
//! ## Features
//!
//! - Parse valid hgrc-like config files efficiently.
//! - Track source locations of config values. Keep multiple locations of
//!   a same config if it is overridden.
//!
//! ## Config Format
//!
//! hgrc files are similar to INI files:
//!
//! ```plain,ignore
//! [section1]
//! name1 = value1
//! name2 = value2
//!
//! [section2]
//! name3 = value3
//!
//! ; This is a comment.
//! # This is also a comment.
//! ```
//!
//! But with some additional features.
//!
//! ### Include other config files
//!
//! Use `%include` to include other config files:
//!
//! ```plain,ignore
//! %include path/to/another/hgrc
//! %include path/to/another/hgrc.d
//! ```
//!
//! The include path is relative to the directory of the current config
//! file being parsed. If it's a directory, files with names ending
//! with `.rc` in it will be read.
//!
//! ### Unset a config
//!
//! Use `%unset` to unset a config:
//!
//! ```plain,ignore
//! [section]
//! %unset name1
//! ```
//!
//! ### Multi-line values
//!
//! Indent non-first lines with a space:
//!
//! ```plain,ignore
//! [section]
//! name1 = value
//!  line2
//!  line3
//! ```

mod builtin;
pub mod config;

pub use configmodel;
pub use configmodel::convert;
pub use configmodel::error;
pub use configmodel::Config;
pub use configmodel::Error;
pub use configmodel::Result;
pub use error::Errors;
// Re-export
pub use minibytes::Text;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

extern crate proc_macro;
use hgrc_parser::Instruction;
use indexmap::IndexMap;
use proc_macro::TokenStream;
use proc_macro::TokenTree;

/// Generate `StaticConfig` from a static string in rc format:
///
/// ```ignore
/// static_rc!(
/// r#"
/// [section]
/// name = value
/// "#
/// )
/// ```
#[proc_macro]
pub fn static_rc(tokens: TokenStream) -> TokenStream {
    // Extract.
    let content: String = match extract_string_literal(tokens.clone()) {
        Some(content) => content,
        None => panic!(
            "static_rc requires a single string literal, got: {:?}",
            tokens
        ),
    };

    // Parse hgrc.
    let mut items: Vec<(&str, &str, Option<String>)> = Vec::new();
    for inst in hgrc_parser::parse(&content).expect("parse static_rc!") {
        match inst {
            Instruction::SetConfig {
                section,
                name,
                value,
                ..
            } => {
                items.push((section, name, Some(value.to_string())));
            }
            Instruction::UnsetConfig { section, name, .. } => {
                items.push((section, name, None));
            }
            Instruction::Include { .. } => {
                panic!("static_rc! does not support %include");
            }
        }
    }

    static_config_from_items(&items)
}

fn extract_string_literal(tokens: TokenStream) -> Option<String> {
    let mut result: Option<String> = None;
    for token in tokens {
        if result.is_some() {
            return None;
        }
        match token {
            TokenTree::Literal(lit) => {
                // Extract the string out. Note public APIs of "Literal" only provides a way to get
                // the content with surrounding " " or r#" "#. Use a naive approach to strip out
                // the " ".
                let quoted = lit.to_string();
                let content = quoted.splitn(2, '"').nth(1)?.rsplitn(2, '"').nth(1)?;
                let content = if quoted.starts_with('r') {
                    content.to_string()
                } else {
                    // Handle escapes naively.
                    content.replace(r"\n", "\n")
                };
                result = Some(content);
                continue;
            }
            TokenTree::Group(group) => {
                result = extract_string_literal(group.stream());
            }
            _ => {}
        }
    }
    result
}

/// Generate `StaticConfig` from a static string in rc format:
///
/// ```ignore
/// static_items![
///     ("section1", "name1", "value1"),
///     ("section1", "name2", "value2"),
/// ]
/// ```
#[proc_macro]
pub fn static_items(tokens: TokenStream) -> TokenStream {
    let mut items: Vec<(String, String, String)> = Vec::new();
    for token in tokens {
        if let TokenTree::Group(group) = token {
            let tokens: Vec<_> = group.stream().into_iter().collect();
            if let [section, _comma1, name, _comma2, value] = &tokens[..] {
                let section = extract_string_literal(section.clone().into()).expect("section");
                let name = extract_string_literal(name.clone().into()).expect("name");
                let value = extract_string_literal(value.clone().into()).expect("value");
                items.push((section, name, value));
            }
        }
    }
    let items: Vec<(&str, &str, Option<String>)> = items
        .iter()
        .map(|v| (v.0.as_str(), v.1.as_str(), Some(v.2.clone())))
        .collect();
    static_config_from_items(&items)
}

/// Generate code for `StaticConfig` for a list of `(section, name, value)`.
/// A `None` `value` means `%unset`. The order of the list is preserved in
/// APIs like `sections()` and `keys()`.
fn static_config_from_items(items: &[(&str, &str, Option<String>)]) -> TokenStream {
    let mut sections: IndexMap<&str, IndexMap<&str, Option<String>>> = IndexMap::new();
    for (section, name, value) in items {
        sections
            .entry(section)
            .or_default()
            .insert(name, value.clone());
    }

    // Generate code. Looks like:
    //
    // {
    //      use staticconfig::phf;
    //      // Workaround nested map. See https://github.com/rust-phf/rust-phf/issues/183
    //      const SECTION1 = phf::phf_ordered_map! {
    //          "name1" => Some("value1"),
    //          "name2" => None, // %unset
    //      };
    //      const SECTION2 = phf::phf_ordered_map! {
    //          ...
    //      };
    //      ...
    //      const SECTIONS = phf::phf_ordered_map! {
    //          "section1" => SECTION1,
    //          "section2" => SECTION2,
    //          ...
    //      };
    //      staticconfig::StaticConfig {
    //          name: "StaticConfig",
    //          sections: SECTIONS,
    //      }
    // }
    let mut code = "{ use staticconfig::phf;\n".to_string();
    for (i, (_section, items)) in sections.iter().enumerate() {
        code += &format!(
            "const SECTION{}: phf::OrderedMap<&'static str, Option<&'static str>> = phf::phf_ordered_map! {{\n",
            i
        );
        for (name, value) in items.iter() {
            code += &format!("    {:?} => {:?},\n", name, value);
        }
        code += "};\n";
    }
    code += "const SECTIONS: phf::OrderedMap<&'static str, phf::OrderedMap<&'static str, Option<&'static str>>> = phf::phf_ordered_map! {\n";
    for (i, (section, _items)) in sections.iter().enumerate() {
        code += &format!("    {:?} => SECTION{},\n", section, i);
    }
    code += "};\n";
    code += r#"staticconfig::StaticConfig::from_macro_rules(SECTIONS) }"#;
    code.parse().unwrap()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::borrow::Cow;

use configmodel::Config;
use configmodel::Text;
use configmodel::ValueSource;
pub use phf;
use phf::OrderedMap;
pub use staticconfig_macros as _detail;

/// Define a `StaticConfig` that implements the `Config` trait.
/// A static config has no runtime cost on parsing or constructing a hash map.
///
/// Supports two formats: hgrc and json-like.
///
/// hgrc is an INI-like format with extra features like `%unset`. For example:
///
/// ```ignore
/// const CONFIG1: StaticConfig = static_config!(r"""
/// [section]
/// name = value
/// """);
/// ```
///
/// json-like describes configs in `{ section: { name: value } }` format
/// where section, name, value are strings. For example:
///
/// ```ignore
/// const CONFIG2: StaticConfig = static_config!{
///     "section": {
///         "name": "value"
///     }
/// };
/// ```
///
/// To assign a name to the config, prefix it with `name =>`. For example:
///
/// ```ignore
/// const CONFIG1: StaticConfig = static_config! {
///     "my_config" => r"""
/// [section]
/// name = value
/// """
/// };
///
/// const CONFIG2: StaticConfig = static_config! {
///     "my_config" => { "section": { "name": "value" } }
/// };
/// ```
#[macro_export]
macro_rules! static_config {
    // json-like { section: { name: value } }
    { $( $section:literal :
         { $(  $name:literal : $value: literal ),* $(,)? }
       ),* $(,)?
    } => {
        staticconfig::_detail::static_items![
            $( $( ( $section, $name, $value ), )* )*
        ]
    };

    // hgrc-like
    ( $text:literal ) => {
        staticconfig::_detail::static_rc!($text)
    };

    // with a name
    ( $name:literal => { $( $body:tt )* } ) => {
        static_config! { $($body)* }.named($name)
    };
    ( $name:literal => $text:literal ) => {
        static_config!($text).named($name)
    };
}

/// Statically compiled config that does not require runtime parsing.
/// Can be used for testing too.
///
/// Use `static_config!` to construct this type.
pub struct StaticConfig {
    name: &'static str,
    sections: OrderedMap<&'static str, OrderedMap<&'static str, Option<&'static str>>>,
}

impl StaticConfig {
    /// Change the "name" of this config.
    pub const fn named(self, name: &'static str) -> Self {
        Self {
            name,
            sections: self.sections,
        }
    }

    /// Construct `StaticConfig`. This is intended to be used only by the
    /// `static_rc!` macro.
    pub const fn from_macro_rules(
        sections: OrderedMap<&'static str, OrderedMap<&'static str, Option<&'static str>>>,
    ) -> Self {
        Self {
            name: "StaticConfig",
            sections,
        }
    }
}

impl Config for StaticConfig {
    fn keys(&self, section: &str) -> Vec<Text> {
        match self.sections.get(section) {
            Some(map) => map.keys().map(|n| Text::from_static(n)).collect(),
            None => Vec::new(),
        }
    }

    fn get_considering_unset(&self, section: &str, name: &str) -> Option<Option<Text>> {
        match self.sections.get(section)?.get(name) {
            None => None,
            Some(None) => Some(None),
            Some(Some(value)) => Some(Some(Text::from_static(value))),
        }
    }

    fn sections(&self) -> Cow<[Text]> {
        let sections: Vec<Text> = self.sections.keys().map(|n| Text::from_static(n)).collect();
        sections.into()
    }

    fn get_sources(&self, section: &str, name: &str) -> Cow<[ValueSource]> {
        match self.get(section, name) {
            Some(value) => Cow::Owned(vec![ValueSource {
                value: Some(value.clone()),
                source: Text::from_static(self.name),
                location: None,
            }]),
            None => Cow::Borrowed(&[]),
        }
    }

    fn layer_name(&self) -> Text {
        Text::from_static(self.name)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Emulates external crate usage where `staticconfig::StaticConfig` is available.
    // This is needed, since `static_rc!` refers to `staticconfig::StaticConfig`.
    pub mod staticconfig {
        pub use crate::phf;
        pub use crate::StaticConfig;
        pub use crate::_detail;
    }

    #[test]
    fn test_rc_format() {
        const CONFIG: StaticConfig = static_config!(
            r#"
[a1]
v1 = x
v3 = y
v2 = z
v2 = zz

[c3]
foo = bar

[b2]
multi-line = line1
  line2
  line3
            "#
        )
        .named("test.rc");

        check_config(&CONFIG);
    }

    #[test]
    fn test_unset() {
        let config = static_config!(
            r#"
[a]
v1 = x
%unset v1
%unset v2
v2 = x
%unset v3
            "#
        );

        assert_eq!(config.get_considering_unset("a", "v1"), Some(None));
        assert_eq!(
            config.get_considering_unset("a", "v2").unwrap().unwrap(),
            "x"
        );
        assert_eq!(config.get_considering_unset("b", "v2"), None);
        assert_eq!(config.get_considering_unset("a", "v3"), Some(None));
        assert_eq!(config.get_considering_unset("a", "v4"), None);
    }

    #[test]
    fn test_json_like() {
        const CONFIG: StaticConfig = static_config! {
            "a1": {
                "v1": "x",
                "v3": "y",
                "v2": "z",
                "v2": "zz",
            },
            "c3": {
                "foo": "bar",
            },
            "b2": {
                "multi-line": "line1\nline2\nline3",
            },
        };

        check_config(&CONFIG);
    }

    #[test]
    fn test_with_name() {
        const CONFIG: StaticConfig = static_config!("a.rc" => "[a]\nb=1");
        assert_eq!(CONFIG.name, "a.rc");
        assert_eq!(CONFIG.get("a", "b").unwrap(), "1");

        let config: StaticConfig = static_config!("b.rc" => {"a": {"b": "1"}});
        assert_eq!(config.name, "b.rc");
        assert_eq!(config.get("a", "b").unwrap(), "1");
    }

    /// Check config equivalent to the one in `test_static_rc`.
    fn check_config(config: &StaticConfig) {
        // sections() works and preserves order.
        assert_eq!(config.sections().into_owned(), ["a1", "c3", "b2"]);

        // keys() works and preserves order.
        assert_eq!(config.keys("a1"), ["v1", "v3", "v2"]);

        // get() works.
        assert_eq!(
            config.get("b2", "multi-line").unwrap(),
            "line1\nline2\nline3"
        );
        assert_eq!(config.get("a1", "v3").unwrap(), "y");
        assert_eq!(config.get("a1", "v2").unwrap(), "zz");

        assert!(config.get("a1", "v4").is_none());
        assert!(config.get("c2", "foo").is_none());

        // layer_name() defined by `named`.
        assert_eq!(config.layer_name(), config.name);

        // get_source() provides the name.
        assert!(config.get_sources("c2", "foo").is_empty());
        let sources = config.get_sources("a1", "v2");
        assert_eq!(sources.len(), 1);
        assert_eq!(sources[0].source, config.name);
        assert_eq!(sources[0].value.as_deref(), Some("zz"));
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::borrow::Cow;
use std::path::PathBuf;
use std::sync::Arc;

use configmodel::Config;
use configmodel::Text;
use configmodel::ValueSource;
use indexmap::IndexSet;

/// A union of multiple configs.
#[derive(Clone)]
pub struct UnionConfig {
    configs: Vec<Arc<dyn Config>>,
    name: Text,
}

impl Default for UnionConfig {
    fn default() -> Self {
        Self::from_configs(Vec::new())
    }
}

impl UnionConfig {
    /// Construct `UnionConfig` from a list of configs.
    /// In case of conflicts, the last one wins.
    pub fn from_configs(configs: Vec<Arc<dyn Config>>) -> Self {
        Self {
            configs,
            name: Text::from_static("UnionConfig"),
        }
    }

    /// Assign a name to this config.
    pub fn named(self, name: Text) -> Self {
        Self {
            configs: self.configs,
            name,
        }
    }

    /// Push a config as a layer. It overrides other configs.
    pub fn push(&mut self, config: Arc<dyn Config>) {
        self.configs.push(config)
    }
}

impl Config for UnionConfig {
    fn keys(&self, section: &str) -> Vec<Text> {
        let mut result: IndexSet<Text> = Default::default();
        // normal order: match order loading configs
        for config in self.configs.iter() {
            for key in config.keys(section) {
                result.insert(key);
            }
        }
        result.into_iter().collect()
    }

    fn get_considering_unset(&self, section: &str, name: &str) -> Option<Option<Text>> {
        // rev order: last config counts
        for config in self.configs.iter().rev() {
            if let Some(value) = config.get_considering_unset(section, name) {
                return Some(value);
            }
        }
        None
    }

    fn sections(&self) -> Cow<[Text]> {
        let mut result: IndexSet<Text> = Default::default();
        // normal order: match order loading configs
        for config in self.configs.iter() {
            for section in config.sections().iter() {
                result.insert(section.clone());
            }
        }
        let result: Vec<Text> = result.into_iter().collect();
        result.into()
    }

    fn get_sources(&self, section: &str, name: &str) -> Cow<[ValueSource]> {
        // The last "Source" counts.
        // normal order: match order loading configs
        let mut result = Vec::new();
        for config in self.configs.iter() {
            let mut sources = config.get_sources(section, name).into_owned();
            result.append(&mut sources);
        }
        result.into()
    }

    fn files(&self) -> Cow<[PathBuf]> {
        let mut result: IndexSet<PathBuf> = Default::default();
        // normal order: match order loading configs
        for config in self.configs.iter() {
            for path in config.files().into_owned() {
                result.insert(path);
            }
        }
        let result: Vec<PathBuf> = result.into_iter().collect();
        result.into()
    }

    fn layers(&self) -> Vec<Arc<dyn Config>> {
        self.configs.clone()
    }

    fn layer_name(&self) -> Text {
        self.name.clone()
    }
}

#[cfg(test)]
mod tests {
    use staticconfig::static_config;

    use super::*;

    #[test]
    fn test_basic() {
        let config1 = static_config!(
            r#"
[b]
w=1
v=2

[a]
v=3
w=5

[d]
v=4
"#
        )
        .named("1.rc");
        let config2 = static_config!(
            r#"
[c]
v=11

[b]
z=12
v=13
"#
        )
        .named("2.rc");
        let config3 = static_config!(
            r#"
[a]
v=21
%unset w

[c]
v=22

[e]
v=23
"#
        )
        .named("3.rc");
        let config = UnionConfig::from_configs(vec![
            Arc::new(config1),
            Arc::new(config2),
            Arc::new(config3),
        ])
        .named("unioned".into());

        // sections() preserves order.
        assert_eq!(config.sections().into_owned(), ["b", "a", "d", "c", "e"]);

        // keys() preserves order.
        assert_eq!(config.keys("b"), ["w", "v", "z"]);

        // get(): last config wins.
        assert_eq!(config.get("a", "v").unwrap(), "21");
        assert_eq!(config.get("a", "w"), None);
        assert_eq!(config.get("b", "v").unwrap(), "13");
        assert_eq!(config.get("b", "w").unwrap(), "1");
        assert_eq!(config.get("c", "v").unwrap(), "22");
        assert_eq!(config.get("d", "v").unwrap(), "4");
        assert_eq!(config.get("e", "v").unwrap(), "23");

        // get_considering_unset()
        assert_eq!(config.get_considering_unset("a", "w"), Some(None));
        assert_eq!(config.get_considering_unset("a", "x"), None);

        // get_sources() returns all sources. The last one counts.
        assert_eq!(
            config
                .get_sources("a", "v")
                .iter()
                .map(|s| s.source.to_string())
                .collect::<Vec<_>>(),
            ["1.rc", "3.rc"]
        );

        // layers() returns configs in order.
        assert_eq!(
            config
                .layers()
                .into_iter()
                .map(|c| c.layer_name())
                .collect::<Vec<_>>(),
            ["1.rc", "2.rc", "3.rc"]
        );

        // layer_name()
        assert_eq!(config.layer_name(), "unioned");
    }

    #[test]
    fn test_push() {
        let config1 = static_config! {"b": { "v": "1" }};
        let config2 = static_config! {"b": { "v": "2" }};

        let mut config = UnionConfig::default();
        config.push(Arc::new(config1));
        assert_eq!(config.get("b", "v").unwrap(), "1");
        config.push(Arc::new(config2));
        assert_eq!(config.get("b", "v").unwrap(), "2");
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::env;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::time::Duration;

use anyhow::bail;
use anyhow::Result;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde_json;
use serde_json::Map;
use serde_json::Value;
use tracing::debug;
use util::file::atomic_write;

const FILE_NAME_ENV: &str = "CONTROL_POINT_FILE";

lazy_static! {
    static ref STATE: Mutex<Value> = Mutex::new(Value::Object(Map::new()));
}

pub fn control_point(name: &str) {
    if env::var(FILE_NAME_ENV).is_ok() {
        let result = (|| {
            if let Some((instruction, args)) = read_instruction(name)? {
                execute(name, &instruction, args)
            } else {
                Ok(())
            }
        })();
        if let Err(e) = result {
            panic!("control-point({:?}) failed: {:?}", name, e);
        }
    }
}

fn execute(name: &str, instruction: &str, args: Map<String, Value>) -> Result<()> {
    STATE
        .lock()
        .as_object_mut()
        .unwrap()
        .insert(name.to_string(), Value::String("processing".to_string()));
    write_state()?;

    let result = (|| -> Result<()> {
        let mut current_instruction = instruction.to_string();
        loop {
            debug!(
                "{:?} - processing instruction: {:?} with arguments {:?}",
                name, instruction, args
            );
            match current_instruction.as_str() {
                "wait" => {
                    std::thread::sleep(Duration::from_millis(1));
                    if let Some((instruction, _args)) = read_instruction(name)? {
                        current_instruction = instruction.to_string();
                    } else {
                        bail!(
                            "missing instruction for currently waiting {:?} control-point",
                            name
                        );
                    }

                    continue;
                }
                "continue" => {
                    return Ok(());
                }
                "panic" => {
                    panic!(
                        "{:?}",
                        args.get("message")
                            .map(|m| m.to_string())
                            .unwrap_or_else(|| "control-point panic".to_string())
                    );
                }
                _ => {
                    bail!(
                        "unknown control-point instruction {:?} with arguments {:?}",
                        instruction,
                        args
                    );
                }
            };
        }
    })();

    STATE.lock().as_object_mut().unwrap().remove(name);
    write_state()?;
    result
}

fn read_instruction(name: &str) -> Result<Option<(String, Map<String, Value>)>> {
    // Unwrap since this code can only be reached if we go through the env check in
    // control_point().
    let contents = fs::read_to_string(env::var(FILE_NAME_ENV).unwrap())?;
    let mut instructions = match serde_json::from_str(&contents)? {
        Value::Object(map) => map,
        _ => bail!("invalid control-point instruction contents {:?}", contents),
    };

    Ok(match instructions.remove(name) {
        Some(Value::Array(instruction)) => {
            let len = instruction.len();
            if len != 1 && len != 2 {
                bail!("invalid control-point instruction {:?}", instruction);
            }
            let mut iter = instruction.into_iter();
            let (name, args) = match (iter.next(), iter.next()) {
                (Some(Value::String(name)), None) => (name, Map::new()),
                (Some(Value::String(name)), Some(Value::Object(map))) => (name, map),
                (first, second) => bail!(
                    "invalid control-point instructions {:?} & {:?}",
                    first,
                    second
                ),
            };

            Some((name, args))
        }
        Some(other) => bail!("invalid control-point non-array instruction {:?}", other),
        None => None,
    })
}

fn write_state() -> Result<()> {
    let path = Path::new(&env::var(FILE_NAME_ENV).unwrap()).with_extension("response");
    atomic_write(&path, |f| {
        f.write(STATE.lock().to_string().as_bytes())?;
        Ok(())
    })?;
    Ok(())
}

pub fn wait(file: &Path, name: &str, timeout: &Duration) -> Result<()> {
    let now = std::time::Instant::now();
    loop {
        if now.elapsed() > *timeout {
            bail!("waited too long for control-point {:?} in {:?}", name, file);
        }
        if !file.exists() {
            std::thread::sleep(Duration::from_millis(1));
            continue;
        }
        let contents = fs::read_to_string(file)?;
        let map = match serde_json::from_str(&contents) {
            Ok(Value::Object(map)) => map,
            _ => bail!("invalid json map in control-point response {:?}", contents),
        };
        if let Some(value) = map.get(name) {
            if value == "processing" {
                return Ok(());
            }
        }

        std::thread::sleep(Duration::from_millis(1));
    }
}

pub fn set_actions(path: &Path, map: &Value) -> Result<()> {
    atomic_write(path, |f| {
        f.write(map.to_string().as_bytes())?;
        Ok(())
    })?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::sync::atomic::AtomicU64;
    use std::sync::atomic::Ordering;
    use std::sync::Arc;
    use std::thread::spawn;

    use serde_json::json;
    use tempdir::TempDir;

    use super::*;

    #[test]
    fn test_wait() {
        let timeout = Duration::from_millis(5000);

        let dir = TempDir::new("testdir").expect("tempdir");
        let file = dir.path().join("control_point_file");
        let response_file = file.with_extension("response");
        std::env::set_var(FILE_NAME_ENV, &file);

        set_actions(
            &file,
            &json!({
                "first": ("wait", ),
                "second": ("wait", ),
                "third": ("wait", ),

            }),
        )
        .unwrap();

        let complete = Arc::new(AtomicU64::new(0));
        let complete2 = complete.clone();
        let action_thread = spawn(move || {
            complete2.fetch_add(1, Ordering::SeqCst);
            control_point("first");
            complete2.fetch_add(1, Ordering::SeqCst);
            control_point("second");
            complete2.fetch_add(1, Ordering::SeqCst);
            control_point("third");
            complete2.fetch_add(1, Ordering::SeqCst);
        });

        wait(&response_file, "first", &timeout).unwrap();
        assert_eq!(complete.load(Ordering::SeqCst), 1);
        set_actions(
            &file,
            &json!({
                "first": ("continue", ),
                "second": ("wait", ),
                "third": ("wait", ),

            }),
        )
        .unwrap();

        wait(&response_file, "second", &timeout).unwrap();
        assert_eq!(complete.load(Ordering::SeqCst), 2);
        set_actions(
            &file,
            &json!({
                "second": ("continue", ),
                "third": ("wait", ),

            }),
        )
        .unwrap();

        wait(&response_file, "third", &timeout).unwrap();
        assert_eq!(complete.load(Ordering::SeqCst), 3);
        set_actions(
            &file,
            &json!({
                "third": ("continue", ),

            }),
        )
        .unwrap();

        action_thread.join().unwrap();
        assert_eq!(complete.load(Ordering::SeqCst), 4);
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use anyhow::Result;
use async_trait::async_trait;
use dag::Vertex;
use types::RepoPathBuf;

/// Tracing the rename history of a file for rename detection in rebase, amend etc
#[async_trait]
pub trait CopyTrace {
    /// Trace the corresponding path of `src_path` in `dst` vertex across renames.
    /// Depending on the relationship of `src` and `dst`, it will search backward,
    /// forward or both.
    async fn trace_rename(
        &self,
        src: Vertex,
        dst: Vertex,
        src_path: RepoPathBuf,
    ) -> Result<Option<RepoPathBuf>>;

    /// Trace the corresponding path of `dst_path` in `src` commit across renames.
    /// It will search backward, i.e. from `dst` to `src` vertex.
    async fn trace_rename_backward(
        &self,
        src: Vertex,
        dst: Vertex,
        dst_path: RepoPathBuf,
    ) -> Result<Option<RepoPathBuf>>;

    /// Trace the corresponding path of `src_path` in `dst` commit across renames.
    /// It will search forward, i.e. from `src` to `dst` vertex.
    async fn trace_rename_forward(
        &self,
        src: Vertex,
        dst: Vertex,
        src_path: RepoPathBuf,
    ) -> Result<Option<RepoPathBuf>>;
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;
use dag::DagAlgorithm;
use hg_metrics::increment_counter;
use manifest::Manifest;
use manifest_tree::TreeManifest;
use manifest_tree::TreeStore;
use pathhistory::RenameTracer;
use storemodel::ReadRootTreeIds;
use types::HgId;
use types::RepoPath;
use types::RepoPathBuf;

use crate::error::CopyTraceError;
use crate::CopyTrace;
use crate::RenameFinder;
use crate::SearchDirection;

pub struct DagCopyTrace {
    /* Input */
    /// Resolve commit ids to trees in batch.
    root_tree_reader: Arc<dyn ReadRootTreeIds + Send + Sync>,

    /// Resolve and prefetch trees in batch.
    tree_store: Arc<dyn TreeStore + Send + Sync>,

    // Find renames for given commits
    rename_finder: Arc<dyn RenameFinder + Send + Sync>,

    /// Commit graph algorithms
    dag: Arc<dyn DagAlgorithm + Send + Sync>,
}

impl DagCopyTrace {
    pub fn new(
        root_tree_reader: Arc<dyn ReadRootTreeIds + Send + Sync>,
        tree_store: Arc<dyn TreeStore + Send + Sync>,
        rename_finder: Arc<dyn RenameFinder + Send + Sync>,
        dag: Arc<dyn DagAlgorithm + Send + Sync>,
    ) -> Result<Self> {
        let dag_copy_trace = Self {
            root_tree_reader,
            tree_store,
            rename_finder,
            dag,
        };
        Ok(dag_copy_trace)
    }

    async fn vertex_to_tree_manifest(&self, commit: &dag::Vertex) -> Result<TreeManifest> {
        let commit_id = HgId::from_slice(commit.as_ref())?;
        let commit_to_tree_id = self
            .root_tree_reader
            .read_root_tree_ids(vec![commit_id])
            .await?;
        if commit_to_tree_id.is_empty() {
            return Err(CopyTraceError::RootTreeIdNotFound(commit_id).into());
        }
        let (_, tree_id) = commit_to_tree_id[0];
        Ok(TreeManifest::durable(self.tree_store.clone(), tree_id))
    }

    async fn trace_rename_commit(
        &self,
        src: dag::Vertex,
        dst: dag::Vertex,
        path: RepoPathBuf,
    ) -> Result<Option<dag::Vertex>> {
        let set = self.dag.range(src.into(), dst.into()).await?;
        let mut rename_tracer = RenameTracer::new(
            set,
            path,
            self.root_tree_reader.clone(),
            self.tree_store.clone(),
        )
        .await?;
        let rename_commit = rename_tracer.next().await?;
        Ok(rename_commit)
    }

    async fn find_renames_in_direction(
        &self,
        commit: dag::Vertex,
        path: &RepoPath,
        direction: SearchDirection,
    ) -> Result<(Option<RepoPathBuf>, dag::Vertex)> {
        let parents = self.dag.parent_names(commit.clone()).await?;
        if parents.is_empty() {
            return Err(CopyTraceError::NoParents(commit).into());
        }
        // For simplicity, we only check p1.
        let p1 = &parents[0];
        let old_manifest = self.vertex_to_tree_manifest(p1).await?;
        let new_manifest = self.vertex_to_tree_manifest(&commit).await?;
        let (rename, next_commit) = match direction {
            SearchDirection::Backward => {
                let rename = self
                    .rename_finder
                    .find_rename_backward(&old_manifest, &new_manifest, path, &commit)
                    .await?;
                (rename, p1.clone())
            }
            SearchDirection::Forward => {
                let rename = self
                    .rename_finder
                    .find_rename_forward(&old_manifest, &new_manifest, path, &commit)
                    .await?;
                (rename, commit)
            }
        };
        Ok((rename, next_commit))
    }

    async fn check_path(
        &self,
        commit: &dag::Vertex,
        path: RepoPathBuf,
    ) -> Result<Option<RepoPathBuf>> {
        let tree = self.vertex_to_tree_manifest(commit).await?;
        if tree.get(&path)?.is_some() {
            Ok(Some(path))
        } else {
            Ok(None)
        }
    }
}

#[async_trait]
impl CopyTrace for DagCopyTrace {
    async fn trace_rename(
        &self,
        src: dag::Vertex,
        dst: dag::Vertex,
        src_path: RepoPathBuf,
    ) -> Result<Option<RepoPathBuf>> {
        tracing::debug!(?src, ?dst, ?src_path, "trace_reanme");
        if self.dag.is_ancestor(src.clone(), dst.clone()).await? {
            return self
                .trace_rename_forward(src.clone(), dst.clone(), src_path)
                .await;
        } else if self.dag.is_ancestor(dst.clone(), src.clone()).await? {
            return self
                .trace_rename_backward(dst.clone(), src.clone(), src_path)
                .await;
        } else {
            let set = dag::Set::from_static_names(vec![src.clone(), dst.clone()]);
            let base = match self.dag.gca_one(set).await? {
                Some(base) => base,
                None => {
                    tracing::trace!("no common base");
                    increment_counter("copytrace_noCommonBase", 1);
                    return Ok(None);
                }
            };
            tracing::trace!(?base);
            let base_path = self
                .trace_rename_backward(base.clone(), src, src_path)
                .await?;
            tracing::trace!(?base_path);
            if let Some(base_path) = base_path {
                return self.trace_rename_forward(base, dst, base_path).await;
            } else {
                increment_counter("copytrace_notInCommonBase", 1);
                return Ok(None);
            }
        }
    }

    async fn trace_rename_backward(
        &self,
        src: dag::Vertex,
        dst: dag::Vertex,
        dst_path: RepoPathBuf,
    ) -> Result<Option<RepoPathBuf>> {
        tracing::trace!(?src, ?dst, ?dst_path, "trace_rename_backward");
        let (mut curr, target, mut curr_path) = (dst, src, dst_path);

        loop {
            tracing::trace!(?curr, ?curr_path, " loop starts");
            let rename_commit = match self
                .trace_rename_commit(target.clone(), curr.clone(), curr_path.clone())
                .await?
            {
                Some(rename_commit) => rename_commit,
                None => return self.check_path(&target, curr_path).await,
            };
            tracing::trace!(?rename_commit, " found");

            if rename_commit == target {
                return Ok(Some(curr_path));
            }
            let (next_path, next_commit) = self
                .find_renames_in_direction(
                    rename_commit,
                    curr_path.as_repo_path(),
                    SearchDirection::Backward,
                )
                .await?;
            if let Some(next_path) = next_path {
                curr = next_commit;
                curr_path = next_path;
            } else {
                // no rename info for curr_path
                return Ok(None);
            }
        }
    }

    async fn trace_rename_forward(
        &self,
        src: dag::Vertex,
        dst: dag::Vertex,
        src_path: RepoPathBuf,
    ) -> Result<Option<RepoPathBuf>> {
        tracing::trace!(?src, ?dst, ?src_path, "trace_rename_forward");
        let (mut curr, target, mut curr_path) = (src, dst, src_path);

        loop {
            tracing::trace!(?curr, ?curr_path, " loop starts");
            let rename_commit = match self
                .trace_rename_commit(curr.clone(), target.clone(), curr_path.clone())
                .await?
            {
                Some(rename_commit) => rename_commit,
                None => return self.check_path(&target, curr_path).await,
            };
            tracing::trace!(?rename_commit, " found");

            if rename_commit == curr {
                return Ok(Some(curr_path));
            }
            let (next_path, next_commit) = self
                .find_renames_in_direction(
                    rename_commit,
                    curr_path.as_repo_path(),
                    SearchDirection::Forward,
                )
                .await?;
            if let Some(next_path) = next_path {
                curr = next_commit;
                curr_path = next_path;
            } else {
                // no rename info for curr_path
                return Ok(None);
            }
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use dag::Vertex;
use types::HgId;
use types::RepoPathBuf;

#[derive(Debug, thiserror::Error)]
pub enum CopyTraceError {
    /// Root tree id can not be found for the given commit
    #[error("Root tree id can not be found for commit: {0:?}")]
    RootTreeIdNotFound(HgId),

    /// Parents can not be found for the given commit
    #[error("Parents can not be found for commit: {0:?}")]
    NoParents(Vertex),

    #[error("File not found: {0:?}")]
    FileNotFound(RepoPathBuf),
}