/*
 * 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.
 */

//! Mercurial-specific config postprocessing

use std::collections::HashMap;
use std::collections::HashSet;
use std::env;
use std::fs;
#[cfg(feature = "fb")]
use std::fs::read_to_string;
use std::hash::Hash;
use std::io;
use std::io::Error as IOError;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use configmodel::Config;
use configmodel::ConfigExt;
use hgplain;
use identity::Identity;
use minibytes::Text;
use url::Url;
use util::path::expand_path;

use crate::config::ConfigSet;
use crate::config::Options;
use crate::error::Error;
use crate::error::Errors;

pub trait OptionsHgExt {
    /// Drop configs according to `$HGPLAIN` and `$HGPLAINEXCEPT`.
    fn process_hgplain(self) -> Self;

    /// Set read-only config items. `items` contains a list of tuple `(section, name)`.
    /// Setting those items to new value will be ignored.
    fn readonly_items<S: Into<Text>, N: Into<Text>>(self, items: Vec<(S, N)>) -> Self;

    /// Set section remap. If a section name matches an entry key, it will be treated as if the
    /// name is the entry value. The remap wouldn't happen recursively. For example, with a
    /// `{"A": "B", "B": "C"}` map, section name "A" will be treated as "B", not "C".
    /// This is implemented via `append_filter`.
    fn remap_sections<K: Eq + Hash + Into<Text>, V: Into<Text>>(self, remap: HashMap<K, V>)
    -> Self;

    /// Filter sections. Sections outside include_sections won't be loaded.
    /// This is implemented via `append_filter`.
    fn filter_sections<B: Clone + Into<Text>>(self, include_sections: Vec<B>) -> Self;
}

pub trait ConfigSetHgExt {
    fn load<S: Into<Text>, N: Into<Text>>(
        &mut self,
        repo_path: Option<&Path>,
        readonly_items: Option<Vec<(S, N)>>,
    ) -> Result<(), Errors>;

    /// Load system config files if config environment variable is not set.
    /// Return errors parsing files.
    fn load_system(&mut self, opts: Options, identity: &Identity) -> Vec<Error>;

    /// Load the dynamic config files for the given repo path.
    /// Returns errors parsing, generating, or fetching the configs.
    fn load_dynamic(
        &mut self,
        repo_path: Option<&Path>,
        opts: Options,
        identity: &Identity,
        proxy_sock_path: Option<String>,
    ) -> Result<Vec<Error>>;

    /// Load user config files (and environment variables).  If config environment variable is
    /// set, load files listed in that environment variable instead.
    /// Return errors parsing files.
    fn load_user(&mut self, opts: Options, identity: &Identity) -> Vec<Error>;

    /// Load repo config files.
    fn load_repo(&mut self, repo_path: &Path, opts: Options, identity: &Identity) -> Vec<Error>;

    /// Load a specified config file. Respect HGPLAIN environment variables.
    /// Return errors parsing files.
    fn load_hgrc(&mut self, path: impl AsRef<Path>, source: &'static str) -> Vec<Error>;

    fn validate_dynamic(&mut self) -> Result<(), Error>;
}

/// Load config from specified repo root path, or global config if no path specified.
/// `extra_values` contains config overrides (i.e. "--config" CLI values).
/// `extra_files` contains additional config files (i.e. "--configfile" CLI values).
pub fn load(
    repo_path: Option<&Path>,
    extra_values: &[String],
    extra_files: &[String],
) -> Result<ConfigSet> {
    let mut cfg = ConfigSet::new();

    let mut errors = Vec::new();
    for path in extra_files {
        errors.extend(cfg.load_path(&path, &"--configfile".into()));
    }

    if let Err(err) = set_overrides(&mut cfg, extra_values) {
        errors.push(err);
    }

    match cfg.load::<Text, Text>(repo_path, None) {
        Ok(_) => {
            if !errors.is_empty() {
                return Err(Errors(errors).into());
            }
        }
        Err(mut err) => {
            err.0.extend(errors);
            return Err(err.into());
        }
    }

    // Load the CLI configs again to make sure they take precedence.
    // The "readonly" facility can't be used to pin the configs
    // because it doesn't interact with the config verification properly.
    for path in extra_files {
        cfg.load_path(&path, &"--configfile".into());
    }

    let _ = set_overrides(&mut cfg, extra_values);

    Ok(cfg)
}

impl OptionsHgExt for Options {
    fn process_hgplain(self) -> Self {
        if hgplain::is_plain(None) {
            let (section_exclude_list, ui_exclude_list) = {
                let plain_exceptions = hgplain::exceptions();

                // [defaults] and [commands] are always excluded.
                let mut section_exclude_list: HashSet<Text> =
                    ["defaults", "commands"].iter().map(|&s| s.into()).collect();

                // [alias], [revsetalias], [templatealias] are excluded if they are outside
                // HGPLAINEXCEPT.
                for name in ["alias", "revsetalias", "templatealias"] {
                    if !plain_exceptions.contains(name) {
                        section_exclude_list.insert(Text::from(name));
                    }
                }

                // These configs under [ui] are always excluded.
                let mut ui_exclude_list: HashSet<Text> = [
                    "debug",
                    "fallbackencoding",
                    "quiet",
                    "slash",
                    "logtemplate",
                    "statuscopies",
                    "style",
                    "traceback",
                    "verbose",
                ]
                .iter()
                .map(|&s| s.into())
                .collect();
                // exitcodemask is excluded if exitcode is outside HGPLAINEXCEPT.
                if !plain_exceptions.contains("exitcode") {
                    ui_exclude_list.insert("exitcodemask".into());
                }

                (section_exclude_list, ui_exclude_list)
            };

            let filter = move |section: Text, name: Text, value: Option<Text>| {
                if section_exclude_list.contains(&section)
                    || (section.as_ref() == "ui" && ui_exclude_list.contains(&name))
                {
                    None
                } else {
                    Some((section, name, value))
                }
            };

            self.append_filter(Box::new(filter))
        } else {
            self
        }
    }

    /// Filter sections. Sections outside of include_sections won't be loaded.
    /// This is implemented via `append_filter`.
    fn filter_sections<B: Clone + Into<Text>>(self, include_sections: Vec<B>) -> Self {
        let include_list: HashSet<Text> = include_sections
            .iter()
            .cloned()
            .map(|section| section.into())
            .collect();

        let filter = move |section: Text, name: Text, value: Option<Text>| {
            if include_list.contains(&section) {
                Some((section, name, value))
            } else {
                None
            }
        };

        self.append_filter(Box::new(filter))
    }

    /// Set section remap. If a section name matches an entry key, it will be treated as if the
    /// name is the entry value. The remap wouldn't happen recursively. For example, with a
    /// `{"A": "B", "B": "C"}` map, section name "A" will be treated as "B", not "C".
    /// This is implemented via `append_filter`.
    fn remap_sections<K, V>(self, remap: HashMap<K, V>) -> Self
    where
        K: Eq + Hash + Into<Text>,
        V: Into<Text>,
    {
        let remap: HashMap<Text, Text> = remap
            .into_iter()
            .map(|(k, v)| (k.into(), v.into()))
            .collect();

        let filter = move |section: Text, name: Text, value: Option<Text>| {
            let section = remap.get(&section).cloned().unwrap_or(section);
            Some((section, name, value))
        };

        self.append_filter(Box::new(filter))
    }

    fn readonly_items<S: Into<Text>, N: Into<Text>>(self, items: Vec<(S, N)>) -> Self {
        let readonly_items: HashSet<(Text, Text)> = items
            .into_iter()
            .map(|(section, name)| (section.into(), name.into()))
            .collect();

        let filter = move |section: Text, name: Text, value: Option<Text>| {
            if readonly_items.contains(&(section.clone(), name.clone())) {
                None
            } else {
                Some((section, name, value))
            }
        };

        self.append_filter(Box::new(filter))
    }
}

/// override config values from a list of --config overrides
fn set_overrides(config: &mut ConfigSet, overrides: &[String]) -> crate::Result<()> {
    for config_override in overrides {
        let equals_pos = config_override
            .find('=')
            .ok_or_else(|| Error::ParseFlag(config_override.to_string()))?;
        let section_name_pair = &config_override[..equals_pos];
        let value = &config_override[equals_pos + 1..];

        let dot_pos = section_name_pair
            .find('.')
            .ok_or_else(|| Error::ParseFlag(config_override.to_string()))?;
        let section = &section_name_pair[..dot_pos];
        let name = &section_name_pair[dot_pos + 1..];

        config.set(section, name, Some(value), &"--config".into());
    }
    Ok(())
}

impl ConfigSetHgExt for ConfigSet {
    /// Load system, user config files.
    fn load<S: Into<Text>, N: Into<Text>>(
        &mut self,
        repo_path: Option<&Path>,
        readonly_items: Option<Vec<(S, N)>>,
    ) -> Result<(), Errors> {
        tracing::info!(
            repo_path = %repo_path.and_then(|p| p.to_str()).unwrap_or("<none>"),
            "loading config"
        );

        let ident = repo_path
            .map(|p| identity::must_sniff_dir(p).map_err(|e| Errors(vec![Error::Other(e)])))
            .transpose()?;
        let ident = ident.unwrap_or_else(identity::default);

        let repo_path = repo_path.map(|p| p.join(ident.dot_dir()));

        let mut errors = vec![];

        let mut opts = Options::new();
        if let Some(readonly_items) = readonly_items {
            opts = opts.readonly_items(readonly_items);
        }

        // The config priority from low to high is:
        //
        //   builtin
        //   dynamic
        //   system
        //   user
        //   repo
        //
        // We load things out of order a bit since the dynamic config can depend
        // on system config (namely, auth_proxy.unix_socket_path).

        // Clone rather than Self::new() so we include any --config overrides
        // already inside self.
        let mut dynamic = self.clone();

        errors.append(&mut self.load_system(opts.clone(), &ident));
        errors.append(&mut self.load_user(opts.clone(), &ident));

        // This is the out-of-orderness. We load the dynamic config on a
        // detached ConfigSet then combine it into our "secondary" config
        // sources to maintian the correct priority.
        errors.append(
            &mut dynamic
                .load_dynamic(
                    repo_path.as_deref(),
                    opts.clone(),
                    &ident,
                    self.get_opt("auth_proxy", "unix_socket_path")
                        .unwrap_or_default(),
                )
                .map_err(|e| Errors(vec![Error::Other(e)]))?,
        );

        let mut low_prio_configs = crate::builtin_static::builtin_system(opts.clone(), &ident);
        low_prio_configs.push(Arc::new(dynamic));
        self.secondary(Arc::new(low_prio_configs));

        if let Some(repo_path) = repo_path.as_deref() {
            errors.append(&mut self.load_repo(repo_path, opts, &ident));
            if let Err(e) = read_set_repo_name(self, repo_path) {
                errors.push(e);
            }
        }

        if !errors.is_empty() {
            return Err(Errors(errors));
        }

        self.validate_dynamic().map_err(|err| Errors(vec![err]))
    }

