// 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::Arc;use std::sync::LazyLock;use std::sync::Mutex; 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::AliasDeclarationParser;use jj_lib::dsl_util::AliasesMap;use jj_lib::secure_config::LoadedSecureConfig;use jj_lib::secure_config::SecureConfig;use rand::SeedableRng as _;use rand_chacha::ChaCha20Rng;use regex::Captures;use regex::Regex;use serde::Serialize as _;use tracing::instrument; use crate::command_error::CommandError;use crate::command_error::config_error;use crate::command_error::config_error_with_message;use crate::ui::Ui; // TODO(#879): Consider generating entire schema dynamically vs. static file.pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json"); const REPO_CONFIG_DIR: &str = "repos";const WORKSPACE_CONFIG_DIR: &str = "workspaces"; /// 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 }} /// Converts [`ConfigValue`] (or [`toml_edit::Value`]) to [`toml::Value`] which/// implements [`serde::Serialize`].pub fn to_serializable_value(value: ConfigValue) -> toml::Value { match value { ConfigValue::String(v) => toml::Value::String(v.into_value()), ConfigValue::Integer(v) => toml::Value::Integer(v.into_value()), ConfigValue::Float(v) => toml::Value::Float(v.into_value()), ConfigValue::Boolean(v) => toml::Value::Boolean(v.into_value()), ConfigValue::Datetime(v) => toml::Value::Datetime(v.into_value()), ConfigValue::Array(array) => { let array = array.into_iter().map(to_serializable_value).collect(); toml::Value::Array(array) } ConfigValue::InlineTable(table) => { let table = table .into_iter() .map(|(k, v)| (k, to_serializable_value(v))) .collect(); toml::Value::Table(table) } }} /// Configuration variable with its source information.#[derive(Clone, Debug, serde::Serialize)]pub struct AnnotatedValue { /// Dotted name path to the configuration variable. #[serde(serialize_with = "serialize_name")] pub name: ConfigNamePathBuf, /// Configuration value. #[serde(serialize_with = "serialize_value")] pub value: ConfigValue, /// Source of the configuration value. #[serde(serialize_with = "serialize_source")] 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,} fn serialize_name<S>(name: &ConfigNamePathBuf, serializer: S) -> Result<S::Ok, S::Error>where S: serde::Serializer,{ name.to_string().serialize(serializer)} fn serialize_value<S>(value: &ConfigValue, serializer: S) -> Result<S::Ok, S::Error>where S: serde::Serializer,{ to_serializable_value(value.clone()).serialize(serializer)} fn serialize_source<S>(source: &ConfigSource, serializer: S) -> Result<S::Ok, S::Error>where S: serde::Serializer,{ source.to_string().serialize(serializer)} /// 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::*; Self { 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>, home_dir: Option<PathBuf>, jj_config: Option<String>,} impl UnresolvedConfigEnv { fn root_config_dir(&self) -> Option<PathBuf> { self.config_dir.as_deref().map(|c| c.join("jj")) } fn resolve(self) -> 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) }); if let Some(path) = home_config_path && (path.exists() || 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); } if let Some(path) = platform_config_dir && path.exists() { paths.push(path); } paths }} #[derive(Clone, Debug)]pub struct ConfigEnv { home_dir: Option<PathBuf>, root_config_dir: Option<PathBuf>, repo_path: Option<PathBuf>, workspace_path: Option<PathBuf>, user_config_paths: Vec<ConfigPath>, repo_config: Option<SecureConfig>, workspace_config: Option<SecureConfig>, command: Option<String>, hostname: Option<String>, rng: Arc<Mutex<ChaCha20Rng>>,} impl ConfigEnv { /// Initializes configuration loader based on environment variables. pub fn from_environment() -> Self { let config_dir = etcetera::choose_base_strategy() .ok() .map(|s| s.config_dir()); // 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, home_dir: home_dir.clone(), jj_config: env::var("JJ_CONFIG").ok(), }; Self { home_dir, root_config_dir: env.root_config_dir(), repo_path: None, workspace_path: None, user_config_paths: env.resolve(), repo_config: None, workspace_config: None, command: None, hostname: whoami::hostname().ok(), // We would ideally use JjRng, but that requires the seed from the // config, which requires the config to be loaded. rng: Arc::new(Mutex::new( if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<u64>()) { ChaCha20Rng::seed_from_u64(value) } else { rand::make_rng() }, )), } } pub fn set_command_name(&mut self, command: String) { self.command = Some(command); } fn load_secure_config( &self, ui: &Ui, config: Option<&SecureConfig>, kind: &str, force: bool, ) -> Result<Option<LoadedSecureConfig>, CommandError> { Ok(match (config, self.root_config_dir.as_ref()) { (Some(config), Some(root_config_dir)) => { let mut guard = self.rng.lock().unwrap(); let loaded_config = if force { config.load_config(&mut guard, &root_config_dir.join(kind)) } else { config.maybe_load_config(&mut guard, &root_config_dir.join(kind)) }?; for warning in &loaded_config.warnings { writeln!(ui.warning_default(), "{warning}")?; } Some(loaded_config) } _ => None, }) } /// 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>, CommandError> { config_files_for(config, ConfigSource::User, || { Ok(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 the repo-specific config file is stored. The /// path is usually `$REPO/.jj/repo`. pub fn reset_repo_path(&mut self, path: &Path) { self.repo_config = Some(SecureConfig::new_repo(path.to_path_buf())); self.repo_path = Some(path.to_owned()); } /// Returns a path to the existing repo-specific config file. fn maybe_repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> { Ok(self .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, false)? .and_then(|c| c.config_file)) } /// Returns a path to the existing repo-specific config file. /// If the config file does not exist, will create a new config ID and /// create a new directory for this. pub fn repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> { Ok(self .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, true)? .and_then(|c| c.config_file)) } /// 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` shouldLines 1–500 of 1870. Load more
// 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::Arc; use std::sync::LazyLock; use std::sync::Mutex; 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::AliasDeclarationParser; use jj_lib::dsl_util::AliasesMap; use jj_lib::secure_config::LoadedSecureConfig; use jj_lib::secure_config::SecureConfig; use rand::SeedableRng as _; use rand_chacha::ChaCha20Rng; use regex::Captures; use regex::Regex; use serde::Serialize as _; use tracing::instrument; use crate::command_error::CommandError; use crate::command_error::config_error; use crate::command_error::config_error_with_message; use crate::ui::Ui; // TODO(#879): Consider generating entire schema dynamically vs. static file. pub const CONFIG_SCHEMA: &str = include_str!("config-schema.json"); const REPO_CONFIG_DIR: &str = "repos"; const WORKSPACE_CONFIG_DIR: &str = "workspaces"; /// 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 } } /// Converts [`ConfigValue`] (or [`toml_edit::Value`]) to [`toml::Value`] which /// implements [`serde::Serialize`]. pub fn to_serializable_value(value: ConfigValue) -> toml::Value { match value { ConfigValue::String(v) => toml::Value::String(v.into_value()), ConfigValue::Integer(v) => toml::Value::Integer(v.into_value()), ConfigValue::Float(v) => toml::Value::Float(v.into_value()), ConfigValue::Boolean(v) => toml::Value::Boolean(v.into_value()), ConfigValue::Datetime(v) => toml::Value::Datetime(v.into_value()), ConfigValue::Array(array) => { let array = array.into_iter().map(to_serializable_value).collect(); toml::Value::Array(array) } ConfigValue::InlineTable(table) => { let table = table .into_iter() .map(|(k, v)| (k, to_serializable_value(v))) .collect(); toml::Value::Table(table) } } } /// Configuration variable with its source information. #[derive(Clone, Debug, serde::Serialize)] pub struct AnnotatedValue { /// Dotted name path to the configuration variable. #[serde(serialize_with = "serialize_name")] pub name: ConfigNamePathBuf, /// Configuration value. #[serde(serialize_with = "serialize_value")] pub value: ConfigValue, /// Source of the configuration value. #[serde(serialize_with = "serialize_source")] 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, } fn serialize_name<S>(name: &ConfigNamePathBuf, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { name.to_string().serialize(serializer) } fn serialize_value<S>(value: &ConfigValue, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { to_serializable_value(value.clone()).serialize(serializer) } fn serialize_source<S>(source: &ConfigSource, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { source.to_string().serialize(serializer) } /// 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::*; Self { 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>, home_dir: Option<PathBuf>, jj_config: Option<String>, } impl UnresolvedConfigEnv { fn root_config_dir(&self) -> Option<PathBuf> { self.config_dir.as_deref().map(|c| c.join("jj")) } fn resolve(self) -> 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) }); if let Some(path) = home_config_path && (path.exists() || 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); } if let Some(path) = platform_config_dir && path.exists() { paths.push(path); } paths } } #[derive(Clone, Debug)] pub struct ConfigEnv { home_dir: Option<PathBuf>, root_config_dir: Option<PathBuf>, repo_path: Option<PathBuf>, workspace_path: Option<PathBuf>, user_config_paths: Vec<ConfigPath>, repo_config: Option<SecureConfig>, workspace_config: Option<SecureConfig>, command: Option<String>, hostname: Option<String>, rng: Arc<Mutex<ChaCha20Rng>>, } impl ConfigEnv { /// Initializes configuration loader based on environment variables. pub fn from_environment() -> Self { let config_dir = etcetera::choose_base_strategy() .ok() .map(|s| s.config_dir()); // 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, home_dir: home_dir.clone(), jj_config: env::var("JJ_CONFIG").ok(), }; Self { home_dir, root_config_dir: env.root_config_dir(), repo_path: None, workspace_path: None, user_config_paths: env.resolve(), repo_config: None, workspace_config: None, command: None, hostname: whoami::hostname().ok(), // We would ideally use JjRng, but that requires the seed from the // config, which requires the config to be loaded. rng: Arc::new(Mutex::new( if let Ok(Ok(value)) = env::var("JJ_RANDOMNESS_SEED").map(|s| s.parse::<u64>()) { ChaCha20Rng::seed_from_u64(value) } else { rand::make_rng() }, )), } } pub fn set_command_name(&mut self, command: String) { self.command = Some(command); } fn load_secure_config( &self, ui: &Ui, config: Option<&SecureConfig>, kind: &str, force: bool, ) -> Result<Option<LoadedSecureConfig>, CommandError> { Ok(match (config, self.root_config_dir.as_ref()) { (Some(config), Some(root_config_dir)) => { let mut guard = self.rng.lock().unwrap(); let loaded_config = if force { config.load_config(&mut guard, &root_config_dir.join(kind)) } else { config.maybe_load_config(&mut guard, &root_config_dir.join(kind)) }?; for warning in &loaded_config.warnings { writeln!(ui.warning_default(), "{warning}")?; } Some(loaded_config) } _ => None, }) } /// 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>, CommandError> { config_files_for(config, ConfigSource::User, || { Ok(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 the repo-specific config file is stored. The /// path is usually `$REPO/.jj/repo`. pub fn reset_repo_path(&mut self, path: &Path) { self.repo_config = Some(SecureConfig::new_repo(path.to_path_buf())); self.repo_path = Some(path.to_owned()); } /// Returns a path to the existing repo-specific config file. fn maybe_repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> { Ok(self .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, false)? .and_then(|c| c.config_file)) } /// Returns a path to the existing repo-specific config file. /// If the config file does not exist, will create a new config ID and /// create a new directory for this. pub fn repo_config_path(&self, ui: &Ui) -> Result<Option<PathBuf>, CommandError> { Ok(self .load_secure_config(ui, self.repo_config.as_ref(), REPO_CONFIG_DIR, true)? .and_then(|c| c.config_file)) } /// 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