1782 lines · 61.2 KB · Rust
RawDownload
// Copyright 2022 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::borrow::Cow;use std::collections::BTreeSet;use std::collections::HashMap;use std::env;use std::env::split_paths;use std::fmt;use std::path::Path;use std::path::PathBuf;use std::process::Command;use std::sync::LazyLock; use etcetera::BaseStrategy as _;use itertools::Itertools as _;use jj_lib::config::ConfigFile;use jj_lib::config::ConfigGetError;use jj_lib::config::ConfigLayer;use jj_lib::config::ConfigLoadError;use jj_lib::config::ConfigMigrationRule;use jj_lib::config::ConfigNamePathBuf;use jj_lib::config::ConfigResolutionContext;use jj_lib::config::ConfigSource;use jj_lib::config::ConfigValue;use jj_lib::config::StackedConfig;use jj_lib::dsl_util;use regex::Captures;use regex::Regex;use tracing::instrument; use crate::command_error::config_error;use crate::command_error::config_error_with_message;use crate::command_error::CommandError;use crate::text_util;use crate::ui::Ui; // TODO(#879): Consider generating entire schema dynamically vs. static file.pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json"); /// Parses a TOML value expression. Interprets the given value as string if it/// can't be parsed and doesn't look like a TOML expression.pub fn parse_value_or_bare_string(value_str: &str) -> Result<ConfigValue, toml_edit::TomlError> {    match value_str.parse() {        Ok(value) => Ok(value),        Err(_) if is_bare_string(value_str) => Ok(value_str.into()),        Err(err) => Err(err),    }} fn is_bare_string(value_str: &str) -> bool {    // leading whitespace isn't ignored when parsing TOML value expression, but    // "\n[]" doesn't look like a bare string.    let trimmed = value_str.trim_ascii().as_bytes();    if let (Some(&first), Some(&last)) = (trimmed.first(), trimmed.last()) {        // string, array, or table constructs?        !matches!(first, b'"' | b'\'' | b'[' | b'{') && !matches!(last, b'"' | b'\'' | b']' | b'}')    } else {        true // empty or whitespace only    }} /// Configuration variable with its source information.#[derive(Clone, Debug)]pub struct AnnotatedValue {    /// Dotted name path to the configuration variable.    pub name: ConfigNamePathBuf,    /// Configuration value.    pub value: ConfigValue,    /// Source of the configuration value.    pub source: ConfigSource,    /// Path to the source file, if available.    pub path: Option<PathBuf>,    /// True if this value is overridden in higher precedence layers.    pub is_overridden: bool,} /// Collects values under the given `filter_prefix` name recursively, from all/// layers.pub fn resolved_config_values(    stacked_config: &StackedConfig,    filter_prefix: &ConfigNamePathBuf,) -> Vec<AnnotatedValue> {    // Collect annotated values in reverse order and mark each value shadowed by    // value or table in upper layers.    let mut config_vals = vec![];    let mut upper_value_names = BTreeSet::new();    for layer in stacked_config.layers().iter().rev() {        let top_item = match layer.look_up_item(filter_prefix) {            Ok(Some(item)) => item,            Ok(None) => continue, // parent is a table, but no value found            Err(_) => {                // parent is not a table, shadows lower layers                upper_value_names.insert(filter_prefix.clone());                continue;            }        };        let mut config_stack = vec![(filter_prefix.clone(), top_item, false)];        while let Some((name, item, is_parent_overridden)) = config_stack.pop() {            // Cannot retain inline table formatting because inner values may be            // overridden independently.            if let Some(table) = item.as_table_like() {                // current table and children may be shadowed by value in upper layer                let is_overridden = is_parent_overridden || upper_value_names.contains(&name);                for (k, v) in table.iter() {                    let mut sub_name = name.clone();                    sub_name.push(k);                    config_stack.push((sub_name, v, is_overridden)); // in reverse order                }            } else {                // current value may be shadowed by value or table in upper layer                let maybe_child = upper_value_names                    .range(&name..)                    .next()                    .filter(|next| next.starts_with(&name));                let is_overridden = is_parent_overridden || maybe_child.is_some();                if maybe_child != Some(&name) {                    upper_value_names.insert(name.clone());                }                let value = item                    .clone()                    .into_value()                    .expect("Item::None should not exist in table");                config_vals.push(AnnotatedValue {                    name,                    value,                    source: layer.source,                    path: layer.path.clone(),                    is_overridden,                });            }        }    }    config_vals.reverse();    config_vals} /// Newtype for unprocessed (or unresolved) [`StackedConfig`].////// This doesn't provide any strict guarantee about the underlying config/// object. It just requires an explicit cast to access to the config object.#[derive(Clone, Debug)]pub struct RawConfig(StackedConfig); impl AsRef<StackedConfig> for RawConfig {    fn as_ref(&self) -> &StackedConfig {        &self.0    }} impl AsMut<StackedConfig> for RawConfig {    fn as_mut(&mut self) -> &mut StackedConfig {        &mut self.0    }} #[derive(Clone, Debug)]enum ConfigPathState {    New,    Exists,} /// A ConfigPath can be in one of two states:////// - exists(): a config file exists at the path/// - !exists(): a config file doesn't exist here, but a new file _can_ be///   created at this path#[derive(Clone, Debug)]struct ConfigPath {    path: PathBuf,    state: ConfigPathState,} impl ConfigPath {    fn new(path: PathBuf) -> Self {        use ConfigPathState::*;        ConfigPath {            state: if path.exists() { Exists } else { New },            path,        }    }     fn as_path(&self) -> &Path {        &self.path    }    fn exists(&self) -> bool {        match self.state {            ConfigPathState::Exists => true,            ConfigPathState::New => false,        }    }} /// Like std::fs::create_dir_all but creates new directories to be accessible to/// the user only on Unix (chmod 700).fn create_dir_all(path: &Path) -> std::io::Result<()> {    let mut dir = std::fs::DirBuilder::new();    dir.recursive(true);    #[cfg(unix)]    {        use std::os::unix::fs::DirBuilderExt as _;        dir.mode(0o700);    }    dir.create(path)} // The struct exists so that we can mock certain global values in unit tests.#[derive(Clone, Default, Debug)]struct UnresolvedConfigEnv {    config_dir: Option<PathBuf>,    // TODO: remove after jj 0.35    macos_legacy_config_dir: Option<PathBuf>,    home_dir: Option<PathBuf>,    jj_config: Option<String>,} impl UnresolvedConfigEnv {    fn resolve(self, ui: &Ui) -> Vec<ConfigPath> {        if let Some(paths) = self.jj_config {            return split_paths(&paths)                .filter(|path| !path.as_os_str().is_empty())                .map(ConfigPath::new)                .collect();        }         let mut paths = vec![];        let home_config_path = self.home_dir.map(|mut home_dir| {            home_dir.push(".jjconfig.toml");            ConfigPath::new(home_dir)        });        let platform_config_path = self.config_dir.clone().map(|mut config_dir| {            config_dir.push("jj");            config_dir.push("config.toml");            ConfigPath::new(config_dir)        });        let platform_config_dir = self.config_dir.map(|mut config_dir| {            config_dir.push("jj");            config_dir.push("conf.d");            ConfigPath::new(config_dir)        });        let legacy_platform_config_path =            self.macos_legacy_config_dir.clone().map(|mut config_dir| {                config_dir.push("jj");                config_dir.push("config.toml");                ConfigPath::new(config_dir)            });        let legacy_platform_config_dir = self.macos_legacy_config_dir.map(|mut config_dir| {            config_dir.push("jj");            config_dir.push("conf.d");            ConfigPath::new(config_dir)        });         if let Some(path) = home_config_path {            if path.exists()                || (platform_config_path.is_none() && legacy_platform_config_path.is_none())            {                paths.push(path);            }        }         // This should be the default config created if there's        // no user config and `jj config edit` is executed.        if let Some(path) = platform_config_path {            paths.push(path);        }         // theoretically these should be an `if let Some(...) = ... && ..., but that        // isn't stable        if let Some(path) = platform_config_dir {            if path.exists() {                paths.push(path);            }        }         if let Some(path) = legacy_platform_config_path {            if path.exists() {                Self::warn_for_deprecated_path(                    ui,                    path.as_path(),                    "~/Library/Application Support/jj",                    "~/.config/jj",                );                paths.push(path);            }        }        if let Some(path) = legacy_platform_config_dir {            if path.exists() {                Self::warn_for_deprecated_path(                    ui,                    path.as_path(),                    "~/Library/Application Support/jj",                    "~/.config/jj",                );                paths.push(path);            }        }         paths    }     fn warn_for_deprecated_path(ui: &Ui, path: &Path, old: &str, new: &str) {        let _ = indoc::writedoc!(            ui.warning_default(),            r"            Deprecated configuration file `{}`.            Configuration files in `{old}` are deprecated, and support will be removed in a future release.            Instead, move your configuration files to `{new}`.            ",            path.display(),        );    }} #[derive(Clone, Debug)]pub struct ConfigEnv {    home_dir: Option<PathBuf>,    repo_path: Option<PathBuf>,    user_config_paths: Vec<ConfigPath>,    repo_config_path: Option<ConfigPath>,    command: Option<String>,} impl ConfigEnv {    /// Initializes configuration loader based on environment variables.    pub fn from_environment(ui: &Ui) -> Self {        let config_dir = etcetera::choose_base_strategy()            .ok()            .map(|s| s.config_dir());         // older versions of jj used a more "GUI" config option,        // which is not designed for user-editable configuration of CLI utilities.        let macos_legacy_config_dir = if cfg!(target_os = "macos") {            etcetera::base_strategy::choose_native_strategy()                .ok()                .map(|s| {                    // note that etcetera calls Library/Application Support the "data dir",                    // Library/Preferences is supposed to be exclusively plists                    s.data_dir()                })                .filter(|data_dir| {                    // User might've purposefully set their config dir to the deprecated one                    Some(data_dir) != config_dir.as_ref()                })        } else {            None        };         // Canonicalize home as we do canonicalize cwd in CliRunner. $HOME might        // point to symlink.        let home_dir = etcetera::home_dir()            .ok()            .map(|d| dunce::canonicalize(&d).unwrap_or(d));         let env = UnresolvedConfigEnv {            config_dir,            macos_legacy_config_dir,            home_dir: home_dir.clone(),            jj_config: env::var("JJ_CONFIG").ok(),        };        ConfigEnv {            home_dir,            repo_path: None,            user_config_paths: env.resolve(ui),            repo_config_path: None,            command: None,        }    }     pub fn set_command_name(&mut self, command: String) {        self.command = Some(command);    }     /// Returns the paths to the user-specific config files or directories.    pub fn user_config_paths(&self) -> impl Iterator<Item = &Path> {        self.user_config_paths.iter().map(ConfigPath::as_path)    }     /// Returns the paths to the existing user-specific config files or    /// directories.    pub fn existing_user_config_paths(&self) -> impl Iterator<Item = &Path> {        self.user_config_paths            .iter()            .filter(|p| p.exists())            .map(ConfigPath::as_path)    }     /// Returns user configuration files for modification. Instantiates one if    /// `config` has no user configuration layers.    ///    /// The parent directory for the new file may be created by this function.    /// If the user configuration path is unknown, this function returns an    /// empty `Vec`.    pub fn user_config_files(        &self,        config: &RawConfig,    ) -> Result<Vec<ConfigFile>, ConfigLoadError> {        config_files_for(config, ConfigSource::User, || self.new_user_config_file())    }     fn new_user_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {        self.user_config_paths()            .next()            .map(|path| {                // No need to propagate io::Error here. If the directory                // couldn't be created, file.save() would fail later.                if let Some(dir) = path.parent() {                    create_dir_all(dir).ok();                }                // The path doesn't usually exist, but we shouldn't overwrite it                // with an empty config if it did exist.                ConfigFile::load_or_empty(ConfigSource::User, path)            })            .transpose()    }     /// Loads user-specific config files into the given `config`. The old    /// user-config layers will be replaced if any.    #[instrument]    pub fn reload_user_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {        config.as_mut().remove_layers(ConfigSource::User);        for path in self.existing_user_config_paths() {            if path.is_dir() {                config.as_mut().load_dir(ConfigSource::User, path)?;            } else {                config.as_mut().load_file(ConfigSource::User, path)?;            }        }        Ok(())    }     /// Sets the directory where repo-specific config file is stored. The path    /// is usually `.jj/repo`.    pub fn reset_repo_path(&mut self, path: &Path) {        self.repo_path = Some(path.to_owned());        self.repo_config_path = Some(ConfigPath::new(path.join("config.toml")));    }     /// Returns a path to the repo-specific config file.    pub fn repo_config_path(&self) -> Option<&Path> {        self.repo_config_path.as_ref().map(|p| p.as_path())    }     /// Returns a path to the existing repo-specific config file.    fn existing_repo_config_path(&self) -> Option<&Path> {        match self.repo_config_path {            Some(ref path) if path.exists() => Some(path.as_path()),            _ => None,        }    }     /// Returns repo configuration files for modification. Instantiates one if    /// `config` has no repo configuration layers.    ///    /// If the repo path is unknown, this function returns an empty `Vec`. Since    /// the repo config path cannot be a directory, the returned `Vec` should    /// have at most one config file.    pub fn repo_config_files(        &self,        config: &RawConfig,    ) -> Result<Vec<ConfigFile>, ConfigLoadError> {        config_files_for(config, ConfigSource::Repo, || self.new_repo_config_file())    }     fn new_repo_config_file(&self) -> Result<Option<ConfigFile>, ConfigLoadError> {        self.repo_config_path()            // The path doesn't usually exist, but we shouldn't overwrite it            // with an empty config if it did exist.            .map(|path| ConfigFile::load_or_empty(ConfigSource::Repo, path))            .transpose()    }     /// Loads repo-specific config file into the given `config`. The old    /// repo-config layer will be replaced if any.    #[instrument]    pub fn reload_repo_config(&self, config: &mut RawConfig) -> Result<(), ConfigLoadError> {        config.as_mut().remove_layers(ConfigSource::Repo);        if let Some(path) = self.existing_repo_config_path() {            config.as_mut().load_file(ConfigSource::Repo, path)?;        }        Ok(())    }     /// Resolves conditional scopes within the current environment. Returns new    /// resolved config.    pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {        let context = ConfigResolutionContext {            home_dir: self.home_dir.as_deref(),            repo_path: self.repo_path.as_deref(),            command: self.command.as_deref(),

Lines 1–500 of 1782.