    fn load_system(&mut self, opts: Options, ident: &Identity) -> Vec<Error> {
        let opts = opts.source("system").process_hgplain();
        let mut errors = Vec::new();

        // If $HGRCPATH is set, use it instead.
        if let Some(Ok(rcpath)) = ident.env_var("CONFIG") {
            #[cfg(unix)]
            let paths = rcpath.split(':');
            #[cfg(windows)]
            let paths = rcpath.split(';');
            for path in paths {
                errors.append(&mut self.load_path(expand_path(path), &opts));
            }
        } else {
            for system_path in all_existing_system_paths(ident) {
                if system_path.exists() {
                    errors.append(&mut self.load_path(system_path, &opts));
                    break;
                }
            }
        }

        errors
    }

    #[cfg(feature = "fb")]
    fn load_dynamic(
        &mut self,
        repo_path: Option<&Path>,
        opts: Options,
        identity: &Identity,
        proxy_sock_path: Option<String>,
    ) -> Result<Vec<Error>> {
        use std::process::Command;
        use std::time::Duration;
        use std::time::SystemTime;

        use util::run_background;

        use crate::fb::dynamicconfig::vpnless_config_path;

        let mut errors = Vec::new();

        // Compute path
        let dynamic_path = get_config_dir(repo_path)?.join("hgrc.dynamic");

        // Check version
        let content = read_to_string(&dynamic_path).ok();
        let version = content.as_ref().and_then(|c| {
            let mut lines = c.split("\n");
            match lines.next() {
                Some(line) if line.starts_with("# version=") => Some(&line[10..]),
                Some(_) | None => None,
            }
        });

        let this_version = ::version::VERSION;

        let vpnless_changed = match (dynamic_path.metadata(), vpnless_config_path().metadata()) {
            (Ok(d), Ok(v)) => v.modified()? > d.modified()?,
            _ => false,
        };

        // Synchronously generate the new config if it's out of date with our version
        if version != Some(this_version) || vpnless_changed {
            tracing::info!(?dynamic_path, file_version=?version, my_version=%this_version, vpnless_changed, "regenerating dynamic config (version mismatch)");
            let (repo_name, user_name) = {
                let mut temp_config = ConfigSet::new();
                if !temp_config.load_user(opts.clone(), identity).is_empty() {
                    bail!("unable to read user config to get user name");
                }

                let repo_name = match repo_path {
                    Some(repo_path) => {
                        let opts = opts.clone().source("temp").process_hgplain();
                        // We need to know the repo name, but that's stored in the repository configs at
                        // the moment. In the long term we need to move that, but for now let's load the
                        // repo config ahead of time to read the name.
                        let repo_hgrc_path = repo_path.join("hgrc");
                        if !temp_config.load_path(repo_hgrc_path, &opts).is_empty() {
                            bail!("unable to read repo config to get repo name");
                        }
                        Some(read_set_repo_name(&mut temp_config, repo_path)?)
                    }
                    None => None,
                };

                (repo_name, temp_config.get_or_default("ui", "username")?)
            };

            // Regen inline
            let res =
                generate_dynamicconfig(repo_path, repo_name, None, user_name, proxy_sock_path);
            if let Err(e) = res {
                let is_perm_error = e
                    .chain()
                    .any(|cause| match cause.downcast_ref::<IOError>() {
                        Some(io_error) if io_error.kind() == ErrorKind::PermissionDenied => true,
                        _ => false,
                    });
                if !is_perm_error {
                    return Err(e);
                }
            }
        } else {
            tracing::debug!(?dynamic_path, version=%this_version, "dynamicconfig version in-sync");
        }

        if !dynamic_path.exists() {
            return Err(IOError::new(
                ErrorKind::NotFound,
                format!("required config not found at {}", dynamic_path.display()),
            )
            .into());
        }

        // Read hgrc.dynamic
        let opts = opts.source("dynamic").process_hgplain();
        errors.append(&mut self.load_path(&dynamic_path, &opts));

        // Log config ages
        // - Done in python for now

        // Regenerate if mtime is old.
        let generation_time: Option<u64> = self.get_opt("configs", "generationtime")?;
        let recursion_marker = env::var("HG_DEBUGDYNAMICCONFIG");
        let mut skip_reason = None;

        if recursion_marker.is_err() {
            if let Some(generation_time) = generation_time {
                let generation_time = Duration::from_secs(generation_time);
                let mtime_age = SystemTime::now()
                    .duration_since(dynamic_path.metadata()?.modified()?)
                    // An error from duration_since means 'now' is older than
                    // 'last_modified'. In that case, let's assume the file
                    // is brand new and has an age of 0.
                    .unwrap_or(Duration::from_secs(0));
                if mtime_age > generation_time {
                    let config_regen_command: Vec<String> =
                        self.get_or("configs", "regen-command", || {
                            vec![
                                identity::cli_name().to_string(),
                                "debugdynamicconfig".to_string(),
                            ]
                        })?;
                    tracing::debug!(
                        "spawn {:?} because mtime({}) {:?} > generation_time {:?}",
                        &config_regen_command,
                        dynamic_path.display(),
                        mtime_age,
                        generation_time
                    );
                    if !config_regen_command.is_empty() {
                        let mut command = Command::new(&config_regen_command[0]);
                        command
                            .args(&config_regen_command[1..])
                            .env("HG_DEBUGDYNAMICCONFIG", "1");

                        if let Some(repo_path) = repo_path {
                            command.current_dir(&repo_path);
                        }

                        let _ = run_background(command);
                    }
                } else {
                    skip_reason = Some("mtime <= configs.generationtime");
                }
            } else {
                skip_reason = Some("configs.generationtime is not set");
            }
        } else {
            skip_reason = Some("HG_DEBUGDYNAMICCONFIG is set");
        }
        if let Some(reason) = skip_reason {
            tracing::debug!("skip spawning debugdynamicconfig because {}", reason);
        }

        Ok(errors)
    }

    #[cfg(not(feature = "fb"))]
    fn load_dynamic(
        &mut self,
        _repo_path: Option<&Path>,
        _opts: Options,
        _identity: &Identity,
        _proxy_sock_path: Option<String>,
    ) -> Result<Vec<Error>> {
        Ok(Vec::new())
    }

    fn load_user(&mut self, opts: Options, ident: &Identity) -> Vec<Error> {
        // If scripting config env var is set, don't load user configs
        if identity::env_var("CONFIG").is_none() {
            if let Some(path) = all_existing_user_paths(ident).next() {
                return self.load_user_internal(Some(&path), opts);
            }
        }

        // Call with empty paths for side effects.
        self.load_user_internal(None, opts)
    }

    fn load_repo(&mut self, repo_path: &Path, opts: Options, identity: &Identity) -> Vec<Error> {
        let mut errors = Vec::new();

        let opts = opts.source("repo").process_hgplain();

        let repo_config_path = repo_path.join(identity.config_repo_file());
        errors.append(&mut self.load_path(repo_config_path, &opts));

        errors
    }

    fn load_hgrc(&mut self, path: impl AsRef<Path>, source: &'static str) -> Vec<Error> {
        let opts = Options::new().source(source).process_hgplain();
        self.load_path(path, &opts)
    }

    #[cfg(feature = "fb")]
    fn validate_dynamic(&mut self) -> Result<(), Error> {
        let allowed_locations: Option<Vec<String>> =
            self.get_opt::<Vec<String>>("configs", "allowedlocations")?;
        let allowed_configs: Option<Vec<String>> =
            self.get_opt::<Vec<String>>("configs", "allowedconfigs")?;

        Ok(self.ensure_location_supersets(
            allowed_locations
                .as_ref()
                .map(|v| HashSet::from_iter(v.iter().map(|s| s.as_str()))),
            allowed_configs.as_ref().map(|v| {
                HashSet::from_iter(v.iter().map(|s| {
                    let split: Vec<&str> = s.splitn(2, ".").into_iter().collect();
                    (split[0], split[1])
                }))
            }),
        ))
    }

    #[cfg(not(feature = "fb"))]
    fn validate_dynamic(&mut self) -> Result<(), Error> {
        Ok(())
    }
}

/// Read repo name from various places (remotefilelog.reponame, paths.default, .hg/reponame).
///
/// Try to write the reponame back to `.hg/reponame`, and set `remotefilelog.reponame`
/// for code paths using them.
///
/// If `configs.forbid-empty-reponame` is `true`, raise if the repo name is empty
/// and `paths.default` is set.
fn read_set_repo_name(config: &mut ConfigSet, repo_path: &Path) -> crate::Result<String> {
    let (repo_name, source): (String, &str) = {
        let mut name: String = config.get_or_default("remotefilelog", "reponame")?;
        let mut source = "remotefilelog.reponame";
        if name.is_empty() {
            tracing::warn!("repo name: no remotefilelog.reponame");
            let path: String = config.get_or_default("paths", "default")?;
            name = repo_name_from_url(config, &path).unwrap_or_default();
            if name.is_empty() {
                tracing::warn!("repo name: no path.default reponame: {}", &path);
            }
            source = "paths.default";
        }
        if name.is_empty() {
            match read_repo_name_from_disk(repo_path) {
                Ok(s) => {
                    name = s;
                    source = "reponame file";
                }
                Err(e) => {
                    tracing::warn!("repo name: no reponame file: {:?}", &e);
                }
            };
        }
        (name, source)
    };

    if !repo_name.is_empty() {
        tracing::debug!("repo name: {:?} (from {})", &repo_name, source);
        if source != "reponame file" {
            let need_rewrite = match read_repo_name_from_disk(repo_path) {
                Ok(s) => s != repo_name,
                Err(_) => true,
            };
            if need_rewrite {
                let path = get_repo_name_path(repo_path);
                match fs::write(&path, &repo_name) {
                    Ok(_) => tracing::debug!("repo name: written to reponame file"),
                    Err(e) => tracing::warn!("repo name: cannot write to reponame file: {:?}", e),
                }
            }
        }
        if source != "remotefilelog.reponame" {
            config.set(
                "remotefilelog",
                "reponame",
                Some(&repo_name),
                &source.into(),
            );
        }
    } else {
        let forbid_empty_reponame: bool =
            config.get_or_default("configs", "forbid-empty-reponame")?;
        if forbid_empty_reponame && config.get("paths", "default").is_some() {
            let msg = "reponame is empty".to_string();
            return Err(Error::General(msg));
        }
    }

    Ok(repo_name)
}

trait ConfigSetExtInternal {
    fn load_user_internal(&mut self, path: Option<&PathBuf>, opts: Options) -> Vec<Error>;
}

impl ConfigSetExtInternal for ConfigSet {
    // For easier testing.
    fn load_user_internal(&mut self, path: Option<&PathBuf>, opts: Options) -> Vec<Error> {
        let mut errors = Vec::new();

        // Covert "$VISUAL", "$EDITOR" to "ui.editor".
        //
        // Unlike Mercurial, don't convert the "$PAGER" environment variable
        // to "pager.pager" config.
        //
        // The environment variable could be from the system profile (ex.
        // /etc/profile.d/...), or the user shell rc (ex. ~/.bashrc). There is
        // no clean way to tell which one it is from.  The value might be
        // tweaked for sysadmin usecases (ex. -n), which are different from
        // SCM's usecases.
        for name in ["VISUAL", "EDITOR"] {
            if let Ok(editor) = env::var(name) {
                if !editor.is_empty() {
                    self.set(
                        "ui",
                        "editor",
                        Some(editor),
                        &opts.clone().source(format!("${}", name)),
                    );
                    break;
                }
            }
        }

        // Convert $HGPROF to profiling.type
        if let Ok(profiling_type) = env::var("HGPROF") {
            self.set("profiling", "type", Some(profiling_type), &"$HGPROF".into());
        }

        let opts = opts.source("user").process_hgplain();

        if let Some(path) = path {
            errors.append(&mut self.load_path(path, &opts));
        }

        // Override ui.merge:interactive (source != user) with ui.merge
        // (source == user). This makes ui.merge in user hgrc effective,
        // even if ui.merge:interactive is not set.
        if self
            .get_sources("ui", "merge:interactive")
            .last()
            .map(|s| s.source().as_ref())
            != Some("user")
            && self
                .get_sources("ui", "merge")
                .last()
                .map(|s| s.source().as_ref())
                == Some("user")
        {
            if let Some(merge) = self.get("ui", "merge") {
                self.set("ui", "merge:interactive", Some(merge), &opts);
            }
        }

        errors
    }
}

/// Using custom "schemes" from config, resolve given url.
pub fn resolve_custom_scheme(config: &dyn Config, url: Url) -> Result<Url> {
    if let Some(tmpl) = config.get_nonempty("schemes", url.scheme()) {
        let non_scheme = match url.as_str().split_once(':') {
            Some((_, after)) => after.trim_start_matches('/'),
            None => bail!("url {url} has no scheme"),
        };

        let resolved_url = if tmpl.contains("{1}") {
            tmpl.replace("{1}", non_scheme)
        } else {
            format!("{tmpl}{non_scheme}")
        };

        return Ok(Url::parse(&resolved_url)
            .with_context(|| format!("parsing resolved custom scheme URL {resolved_url}"))?);
    }

    Ok(url)
}

pub fn repo_name_from_url(config: &dyn Config, s: &str) -> Option<String> {
    // Use a base_url to support non-absolute urls.
    let base_url = Url::parse("file:///.").unwrap();
    let parse_opts = Url::options().base_url(Some(&base_url));
    match parse_opts.parse(s) {
        Ok(url) => {
            let url = resolve_custom_scheme(config, url).ok()?;

            tracing::trace!("parsed url {}: {:?}", s, url);
            match url.scheme() {
                "mononoke" => {
                    // In Mononoke URLs, the repo name is always the full path
                    // with slashes trimmed.
                    let path = url.path().trim_matches('/');
                    if !path.is_empty() {
                        return Some(path.to_string());
                    }
                }
                _ => {
                    // Try the last segment in url path.
                    if let Some(last_segment) = url
                        .path_segments()
                        .and_then(|s| s.rev().find(|s| !s.is_empty()))
                    {
                        return Some(last_segment.to_string());
                    }
                    // Try path. `path_segment` can be `None` for URL like "test:reponame".
                    let path = url.path().trim_matches('/');
                    if !path.is_empty() {
                        return Some(path.to_string());
                    }
                    // Try the hostname. ex. in "fb://fbsource", "fbsource" is a host not a path.
                    // Also see https://www.mercurial-scm.org/repo/hg/help/schemes
                    if let Some(host_str) = url.host_str() {
                        return Some(host_str.to_string());
                    }
                }
            }
        }
        Err(e) => {
            tracing::warn!("cannot parse url {}: {:?}", s, e);
        }
    }
    None
}

#[cfg(feature = "fb")]
fn get_config_dir(repo_path: Option<&Path>) -> Result<PathBuf, Error> {
    Ok(match repo_path {
        Some(repo_path) => {
            let shared_path = repo_path.join("sharedpath");
            if shared_path.exists() {
                let raw = read_to_string(&shared_path).map_err(|e| Error::Io(shared_path, e))?;
                let trimmed = raw.trim_end_matches("\n");
                // sharedpath can be relative, so join it with repo_path.
                repo_path.join(trimmed)
            } else {
                repo_path.to_path_buf()
            }
        }
        None => {
            let dirs = vec![
                std::env::var("TESTTMP")
                    .ok()
                    .map(|d| PathBuf::from(d).join(".cache")),
                std::env::var("HG_CONFIG_CACHE_DIR").ok().map(PathBuf::from),
                dirs::cache_dir(),
                Some(std::env::temp_dir()),
            ];

            let mut errs = vec![];
            for mut dir in dirs.into_iter().flatten() {
                dir.push("edenscm");
                match util::path::create_shared_dir_all(&dir) {
                    Err(err) => {
                        tracing::debug!("error setting up config cache dir {:?}: {}", dir, err);
                        errs.push((dir, err));
                        continue;
                    }
                    Ok(()) => return Ok(dir),
                }
            }

            return Err(Error::General(format!(
                "couldn't find config cache dir: {:?}",
                errs
            )));
        }
    })
}

#[cfg(feature = "fb")]
pub fn calculate_dynamicconfig(
    config_dir: PathBuf,
    repo_name: Option<impl AsRef<str>>,
    canary: Option<String>,
    user_name: String,
    proxy_sock_path: Option<String>,
) -> Result<ConfigSet> {
    use crate::fb::dynamicconfig::Generator;
    Generator::new(repo_name, config_dir, user_name, proxy_sock_path)?.execute(canary)
}

#[cfg(feature = "fb")]
pub fn generate_dynamicconfig(
    repo_path: Option<&Path>,
    repo_name: Option<impl AsRef<str>>,
    canary: Option<String>,
    user_name: String,
    proxy_sock_path: Option<String>,
) -> Result<()> {
    use std::io::Write;

    use filetime::set_file_mtime;
    use filetime::FileTime;
    use tempfile::tempfile_in;

    tracing::debug!(
        repo_path = ?repo_path,
        canary = ?canary,
        "generate_dynamicconfig",
    );

    // Resolve sharedpath
    let config_dir = get_config_dir(repo_path)?;

    // Verify that the filesystem is writable, otherwise exit early since we won't be able to write
    // the config.
    if tempfile_in(&config_dir).is_err() {
        return Err(IOError::new(
            ErrorKind::PermissionDenied,
            format!("no write access to {:?}", config_dir),
        )
        .into());
    }

    let version = ::version::VERSION;
    let header = format!(
        concat!(
            "# version={}\n",
            "# reponame={}\n",
            "# canary={:?}\n",
            "# username={}\n",
            "# Generated by `hg debugdynamicconfig` - DO NOT MODIFY\n",
        ),
        version,
        repo_name.as_ref().map_or("no_repo", |r| r.as_ref()),
        canary.as_ref(),
        &user_name,
    );

    let hgrc_path = config_dir.join("hgrc.dynamic");
    let global_config_dir = get_config_dir(None)?;

    let config = calculate_dynamicconfig(
        global_config_dir,
        repo_name,
        canary,
        user_name,
        proxy_sock_path,
    )?;
    let config_str = format!("{}{}", header, config.to_string());

    // If the file exists and will be unchanged, just update the mtime.
    if hgrc_path.exists() && read_to_string(&hgrc_path).unwrap_or_default() == config_str {
        let time = FileTime::now();
        tracing::debug!("bump {:?} mtime to {:?}", &hgrc_path, &time);
        set_file_mtime(hgrc_path, time)?;
    } else {
        tracing::debug!("rewrite {:?}", &hgrc_path);
        util::file::atomic_write(&hgrc_path, |f| {
            f.write_all(config_str.as_bytes())?;
            Ok(())
        })?;
    }

    Ok(())
}

/// Get the path of the reponame file.
fn get_repo_name_path(shared_dot_hg_path: &Path) -> PathBuf {
    shared_dot_hg_path.join("reponame")
}

/// Read repo name from shared `.hg` path.
pub fn read_repo_name_from_disk(shared_dot_hg_path: &Path) -> io::Result<String> {
    let repo_name_path = get_repo_name_path(shared_dot_hg_path);
    let name = fs::read_to_string(&repo_name_path)?.trim().to_string();
    if name.is_empty() {
        Err(IOError::new(
            ErrorKind::InvalidData,
            format!("reponame could not be empty ({})", repo_name_path.display()),
        ))
    } else {
        Ok(name)
    }
}

pub fn all_existing_system_paths<'a>(id: &'a Identity) -> impl Iterator<Item = PathBuf> + 'a {
    Some(id)
        .into_iter()
        .chain(identity::all())
        .filter_map(|id| id.system_config_path().filter(|p| p.exists()))
}

pub fn all_existing_user_paths<'a>(id: &'a Identity) -> impl Iterator<Item = PathBuf> + 'a {
    Some(id)
        .into_iter()
        .chain(identity::all())
        .flat_map(|id| id.user_config_paths().into_iter().filter(|p| p.exists()))
}

pub fn default_user_config_path() -> Result<PathBuf> {
    let id = identity::default();
    let res = all_existing_user_paths(&id)
        .chain(id.user_config_paths().into_iter())
        .next()
        .with_context(|| "unable to determine user config location")?;
    Ok(res)
}

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

    use once_cell::sync::Lazy;
    use tempdir::TempDir;
    use testutil::envs::lock_env;

    use super::*;

    static CONFIG_ENV_VAR: Lazy<&str> =
        Lazy::new(|| identity::default().env_name_static("CONFIG").unwrap());
    static HGPLAIN: Lazy<&str> =
        Lazy::new(|| identity::default().env_name_static("PLAIN").unwrap());
    static HGPLAINEXCEPT: Lazy<&str> =
        Lazy::new(|| identity::default().env_name_static("PLAINEXCEPT").unwrap());

    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_basic_hgplain() {
        let mut env = lock_env();

        env.set(*HGPLAIN, Some("1"));
        env.set(*HGPLAINEXCEPT, None);

        let opts = Options::new().process_hgplain();
        let mut cfg = ConfigSet::new();
        cfg.parse(
            "[defaults]\n\
             commit = commit -d 0\n\
             [ui]\n\
             verbose = true\n\
             username = test\n\
             [alias]\n\
             l = log\n",
            &opts,
        );

        assert!(cfg.keys("defaults").is_empty());
        assert_eq!(cfg.get("ui", "verbose"), None);
        assert_eq!(cfg.get("ui", "username"), Some("test".into()));
        assert_eq!(cfg.get("alias", "l"), None);
    }

    #[test]
    fn test_static_config_hgplain() {
        let mut env = lock_env();

        env.set("TESTTMP", Some("1"));

        let cfg = load(None, &[], &[]).unwrap();

        // Sanity that we have a test value from static config.
        assert_eq!(
            cfg.get("alias", "some-command"),
            Some("some-command --some-flag".into())
        );
        let sources = cfg.get_sources("alias", "some-command");
        assert_eq!(sources.len(), 1);
        assert_eq!(sources[0].source(), &"builtin:test_config");

        // With HGPLAIN=1, aliases should get dropped.
        env.set(*HGPLAIN, Some("1"));
        let cfg = load(None, &[], &[]).unwrap();
        assert_eq!(cfg.get("alias", "some-command"), None);
    }

    #[test]
    fn test_hgplainexcept() {
        let mut env = lock_env();

        env.set(*HGPLAIN, None);
        env.set(*HGPLAINEXCEPT, Some("alias,revsetalias"));

        let opts = Options::new().process_hgplain();
        let mut cfg = ConfigSet::new();
        cfg.parse(
            "[defaults]\n\
             commit = commit -d 0\n\
             [alias]\n\
             l = log\n\
             [templatealias]\n\
             u = user\n\
             [revsetalias]\n\
             @ = master\n",
            &opts,
        );

        assert!(cfg.keys("defaults").is_empty());
        assert_eq!(cfg.get("alias", "l"), Some("log".into()));
        assert_eq!(cfg.get("revsetalias", "@"), Some("master".into()));
        assert_eq!(cfg.get("templatealias", "u"), None);
    }

    #[test]
    fn test_is_plain() {
        let mut env = lock_env();

        use hgplain::is_plain;

        env.set(*HGPLAIN, None);
        env.set(*HGPLAINEXCEPT, None);
        assert!(!is_plain(None));

        env.set(*HGPLAIN, Some("1"));
        assert!(is_plain(None));
        assert!(is_plain(Some("banana")));

        env.set(*HGPLAINEXCEPT, Some("dog,banana,tree"));
        assert!(!is_plain(Some("banana")));

        env.set(*HGPLAIN, None);
        assert!(!is_plain(Some("banana")));
    }

    #[test]
    fn test_config_path() {
        let mut env = lock_env();

        let dir = TempDir::new("test_config_path").unwrap();

        write_file(dir.path().join("1.rc"), "[x]\na=1");
        write_file(dir.path().join("2.rc"), "[y]\nb=2");

        #[cfg(unix)]
        let hgrcpath = "$T/1.rc:$T/2.rc";
        #[cfg(windows)]
        let hgrcpath = "$T/1.rc;%T%/2.rc";

        env.set("EDITOR", None);
        env.set("VISUAL", None);
        env.set("HGPROF", None);

        env.set("T", Some(dir.path().to_str().unwrap()));
        env.set(*CONFIG_ENV_VAR, Some(hgrcpath));

        let mut cfg = ConfigSet::new();

        let identity = identity::default();
        cfg.load_user(Options::new(), &identity);
        assert!(
            cfg.sections().is_empty(),
            "sections {:?} is not empty",
            cfg.sections()
        );

        let identity = identity::default();
        cfg.load_system(Options::new(), &identity);
        assert_eq!(cfg.get("x", "a"), Some("1".into()));
        assert_eq!(cfg.get("y", "b"), Some("2".into()));
    }

    #[test]
    fn test_load_user() {
        let _env = lock_env();

        let dir = TempDir::new("test_hgrcpath").unwrap();
        let path = dir.path().join("1.rc");

        write_file(path.clone(), "[ui]\nmerge=x");

        let mut cfg = ConfigSet::new();
        cfg.load_user_internal(Some(&path), Options::new());
        assert_eq!(cfg.get("ui", "merge").unwrap(), "x");
        assert_eq!(cfg.get("ui", "merge:interactive").unwrap(), "x");

        let mut cfg = ConfigSet::new();
        cfg.set("ui", "merge", Some("foo"), &"system".into());
        cfg.set("ui", "merge:interactive", Some("foo"), &"system".into());
        cfg.load_user_internal(Some(&path), Options::new());
        assert_eq!(cfg.get("ui", "merge").unwrap(), "x");
        assert_eq!(cfg.get("ui", "merge:interactive").unwrap(), "x");

        let mut cfg = ConfigSet::new();
        cfg.set("ui", "merge:interactive", Some("foo"), &"system".into());
        write_file(path.clone(), "[ui]\nmerge=x\nmerge:interactive=y\n");
        cfg.load_user_internal(Some(&path), Options::new());
        assert_eq!(cfg.get("ui", "merge").unwrap(), "x");
        assert_eq!(cfg.get("ui", "merge:interactive").unwrap(), "y");

        let mut cfg = ConfigSet::new();
        cfg.set("ui", "merge", Some("a"), &"system".into());
        cfg.set("ui", "merge:interactive", Some("b"), &"system".into());
        write_file(path.clone(), "");
        cfg.load_user_internal(Some(&path), Options::new());
        assert_eq!(cfg.get("ui", "merge").unwrap(), "a");
        assert_eq!(cfg.get("ui", "merge:interactive").unwrap(), "b");
        write_file(path.clone(), "[ui]\nmerge:interactive=y\n");
        cfg.load_user_internal(Some(&path), Options::new());
        assert_eq!(cfg.get("ui", "merge").unwrap(), "a");
        assert_eq!(cfg.get("ui", "merge:interactive").unwrap(), "y");

        drop(path);
    }

    #[test]
    fn test_load_hgrc() {
        let dir = TempDir::new("test_hgrcpath").unwrap();
        let path = dir.path().join("1.rc");

        write_file(path.clone(), "[x]\na=1\n[alias]\nb=c\n");

        let mut env = lock_env();

        env.set(*HGPLAIN, Some("1"));
        env.set(*HGPLAINEXCEPT, None);

        let mut cfg = ConfigSet::new();
        cfg.load_hgrc(&path, "hgrc");

        assert!(cfg.keys("alias").is_empty());
        assert!(cfg.get("alias", "b").is_none());
        assert_eq!(cfg.get("x", "a").unwrap(), "1");

        env.set(*HGPLAIN, None);
        cfg.load_hgrc(&path, "hgrc");

        assert_eq!(cfg.get("alias", "b").unwrap(), "c");
    }

    #[test]
    fn test_section_filter() {
        let opts = Options::new().filter_sections(vec!["x", "y"]);
        let mut cfg = ConfigSet::new();
        cfg.parse(
            "[x]\n\
             a=1\n\
             [y]\n\
             b=2\n\
             [z]\n\
             c=3",
            &opts,
        );

        assert_eq!(cfg.sections(), vec![Text::from("x"), Text::from("y")]);
        assert_eq!(cfg.get("z", "c"), None);
    }

    #[test]
    fn test_section_remap() {
        let mut remap = HashMap::new();
        remap.insert("x", "y");
        remap.insert("y", "z");

        let opts = Options::new().remap_sections(remap);
        let mut cfg = ConfigSet::new();
        cfg.parse(
            "[x]\n\
             a=1\n\
             [y]\n\
             b=2\n\
             [z]\n\
             c=3",
            &opts,
        );

        assert_eq!(cfg.get("y", "a"), Some("1".into()));
        assert_eq!(cfg.get("z", "b"), Some("2".into()));
        assert_eq!(cfg.get("z", "c"), Some("3".into()));
    }

    #[test]
    fn test_readonly_items() {
        let opts = Options::new().readonly_items(vec![("x", "a"), ("y", "b")]);
        let mut cfg = ConfigSet::new();
        cfg.parse(
            "[x]\n\
             a=1\n\
             [y]\n\
             b=2\n\
             [z]\n\
             c=3",
            &opts,
        );

        assert_eq!(cfg.get("x", "a"), None);
        assert_eq!(cfg.get("y", "b"), None);
        assert_eq!(cfg.get("z", "c"), Some("3".into()));
    }

    #[test]
    fn test_py_core_items() {
        let mut env = lock_env();

        // Skip real dynamic config.
        env.set("TESTTMP", Some("1"));

        let mut cfg = ConfigSet::new();
        cfg.load::<String, String>(None, None).unwrap();
        assert_eq!(cfg.get("treestate", "repackfactor").unwrap(), "3");
    }

    #[test]
    fn test_load_cli_args() {
        let mut env = lock_env();

        // Skip real dynamic config.
        env.set("TESTTMP", Some("1"));

        let dir = TempDir::new("test_load").unwrap();

        let repo_rc = dir.path().join(".hg/hgrc");
        write_file(repo_rc, "[s]\na=orig\nb=orig\nc=orig");

        let other_rc = dir.path().join("other.rc");
        write_file(other_rc.clone(), "[s]\na=other\nb=other");

        let cfg = load(
            Some(dir.path()),
            &["s.b=flag".to_string()],
            &[format!("{}", other_rc.display())],
        )
        .unwrap();

        assert_eq!(cfg.get("s", "a"), Some("other".into()));
        assert_eq!(cfg.get("s", "b"), Some("flag".into()));
        assert_eq!(cfg.get("s", "c"), Some("orig".into()));
    }

    #[test]
    fn test_repo_name_from_url() {
        let config = BTreeMap::<&str, &str>::from([("schemes.fb", "mononoke://example.com/{1}")]);

        let check = |url, name| {
            assert_eq!(repo_name_from_url(&config, url).as_deref(), name);
        };

        // Ordinary schemes use the basename as the repo name
        check("repo", Some("repo"));
        check("../path/to/repo", Some("repo"));
        check("file:repo", Some("repo"));
        check("file:/path/to/repo", Some("repo"));
        check("file://server/path/to/repo", Some("repo"));
        check("ssh://user@host/repo", Some("repo"));
        check("ssh://user@host/path/to/repo", Some("repo"));
        check("file:/", None);

        // This isn't correct, but is a side-effect of earlier hacks (should
        // be `None`)
        check("ssh://user@host:100/", Some("host"));

        // Mononoke scheme uses the full path, and repo names can contain
        // slashes.
        check("mononoke://example.com/repo", Some("repo"));
        check("mononoke://example.com/path/to/repo", Some("path/to/repo"));
        check("mononoke://example.com/", None);

        // FB scheme uses the full path.
        check("fb:repo", Some("repo"));
        check("fb:path/to/repo", Some("path/to/repo"));
        check("fb:", None);

        // FB scheme works even when there are extra slashes that shouldn't be
        // there.
        check("fb://repo/", Some("repo"));
        check("fb://path/to/repo", Some("path/to/repo"));
    }

    #[test]
    fn test_resolve_custom_scheme() {
        let config = BTreeMap::<&str, &str>::from([
            ("schemes.append", "appended://bar/"),
            ("schemes.subst", "substd://bar/{1}/baz"),
        ]);

        let check = |url, resolved| {
            assert_eq!(
                resolve_custom_scheme(&config, Url::parse(url).unwrap())
                    .unwrap()
                    .as_str(),
                resolved
            );
        };

        check("other://foo", "other://foo");
        check("append:one/two", "appended://bar/one/two");
        check("subst://one/two", "substd://bar/one/two/baz");
    }
}


/*
 * 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.
 */

//! `configloader` is about how to load config locally and remotely for a repo.
//!
//! It is heavyweight because of the remote config logic. There are lightweight
//! choices if you do not need logic to load configs, for example, if you can
//! just get a config from elsewhere.
//!
//! If you're looking for just reading configs, use `&dyn configmodel::Config`.
//! If you're looking for reading configs and some extra features like setting
//! configs, use `configset::ConfigSet`.

pub mod hg;

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 configset::config;
pub use error::Errors;
// Re-export
pub use minibytes::Text;

#[cfg(feature = "fb")]
mod fb;

mod builtin_static;


/*
 * 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 staticconfig::static_config;
use staticconfig::StaticConfig;

/// Default config. Partially migrated from configitems.py.
///
/// Lowest priority. Should always be loaded.
pub static CONFIG: StaticConfig = static_config!("builtin:core" => r#"
[treestate]
mingcage=900
minrepackthreshold=10M
repackfactor=3

[ui]
timeout=600
color=auto
paginate=true

[checkout]
resumable=true

[tracing]
stderr=false
threshold=10

[format]
generaldelta=false
usegeneraldelta=true

[color]
status.added=green bold
status.clean=none
status.copied=none
status.deleted=cyan bold underline
status.ignored=black bold
status.modified=blue bold
status.removed=red bold
status.unknown=magenta bold underline

[unsafe]
filtersuspectsymlink=true

[experimental]
exportstack-max-commit-count=100
exportstack-max-file-count=400
exportstack-max-bytes=20M

log-implicit-follow-threshold=10000

[zsh]
completion-age=7
completion-description=false

[merge]
enable-merge-tool-script=true
"#);


/*
 * 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 staticconfig::static_config;
use staticconfig::StaticConfig;

/// Merge tools.
///
/// Should be loaded except when RCPATH environment variable is set.
pub static CONFIG: StaticConfig = static_config!("builtin:merge-tools" => r#"
[merge-tools]
kdiff3.args=--auto --L1 base --L2 local --L3 other $base $local $other -o $output
kdiff3.regkey=Software\KDiff3
kdiff3.regkeyalt=Software\Wow6432Node\KDiff3
kdiff3.regappend=\kdiff3.exe
kdiff3.fixeol=True
kdiff3.gui=True
kdiff3.diffargs=--L1 $plabel1 --L2 $clabel $parent $child

gvimdiff.args=--nofork -d -g -O $local $other $base
gvimdiff.regkey=Software\Vim\GVim
gvimdiff.regkeyalt=Software\Wow6432Node\Vim\GVim
gvimdiff.regname=path
gvimdiff.priority=-9
gvimdiff.diffargs=--nofork -d -g -O $parent $child

vimdiff.args=$local $other $base -c 'redraw | echomsg "hg merge conflict, type \":cq\" to abort vimdiff"'
vimdiff.check=changed
vimdiff.priority=-10

merge.check=conflicts
merge.priority=-100

gpyfm.gui=True

meld.gui=True
meld.args=--label='local' $local --label='merged' $base --label='other' $other -o $output
meld.check=changed
meld.diffargs=-a --label=$plabel1 $parent --label=$clabel $child

tkdiff.args=$local $other -a $base -o $output
tkdiff.gui=True
tkdiff.priority=-8
tkdiff.diffargs=-L $plabel1 $parent -L $clabel $child

xxdiff.args=--show-merged-pane --exit-with-merge-status --title1 local --title2 base --title3 other --merged-filename $output --merge $local $base $other
xxdiff.gui=True
xxdiff.priority=-8
xxdiff.diffargs=--title1 $plabel1 $parent --title2 $clabel $child

diffmerge.regkey=Software\SourceGear\SourceGear DiffMerge\
diffmerge.regkeyalt=Software\Wow6432Node\SourceGear\SourceGear DiffMerge\
diffmerge.regname=Location
diffmerge.priority=-7
diffmerge.args=-nosplash -merge -title1=local -title2=merged -title3=other $local $base $other -result=$output
diffmerge.check=changed
diffmerge.gui=True
diffmerge.diffargs=--nosplash --title1=$plabel1 --title2=$clabel $parent $child

p4merge.args=$base $local $other $output
p4merge.regkey=Software\Perforce\Environment
p4merge.regkeyalt=Software\Wow6432Node\Perforce\Environment
p4merge.regname=P4INSTROOT
p4merge.regappend=\p4merge.exe
p4merge.gui=True
p4merge.priority=-8
p4merge.diffargs=$parent $child

p4mergeosx.executable = /Applications/p4merge.app/Contents/MacOS/p4merge
p4mergeosx.args = $base $local $other $output
p4mergeosx.gui = True
p4mergeosx.priority=-8
p4mergeosx.diffargs=$parent $child

tortoisemerge.args=/base:$base /mine:$local /theirs:$other /merged:$output
tortoisemerge.regkey=Software\TortoiseSVN
tortoisemerge.regkeyalt=Software\Wow6432Node\TortoiseSVN
tortoisemerge.check=changed
tortoisemerge.gui=True
tortoisemerge.priority=-8
tortoisemerge.diffargs=/base:$parent /mine:$child /basename:$plabel1 /minename:$clabel

ecmerge.args=$base $local $other --mode=merge3 --title0=base --title1=local --title2=other --to=$output
ecmerge.regkey=Software\Elli\xc3\xa9 Computing\Merge
ecmerge.regkeyalt=Software\Wow6432Node\Elli\xc3\xa9 Computing\Merge
ecmerge.gui=True
ecmerge.diffargs=$parent $child --mode=diff2 --title1=$plabel1 --title2=$clabel

# editmerge is a small script shipped in contrib.
# It needs this config otherwise it behaves the same as internal:local
editmerge.args=$output
editmerge.check=changed
editmerge.premerge=keep

filemerge.executable=/Developer/Applications/Utilities/FileMerge.app/Contents/MacOS/FileMerge
filemerge.args=-left $other -right $local -ancestor $base -merge $output
filemerge.gui=True

filemergexcode.executable=/Applications/Xcode.app/Contents/Applications/FileMerge.app/Contents/MacOS/FileMerge
filemergexcode.args=-left $other -right $local -ancestor $base -merge $output
filemergexcode.gui=True

; Windows version of Beyond Compare
beyondcompare3.args=$local $other $base $output /ro /lefttitle=local /centertitle=base /righttitle=other /automerge /reviewconflicts /solo
beyondcompare3.regkey=Software\Scooter Software\Beyond Compare 3
beyondcompare3.regname=ExePath
beyondcompare3.gui=True
beyondcompare3.priority=-2
beyondcompare3.diffargs=/lro /lefttitle=$plabel1 /righttitle=$clabel /solo /expandall $parent $child

; Linux version of Beyond Compare
bcompare.args=$local $other $base -mergeoutput=$output -ro -lefttitle=parent1 -centertitle=base -righttitle=parent2 -outputtitle=merged -automerge -reviewconflicts -solo
bcompare.gui=True
bcompare.priority=-1
bcompare.diffargs=-lro -lefttitle=$plabel1 -righttitle=$clabel -solo -expandall $parent $child

; OS X version of Beyond Compare
bcomposx.executable = /Applications/Beyond Compare.app/Contents/MacOS/bcomp
bcomposx.args=$local $other $base -mergeoutput=$output -ro -lefttitle=parent1 -centertitle=base -righttitle=parent2 -outputtitle=merged -automerge -reviewconflicts -solo
bcomposx.gui=True
bcomposx.priority=-1
bcomposx.diffargs=-lro -lefttitle=$plabel1 -righttitle=$clabel -solo -expandall $parent $child

winmerge.args=/e /x /wl /ub /dl other /dr local $other $local $output
winmerge.regkey=Software\Thingamahoochie\WinMerge
winmerge.regkeyalt=Software\Wow6432Node\Thingamahoochie\WinMerge\
winmerge.regname=Executable
winmerge.check=changed
winmerge.gui=True
winmerge.priority=-10
winmerge.diffargs=/r /e /x /ub /wl /dl $plabel1 /dr $clabel $parent $child

araxis.regkey=SOFTWARE\Classes\TypeLib\{46799e0a-7bd1-4330-911c-9660bb964ea2}\7.0\HELPDIR
araxis.regappend=\ConsoleCompare.exe
araxis.priority=-2
araxis.args=/3 /a2 /wait /merge /title1:"Other" /title2:"Base" /title3:"Local :"$local $other $base $local $output
araxis.checkconflict=True
araxis.binary=True
araxis.gui=True
araxis.diffargs=/2 /wait /title1:$plabel1 /title2:$clabel $parent $child

diffuse.priority=-3
diffuse.args=$local $base $other
diffuse.gui=True
diffuse.diffargs=$parent $child

UltraCompare.regkey=Software\Microsoft\Windows\CurrentVersion\App Paths\UC.exe
UltraCompare.regkeyalt=Software\Wow6432Node\Microsoft\Windows\CurrentVersion\App Paths\UC.exe
UltraCompare.args = $base $local $other -title1 base -title3 other
UltraCompare.priority = -2
UltraCompare.gui = True
UltraCompare.binary = True
UltraCompare.check = conflicts,changed
UltraCompare.diffargs=$child $parent -title1 $clabel -title2 $plabel1
"#);


/*
 * 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.
 */

//! Statically _compiled_ configs. See `core` or `merge_tools` for example.
//!
//! Use `staticconfig::static_config!` to define static configs so they do not
//! have runtime parsing or hashmap insertion overhead.

use std::sync::Arc;

use configmodel::Config;
use configset::config::ConfigSet;
use configset::config::Options;
use identity::Identity;
use staticconfig::static_config;
use staticconfig::StaticConfig;
use unionconfig::UnionConfig;

use crate::hg::OptionsHgExt;

mod core;
mod merge_tools;
pub(crate) mod sapling;

/// Return static builtin system config.
///
/// The actual selection of configs depends on `ident`.
///
/// This config is intended to have the lowest priority and can be overridden
/// by system config files.
pub(crate) fn builtin_system(opts: Options, ident: &Identity) -> UnionConfig {
    let mut configs: Vec<Arc<dyn Config>> = vec![Arc::new(&core::CONFIG)];
    if ident.env_var("CONFIG").is_none() {
        configs.push(Arc::new(&merge_tools::CONFIG));
    }
    let is_test = std::env::var_os("TESTTMP").is_some();
    let force_prod = std::env::var_os("TEST_PROD_CONFIGS").is_some();
    if ident.cli_name() == "sl" && (!is_test || force_prod) {
        configs.push(Arc::new(&sapling::CONFIG));
    }

    #[cfg(feature = "fb")]
    if !is_test || force_prod {
        configs.push(Arc::new(&crate::fb::static_system::CONFIG));
    }

    // Include a test static config so it's easy for unit and
    // integration tests to cover static config.
    if is_test {
        configs.push(Arc::new(&TEST_CONFIG));
    }

    apply_filters(UnionConfig::from_configs(configs), opts)
}

// Apply filter funcs from Options. This can do various things such as
// ignoring or renaming certain sections.
fn apply_filters(mut uc: UnionConfig, opts: Options) -> UnionConfig {
    let mut filter_overrides = ConfigSet::new();
    let opts = opts.source("builtin").process_hgplain();

    let filtered_opts: Options = "(filtered)".into();
    for section in uc.sections().iter() {
        for name in uc.keys(section) {
            let value = uc.get(section, &name);

            match opts.filter(section.clone(), name.clone(), value.clone()) {
                // None means this value was complete filtered out.
                // Insert explicit `None` into the overrides.
                None => filter_overrides.set(section, name, None::<&str>, &filtered_opts),
                Some((s, n, v)) => {
                    if s != section || n != name {
                        // If the filter mutated the section or name,
                        // first we need to add an override to hide
                        // the previous value.
                        filter_overrides.set(section, name, None::<&str>, &filtered_opts)
                    } else if v == value {
                        // If the config item is unchanged, we don't need an override.
                        continue;
                    }

                    filter_overrides.set(s, n, v, &filtered_opts)
                }
            }
        }
    }

    uc.push(Arc::new(filter_overrides));

    uc
}

/// Static system config used in tests.
pub static TEST_CONFIG: StaticConfig = static_config!("builtin:test_config" => r#"
[alias]
some-command = some-command --some-flag
"#);


/*
 * 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 staticconfig::static_config;
use staticconfig::StaticConfig;

/// Config used by the Sapling identity. Might be merged with core config.
pub static CONFIG: StaticConfig = static_config!("builtin:sapling" => r###"
[alias]
metaedit=metaedit --batch
journal=journal --verbose
restack=rebase --restack
restack:doc=automatically restack commits
 ""
 "    When commits are modified by commands like amend and absorb, their"
 "    descendant commits may be left behind as orphans.  Rebase these"
 "    orphaned commits onto the newest versions of their ancestors, making"
 "    the stack linear again."
sl=smartlog -T '{sl}'
ssl=smartlog -T '{ssl}'

[annotate]
default-flags=user short-date

[automv]
similarity=75

[blackbox]
maxfiles=30
maxsize=5242880
logsource=true
track=command, command_alias, command_finish, command_exception,
 commitcloud_sync, exthook, pythonhook, fsmonitor, watchman, merge_resolve,
 profile, hgsql, sqllock, pushrebase, status, metrics, visibility, perftrace

[color]
log.changeset=bold blue
sl.active=yellow
sl.amended=brightblack:none
sl.branch=bold
sl.book=green
sl.changed=brightblack:none
sl.current=magenta
sl.diff=bold
sl.draft=brightyellow bold
sl.folded=brightblack:none
sl.hiddenlabel=brightblack:none
sl.hiddennode=brightblack:none
sl.histedited=brightblack:none
sl.landed=brightblack:none
sl.undone=brightblack:none
sl.oldbook=bold brightred
sl.public=yellow
sl.rebased=brightblack:none
sl.remote=green
sl.snapshotnode=brightblue:blue
sl.split=brightblack:none
sl.stablecommit = bold green
sl.tasks=bold
sl.backuppending=cyan
sl.backupfail=red
sl.shelvedlabel=bold
ssl.abandoned=brightblack:black+bold
ssl.accepted=brightgreen bold
ssl.committed=cyan
ssl.landing=green
ssl.review=#f7923b:color214:brightyellow bold
ssl.revision=#f21395:color199:brightmagenta bold
ssl.unpublished=#8d949e:color248:none
ssl.landfailed=red
ssl.finalreview=#8036cc:color92:magenta
ssl.unsync=brightred bold
ssl.signal_okay=green
ssl.signal_in_progress=cyan
ssl.signal_warning=yellow
ssl.signal_failed=red
sb.active=green
doctor.treatment=yellow
use-rust=false

[connectionpool]
lifetime=300

[debugnetwork]
speed-test-download-size=10M
speed-test-upload-size=2M

[diff]
git=True
nobinary=True

[directaccess]
loadsafter=tweakdefaults

[experimental]
bundle2-exp=True
changegroup3=True
copytrace=off
crecord=True
disallowhgignorefileset=True
evolution=obsolete
fsmonitor.transaction_notify=True
graphstyle.grandparent=|
histedit.autoverb=True
metalog=True
narrow-heads=True
new-clone-path=True
rebase.multidest=True
samplestatus=3
updatecheck=noconflict
verifyhiddencache=False
worddiff=True
mmapindexthreshold=1
numworkersremover=1
graph.renderer=lines
nativecheckout=True
numworkerswriter=2
network-doctor=True

[extensions]
absorb=
amend=
automv=
blackbox=
chistedit=
clindex=
conflictinfo=
copytrace=
debugnetwork=
dialect=
directaccess=
dirsync=
errorredirect=!
extorder=
fastpartialmatch=
fbhistedit=
fsmonitor=
ghstack=
githelp=
gitrevset=!
hgsubversion=!
histedit=
infinitepush=!
journal=
logginghelper=
lz4revlog=
morestatus=
myparent=
obsshelve=
patchrmdir=!
progressfile=
pullcreatemarkers=
pushrebase=!
rage=!
rebase=
remotefilelog=
remotenames=
schemes=
shelve=
smartlog=
sparse=
strip=
traceprof=
treedirstate=
treemanifest=
tweakdefaults=
undo=

[extorder]
fsmonitor=sqldirstate, sparse
journal=eden

[format]
aggressivemergedeltas=True
generaldelta=True
manifestcachesize=10
maxchainlen=30000
use-zstore-commit-data-revlog-fallback=true
use-zstore-commit-data=False
noloosefile=True
userustmutablestore=True
use-symlink-atomic-write=False

[fsmonitor]
mode=on
timeout=600
track-ignore-files=False
walk_on_invalidate=False
warn-fresh-instance=True
# TODO: T130638905 Update this
sockpath=/opt/facebook/watchman/var/run/watchman/%i-state/sock

[ghrevset]
autopull=True

[histgrep]
allowfullrepogrep=False

[merge]
checkignored=ignore
checkunknown=ignore
on-failure=continue
printcandidatecommmits=True

[merge-tools]
editmerge.args=$output
editmerge.check=changed
editmerge.premerge=keep
editmergeps.args=$output
editmergeps.check=changed
editmergeps.premerge=keep

[mutation]
automigrate=true
enabled=true
record=true

[phases]
publish=False

[remotefilelog]
cachelimit=20GB
cleanoldpacks=True
fastdatapack=True
historypackv1=True
localdatarepack=True
useruststore=True
manifestlimit=4GB
http=True
userustrepack=True
cachepath=~/.sl_cache
retryprefetch=True
fetchpacks=True
getpackversion=2
write-hgcache-to-indexedlog=True
write-local-to-indexedlog=True

[remotenames]
# TODO what's the right oss value for this?
autopullhoistpattern=
autopullpattern=re:^remote/[A-Za-z0-9._/-]+$
cachedistance=False
disallowedbookmarks=master
 remote/master
 main
 remote/main
disallowedhint=please don't specify 'remote/' prefix in remote bookmark's name
disallowedto=^remote/
hoist=remote
precachecurrent=False
precachedistance=False
pushanonheads=False
pushrev=.
rename.default=remote
resolvenodes=False
selectivepull=True
selectivepulldefault=master
astheaddiscovery=False

[server]
preferuncompressed=True
uncompressed=True

[sigtrace]
interval=60

[templatealias]
sl_hash_minlen=9
sl_phase_label="{ifeq(phase, 'public', 'sl.public', 'sl.draft')}"
sl_node="{label(sl_phase_label, shortest(node, sl_hash_minlen))}"
sl_node_debug="{label(sl_phase_label, '{node}')}"
sl_phase_debug="{ifeq(phase, 'draft', '', '({phase})')}"
sl_undonode="{label('sl.oldbook', '{shortest(node, sl_hash_minlen)}')}"
sl_donode="{label('sl.book', '{shortest(node, sl_hash_minlen)}')}"
sl_some="{label('sl.draft', '{shortest(node, sl_hash_minlen)}')}"
undo_node_info="{if(undonecommits(UNDOINDEX), sl_undonode, if(donecommits(UNDOINDEX), if(revset('{node} and olddraft(0)'), sl_some, sl_donode), sl_node))}"
sl_user="{label('sl.user', author|emailuser)}"
sl_active="{if(activebookmark, label('sl.active', '{activebookmark}*'))}"
sl_labeled_bm="{label('sl.book', bookmark)}"
sl_nonactive="{ifeq(bookmark, active, '', '{sl_labeled_bm} ')}"
sl_others="{strip(bookmarks % '{sl_nonactive}')}"
sl_remote="{label('sl.remote', remotebookmarks)}"
sl_books="{separate(' ', sl_active, sl_others, sl_remote)}"
sl_cloud_books = "{separate(' ', sl_active, sl_others)}"
sl_oldbm="{label('sl.book', oldbookmarks(UNDOINDEX))}"
sl_leaving="{label('sl.oldbook', removedbookmarks(UNDOINDEX))}"
sl_bookchanges="{separate(' ', sl_leaving, sl_oldbm)}"
sl_signal_okay="âœ“"
sl_signal_failed="âœ—"
sl_signal_warning="â€¼"
sl_signal_in_progress="â‹¯"
github_sl_difflink="{
  if(github_pull_request_url,
    hyperlink(github_pull_request_url, '#{github_pull_request_number}'),
    if(sapling_pr_follower, label('ssl.unpublished', 'follower'))
  )}"
phab_sl_difflink="{hyperlink(separate('', 'https://our.intern.facebook.com/intern/diff/', phabdiff, '/'), phabdiff)}"
sl_difflink="{if(github_repo, github_sl_difflink, phab_sl_difflink)}"
github_sl_diffsignal="{case(github_pull_request_status_check_rollup, 'SUCCESS', sl_signal_okay, 'PENDING', sl_signal_in_progress, 'FAILURE', sl_signal_failed)}"
github_sl_diffsignallabel="{case(github_pull_request_status_check_rollup, 'SUCCESS', 'ssl.signal_okay', 'PENDING', 'ssl.signal_in_progress', 'FAILURE', 'ssl.signal_failed')}"
phab_sl_diffsignal="{case(phabsignalstatus, 'Okay', sl_signal_okay, 'In Progress', sl_signal_in_progress, 'Warning', sl_signal_warning, 'Failed', sl_signal_failed)}"
phab_sl_diffsignallabel="{case(phabsignalstatus, 'Okay', 'ssl.signal_okay', 'In Progress', 'ssl.signal_in_progress', 'Warning', 'ssl.signal_warning', 'Failed', 'ssl.signal_failed')}"
sl_diffsignal="{if(github_repo, github_sl_diffsignal, phab_sl_diffsignal)}"
sl_diffsignallabel="{if(github_repo, github_sl_diffsignallabel, phab_sl_diffsignallabel)}"
sl_diffstatus="{if(github_repo, github_sl_diffstatus, phab_sl_diffstatus)}"
sl_difflabel="{if(github_repo, github_sl_difflabel, phab_sl_difflabel)}"

# When presenting the PR state to the user, if it is Closed/Merged, we use
# that as the state; otherwise (if it is Open/Unknown), we consider the review
# decision to be the PR state.
github_pr_state="{case(github_pull_request_state,
    'CLOSED', 'CLOSED',
    'MERGED', 'MERGED',
    if(github_pull_request_review_decision, github_pull_request_review_decision, 'NO_DECISION')
    )}"
github_sl_diffstatus="{case(github_pr_state,
    'CLOSED', 'Closed',
    'MERGED', 'Merged',
    'APPROVED', 'Approved',
    'CHANGES_REQUESTED', 'Changes Requested',
    'REVIEW_REQUIRED', 'Review Required',
    'NO_DECISION', 'Unreviewed'
    )}"
# At least for ghstack, PRs are never merged directly because of the branch
# tricks it does, so Merged often means Closed, so we use the same style for
# both. We could consider checking for the "Merged" tag to distinguish between
# "closed but merged" and ordinary "closed", which is more like Abandoned in
# Phabricator.
github_sl_difflabel="{case(github_pr_state,
    'CLOSED', 'ssl.committed',
    'MERGED', 'ssl.committed',
    'APPROVED', 'ssl.accepted',
    'CHANGES_REQUESTED', 'ssl.revision',
    'REVIEW_REQUIRED', 'ssl.review',
    'NO_DECISION', 'ssl.unpublished',
    'sl.diff'
    )}"

phab_sl_diffstatus="{phabstatus}{case(phabstatus, 'Waiting For Author', '*', 'Needs Revision', '*')}"
phab_sl_difflabel="{case(phabstatus,
 'Landing', 'ssl.landing',
 'Accepted', 'ssl.accepted',
 'Waiting For Author', 'ssl.revision',
 'Needs Revision', 'ssl.revision',
 'Changes Planned', 'ssl.revision',
 'Committed', 'ssl.committed',
 'Needs Review', 'ssl.review',
 'Abandoned', 'ssl.abandoned',
 'Unpublished', 'ssl.unpublished',
 'Committing', 'ssl.landing',
 'Recently Failed to Land', 'ssl.landfailed',
 'Needs Final Review', 'ssl.finalreview',
 'sl.diff'
 )}"
sl_date="{label('sl.date', '{smartdate(date, sl_date_threshold, age(date), simpledate(date, sl_date_timezone))}')}"
sl_date_threshold=5400
sl_diff="{label('sl.diff', sl_difflink)}"
sl_task="{label('sl.tasks', 'T{task}')}"
sl_tasks="{join(tasks % '{sl_task}', ' ')}"
sl_backupstatus="{ifcontains(rev, revset('notbackedup()'), if(backingup, label('sl.backuppending', '(Backing up)'), ifcontains(rev, revset('{node} and age(\'<10m\')'), label('sl.backuppending', '(Backup pending)'), label('sl.backupfail', '(Not backed up)'))))}"
sl_landed="{if(singlepublicsuccessor, label('sl.landed', '(Landed as {shortest(singlepublicsuccessor, sl_hash_minlen)})'))}"
sl_amended="{if(amendsuccessors, label('sl.amended', '(Amended as {join(amendsuccessors % \'{shortest(amendsuccessor, sl_hash_minlen)}\', ', ')})'))}"
sl_rebased="{if(rebasesuccessors, label('sl.rebased', '(Rebased as {join(rebasesuccessors % \'{shortest(rebasesuccessor, sl_hash_minlen)}\', ', ')})'))}"
sl_split="{if(splitsuccessors, label('sl.split', '(Split into {join(splitsuccessors % \'{shortest(splitsuccessor, sl_hash_minlen)}\', ', ')})'))}"
sl_folded="{if(foldsuccessors, label('sl.folded', '(Folded into {join(foldsuccessors % \'{shortest(foldsuccessor, sl_hash_minlen)}\', ', ')})'))}"
sl_histedited="{if(histeditsuccessors, label('sl.histedited', '(Histedited as {join(histeditsuccessors % \'{shortest(histeditsuccessor, sl_hash_minlen)}\', ', ')})'))}"
sl_undone="{if(undosuccessors, label('sl.undone', '(Undone to {join(undosuccessors % \'{shortest(undosuccessor, sl_hash_minlen)}\', ', ')})'))}"
sl_mutation_names=dict(amend="Amended as", rebase="Rebased to", split="Split into", fold="Folded into", histedit="Histedited to", land="Landed as", pushrebase="Landed as")
sl_mutations="{join(mutations % '[{get(sl_mutation_names, operation, \'Rewritten into\')} {join(successors % \'{node|short}\', \', \')}]', ' ')}"
sl_hidden="{ifcontains(rev, revset('hidden()'), label('sl.hiddenlabel', '(hidden)'))}"
sl_label="{ifeq(graphnode, '@', 'sl.current', ifeq(graphnode, 'x', 'sl.hiddennode', ifeq(graphnode, 's', 'sl.snapshotnode')))}"
sl_desclabel="{ifeq(graphnode, '@', 'sl.desc.current', ifeq(graphnode, 'x', 'sl.desc.hiddennode', ifeq(phase, 'public', 'sl.desc.public', 'sl.desc')))}"
sl_shelved="{if(shelveenabled, ifcontains(rev, revset('shelved()'), label('sl.shelvedlabel', '(shelved)')), '')}"
ssl_unsync="{label('ssl.unsync', ifeq(syncstatus, 'unsync', '(local changes)'))}"
sl_stablecommit="{label('sl.stablecommit', smallcommitmeta('arcpull_stable'))}"
sl_node_info="{separate(' ', sl_node, sl_mutations, sl_backup, sl_shelved)}"
sl_node_info_debug="{separate(' ', sl_node_debug, sl_mutations, sl_hidden, sl_backup, sl_shelved, sl_phase_debug)}"
sl_diff_super="{ifeq(graphnode, 's', '', if(sl_diff, separate(' ', label(sl_difflabel, separate(' ', sl_difflink, sl_diffstatus)), label(sl_diffsignallabel, sl_diffsignal), ssl_unsync)))}"
sl_diff_colors="{if(sl_diff, label(sl_difflabel, '{phabdiff}'))}"
sl_header_normal="{separate('  ', sl_userdefined_prefix, sl_node_info, sl_date, sl_user, sl_diff, sl_tasks, sl_books, sl_stablecommit, sl_userdefined_suffix)}"
sl_header_super="{separate('  ', sl_userdefined_prefix, sl_node_info, sl_date, sl_user, sl_diff_super, sl_tasks, sl_books, sl_stablecommit, sl_userdefined_suffix)}"
sl_header_colors="{separate('  ', sl_userdefined_prefix, sl_node_info, sl_date, sl_user, sl_diff_colors, sl_tasks, sl_books, sl_stablecommit, sl_userdefined_suffix)}"
sl_header_debug="{separate('  ', sl_userdefined_prefix, sl_node_info_debug, sl_date, sl_user, sl_diff, sl_tasks, sl_books, sl_stablecommit, sl_userdefined_suffix)}"
sl_header_short="{separate('  ', sl_userdefined_prefix, sl_node_info, sl_date, sl_books, sl_stablecommit, sl_userdefined_suffix)}"
sl_desc="{label(sl_desclabel, truncatelonglines(desc|firstline, termwidth - graphwidth - 2, ellipsis))}"
ellipsis='â€¦'
sl_use_short_header="{ifeq(verbosity, 'verbose', '', ifeq(graphnode, '@', '', ifeq(phase, 'public', ifeq('{username|email}', '{author|email}', '', 'true'))))}"
sl="{label(sl_label, if(sl_use_short_header, sl_header_short, separate('\n', sl_header_normal, sl_desc, '\n')))}"
ssl="{label(sl_label, if(sl_use_short_header, sl_header_short, separate('\n', sl_header_super, sl_desc, '\n')))}"
csl="{label(sl_label, if(sl_use_short_header, sl_header_short, separate('\n', sl_header_colors, sl_desc, '\n')))}"
sl_debug="{label(sl_label, separate('\n', sl_header_debug, sl_desc, '\n'))}"
undo_newwp="{if(oldworkingcopyparent(UNDOINDEX), '(working copy will move here)')}"
undopreview="{separate('\n', separate('  ', undo_node_info, undo_newwp, sl_user, sl_diff, sl_tasks, sl_bookchanges), '{sl_desc}', '\n')}"
sb_date="{date(date, '%x')}"
sb_item="{sb_date} {bookmarks}\n           {desc|firstline}\n"
sb_active="{label('sb.active', sb_item)}"
sb="{if(activebookmark, sb_active, sb_item)}"
sl_cloud_node="{label(sl_phase_label, truncatelonglines(node, sl_hash_minlen))}"
sl_cloud_header_super="{separate('  ', sl_userdefined_prefix, sl_cloud_node, sl_date, sl_user, sl_diff_super, sl_tasks, sl_cloud_books, sl_stablecommit, sl_userdefined_suffix)}"
sl_cloud_header_normal="{separate('  ', sl_userdefined_prefix, sl_cloud_node, sl_date, sl_user, sl_diff, sl_tasks, sl_cloud_books, sl_stablecommit, sl_userdefined_suffix)}"
sl_cloud_header_short="{separate('  ', sl_userdefined_prefix, sl_cloud_node, sl_date, sl_cloud_books, sl_userdefined_suffix)}"
sl_cloud="{label(sl_label, if(sl_use_short_header, sl_cloud_header_short, separate('\n', sl_cloud_header_normal, sl_desc, '\n')))}"
ssl_cloud="{label(sl_label, if(sl_use_short_header, sl_cloud_header_short, separate('\n', sl_cloud_header_super, sl_desc, '\n')))}"
jf_submit_template='\{
 "node": {node|json},
 "date": {date|json},
 "desc": {desc|utf8|json},
 "diff": {diff()|json},
 "files": {files|json},
 "user": {author|utf8|json},
 "parents": ["{p1node}" {ifeq(p2node, "0000000000000000000000000000000000000000","",", \"{p2node}\"")}],
 "publicbase": {singlepublicbase|json},
 "publicbase_svnrev": {revset('last(::%d - not public())', rev) % '{svnrev}'|json},
 "parentrevision": {myparentdiff|json},
 "phase": {phase|json}
 },\n'
sl_backup="{if(enabled('commitcloud'),sl_backupstatus)}"

[ui]
style=sl_default

[revsetalias]
sb(n)=first(sort(bookmark(), -rev), n)
sba=sort(bookmark(), -rev)
top=top()
bottom=bottom()
next=next()
prev=previous
previous=previous()
base=last(public() & ::.)
obsrelated(x)=mutrelated(x)
focusedsmartlog(x)=focusedbranch(x) + draftbranch(x)^ + present(master)
stable=getstablerev()
stable_for($1)=getstablerev($1)

[treestate]
automigrate=True

[treemanifest]
sendtrees=True
treeonly=True
http=True
usecunionstore=False
rustmanifest=True
pullprefetchrevs=master
ondemandfetch=True
prefetchdraftparents=False
useruststore=True

[tweakdefaults]
showupdated=True
defaultdest=remote/master
tagsmessage=NOTE: new tags are disabled in this repository
singlecolonwarn=True
singlecolonmsg=':' is deprecated; use '::' instead.

[ui]
disallowemptyupdate=True
enableincomingoutgoing=False
hyperlink=True
interface=curses
logmeasuredtimes=True
merge:interactive=editmerge
mergemarkers=detailed
origbackuppath=.sl/origbackups
rollback=False
suggesthgprev=True
allowmerge=True
disallowedbrancheshint=use bookmarks instead
threaded=False
merge=internal:merge

[progress]
renderer=rust:simple

[verify]
skipmanifests=True

[visibility]
automigrate=start
enabled=true

[worker]
rustworkers=True
numcpus=4

[clindex]
nodemap=True
verify=False

[committemplate]
changeset={if(desc, desc, emptymsg)}\n
 SL: Enter commit message.  Lines beginning with 'SL:' are removed.
 SL: {extramsg}
 SL: --
 SL: user: {author}\n{ifgt(parents|count, 1,
 "SL: merging:\n{parents % 'SL:   {node|short}: {desc|firstline}\n'}")
 }{if(currentbookmark,
 "SL: bookmark '{currentbookmark}'\n")}{
 filechanges}
defaulttitle=<Replace this line with a title. Use 1 line only, 67 chars or less>
emptymsg={if(title, title, defaulttitle)}\n
 Summary: {summary}\n
 Test Plan: {testplan}\n
filechanges={ifgt(parents|count, 1, filechangesmerge,
 ifgt(files|count, filechangethreshold, filechangesplain, filechangesdetailed))}
filechangesmerge=
filechangethreshold=100
filechangesplain={
 file_adds % "SL: added {file}\n"}{
 file_mods % "SL: changed {file}\n"}{
 file_dels % "SL: removed {file}\n"}{
 if(files, "", "SL: no files changed\n")}
filechangesdetailed={ifeq(verbosity,"verbose",diff()|hgprefix,stat("status")|hgprefix)}\n

[copytrace]
fastcopytrace=True
maxmovescandidatestocheck=0

[perftweaks]
disablecasecheck=True

[profiling:background]
enabled=1
format=text
freq=1
minelapsed=20
output=blackbox
sort=inlinetime
statformat=hotpath
type=stat

[smartlog]
names=master main
repos=remote/
indentnonpublic=True

[rebase]
experimental.inmemory.nomergedriver=False
experimental.inmemory=True

[packs]
maxpacksize=8GB
maxpackfilecount=1024
maxdatapendingbytes=4GB
maxdatabytes=50GB
maxhistorybytes=20GB

[amend]
autorestack=no-conflict
autorestackmsg=automatically restacking children!

[absorb]
amendflag=stack

[hiddenerror]
message=attempted to access hidden commit {0}
hint=use 'sl log -r {0} --hidden' to see more details about this commit

[histedit]
defaultrev=limit(only(.) & draft(), 50)
dropmissing=True
linelen=1000

[morestatus]
show=True

[fastpartialmatch]
generationnumber=5

[clone]
default-destination-dir=$HOME
nativecheckout=True
nativepull=True

[commands]
naked-default.in-repo=sl
naked-default.no-repo=help

[scale]
largeworkingcopy=True

[runlog]
enable=True
boring-commands=debugrunlog config version debugdynamicconfig

[deprecate]
rev-option=true
clone-include-option=true
clone-exclude-option=true

[indexedlog]
data.max-bytes-per-log=7G

[pull]
httpbookmarks=True
httphashprefix=True
master-fastpath=True

[exchange]
httpcommitlookup=True

[scmstore]
backingstore=True
contentstorefallback=False
lfsptrwrites=True
auxindexedlog=True

[nativecheckout]
usescmstore=True

[pager]
pager=internal:streampager

[config]
use-rust=true

[init]
prefer-git=True
"###);


/*
 * 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::BTreeMap;
use std::ops::Range;
use std::path::PathBuf;
use std::str;
use std::sync::Arc;

use minibytes::Text;

use crate::convert::FromConfigValue;
use crate::Error;
use crate::Result;

/// Readable config. This can be used as a trait object.
#[auto_impl::auto_impl(&, Box, Arc)]
pub trait Config: Send + Sync {
    /// Get config names in the given section. Sorted by insertion order.
    fn keys(&self, section: &str) -> Vec<Text>;

    /// Keys with the given prefix.
    fn keys_prefixed(&self, section: &str, prefix: &str) -> Vec<Text> {
        self.keys(section)
            .into_iter()
            .filter(|k| k.starts_with(prefix))
            .collect()
    }

    /// Get config value for a given config.
    /// Return `None` if the config item does not exist or is unset.
    fn get(&self, section: &str, name: &str) -> Option<Text> {
        self.get_considering_unset(section, name)?
    }

    /// Similar to `get`, but can represent "%unset" result.
    /// - `None`: not set or unset.
    /// - `Some(None)`: unset.
    /// - `Some(Some(value))`: set.
    fn get_considering_unset(&self, section: &str, name: &str) -> Option<Option<Text>>;

    /// Get a nonempty config value for a given config.
    /// Return `None` if the config item does not exist, is unset or is empty str.
    fn get_nonempty(&self, section: &str, name: &str) -> Option<Text> {
        self.get(section, name).filter(|v| !v.is_empty())
    }

    /// Get config sections.
    fn sections(&self) -> Cow<[Text]>;

    /// Get the sources of a config.
    fn get_sources(&self, section: &str, name: &str) -> Cow<[ValueSource]>;

    /// Get on-disk files loaded for this `Config`.
    fn files(&self) -> Cow<[PathBuf]> {
        Cow::Borrowed(&[])
    }

    /// Break the config into (immutable) layers.
    ///
    /// If returns an empty list, then the config object is considered atomic.
    ///
    /// If returns a list, then those are considered "sub"-configs that this
    /// config will consider. The order matters. Config with a larger index
    /// overrides configs with smaller indexes. Note the combination of all
    /// sub-configs might not be equivalent to the "self" config, since
    /// there might be some overrides.
    fn layers(&self) -> Vec<Arc<dyn Config>> {
        Vec::new()
    }

    /// The name of the current layer.
    fn layer_name(&self) -> Text;
}

/// Extra APIs (incompatible with trait objects) around reading config.
pub trait ConfigExt: Config {
    /// Get a config item. Convert to type `T`.
    fn get_opt<T: FromConfigValue>(&self, section: &str, name: &str) -> Result<Option<T>> {
        self.get(section, name)
            .map(|bytes| T::try_from_str(&bytes))
            .transpose()
    }

    /// Get a nonempty config item. Convert to type `T`.
    fn get_nonempty_opt<T: FromConfigValue>(&self, section: &str, name: &str) -> Result<Option<T>> {
        self.get_nonempty(section, name)
            .map(|bytes| T::try_from_str(&bytes))
            .transpose()
    }

    /// Get a config item. Convert to type `T`.
    ///
    /// If the config item is not set, calculate it using `default_func`.
    fn get_or<T: FromConfigValue>(
        &self,
        section: &str,
        name: &str,
        default_func: impl Fn() -> T,
    ) -> Result<T> {
        Ok(self.get_opt(section, name)?.unwrap_or_else(default_func))
    }

    /// Get a config item. Convert to type `T`.
    ///
    /// If the config item is not set, return `T::default()`.
    fn get_or_default<T: Default + FromConfigValue>(&self, section: &str, name: &str) -> Result<T> {
        self.get_or(section, name, Default::default)
    }

    /// Get a config item. Convert to type `T`.
    ///
    /// If the config item is not set, return Error::NotSet.
    fn must_get<T: Default + FromConfigValue>(&self, section: &str, name: &str) -> Result<T> {
        match self.get_nonempty_opt(section, name)? {
            Some(val) => Ok(val),
            None => Err(Error::NotSet(section.to_string(), name.to_string())),
        }
    }
}

impl<T: Config> ConfigExt for T {}

impl Config for BTreeMap<&str, &str> {
    fn keys(&self, section: &str) -> Vec<Text> {
        let prefix = format!("{}.", section);
        BTreeMap::keys(self)
            .filter_map(|k| k.strip_prefix(&prefix).map(|k| k.to_string().into()))
            .collect()
    }

    fn sections(&self) -> Cow<[Text]> {
        let mut sections = Vec::new();
        let mut last_section = None;
        for section in BTreeMap::keys(self).filter_map(|k| k.split('.').next()) {
            if Some(section) != last_section {
                last_section = Some(section);
                sections.push(Text::from(section.to_string()));
            }
        }
        Cow::Owned(sections)
    }

    fn get_considering_unset(&self, section: &str, name: &str) -> Option<Option<Text>> {
        let key: &str = &format!("{}.{}", section, name);
        BTreeMap::get(self, &key).map(|v| Some(v.to_string().into()))
    }

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

    fn layer_name(&self) -> Text {
        Text::from_static("BTreeMap")
    }
}

impl Config for BTreeMap<String, String> {
    fn keys(&self, section: &str) -> Vec<Text> {
        let prefix = format!("{}.", section);
        BTreeMap::keys(self)
            .filter_map(|k| k.strip_prefix(&prefix).map(|k| k.to_string().into()))
            .collect()
    }

    fn sections(&self) -> Cow<[Text]> {
        let mut sections = Vec::new();
        let mut last_section = None;
        for section in BTreeMap::keys(self).filter_map(|k| k.split('.').next()) {
            if Some(section) != last_section {
                last_section = Some(section);
                sections.push(Text::from(section.to_string()));
            }
        }
        Cow::Owned(sections)
    }

    fn get_considering_unset(&self, section: &str, name: &str) -> Option<Option<Text>> {
        BTreeMap::get(self, &format!("{}.{}", section, name)).map(|v| Some(v.clone().into()))
    }

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

    fn layer_name(&self) -> Text {
        Text::from_static("BTreeMap")
    }
}

/// A config value with associated metadata like where it comes from.
#[derive(Clone, Debug)]
pub struct ValueSource {
    pub value: Option<Text>,
    pub source: Text, // global, user, repo, "--config", or an extension name, etc.
    pub location: Option<ValueLocation>,
}

/// The on-disk file name and byte offsets that provide the config value.
/// Useful if applications want to edit config values in-place.
#[derive(Clone, Debug)]
pub struct ValueLocation {
    pub path: Arc<PathBuf>,
    pub content: Text,
    pub location: Range<usize>,
}

impl ValueSource {
    /// Return the actual value stored in this config value, or `None` if unset.
    pub fn value(&self) -> &Option<Text> {
        &self.value
    }

    /// Return the "source" information for the config value. It's usually who sets the config,
    /// like "--config", "user_hgrc", "system_hgrc", etc.
    pub fn source(&self) -> &Text {
        &self.source
    }

    /// Return the file path and byte range for the exact config value,
    /// or `None` if there is no such information.
    ///
    /// If the value is `None`, the byte range is for the "%unset" statement.
    pub fn location(&self) -> Option<(PathBuf, Range<usize>)> {
        self.location
            .as_ref()
            .map(|src| (src.path.as_ref().to_path_buf(), src.location.clone()))
    }

    /// Return the file content. Or `None` if there is no such information.
    pub fn file_content(&self) -> Option<Text> {
        self.location.as_ref().map(|src| src.content.clone())
    }
}

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

    fn wants_impl(_: impl Config) {}

    #[test]
    fn test_btreemap_config() {
        let map: BTreeMap<&str, &str> = vec![("foo.bar", "baz")].into_iter().collect();
        assert_eq!(format!("{:?}", Config::keys(&map, "foo")), "[\"bar\"]");
        assert_eq!(
            format!("{:?}", Config::get(&map, "foo", "bar")),
            "Some(\"baz\")"
        );
        assert_eq!(format!("{:?}", Config::get(&map, "foo", "baz")), "None");

        // Make sure we can pass BTreeMap config to generic func.
        wants_impl(&map);
    }

    #[test]
    fn test_must_get() {
        let map: BTreeMap<&str, &str> = vec![("foo.bar", "baz")].into_iter().collect();
        assert_eq!(
            map.must_get::<Vec<String>>("foo", "bar").unwrap(),
            vec!["baz".to_string()]
        );
        assert!(matches!(
            map.must_get::<Vec<String>>("foo", "nope"),
            Err(Error::NotSet(_, _))
        ));
    }
}