fix(config): load user config
Some checks failed
Publish Docker Images / coverage-and-sonar (push) Failing after 9m51s
Some checks failed
Publish Docker Images / coverage-and-sonar (push) Failing after 9m51s
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1667,6 +1667,7 @@ dependencies = [
|
||||
"assert_fs",
|
||||
"async-trait",
|
||||
"clap",
|
||||
"etcetera",
|
||||
"git-conventional",
|
||||
"inquire",
|
||||
"jj-lib",
|
||||
|
||||
@@ -22,6 +22,7 @@ test-utils = []
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.89"
|
||||
etcetera = "0.11.0"
|
||||
clap = { version = "4.5.57", features = ["derive"] }
|
||||
git-conventional = "0.12.9"
|
||||
inquire = "0.9.2"
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use etcetera::BaseStrategy;
|
||||
use jj_lib::{
|
||||
config::StackedConfig,
|
||||
config::{ConfigSource, StackedConfig},
|
||||
ref_name::WorkspaceName,
|
||||
repo::{Repo, StoreFactories},
|
||||
settings::UserSettings,
|
||||
@@ -37,9 +38,62 @@ impl JjLib {
|
||||
}
|
||||
|
||||
fn load_settings() -> Result<UserSettings, Error> {
|
||||
let config = StackedConfig::with_defaults();
|
||||
let mut config = StackedConfig::with_defaults();
|
||||
for path in Self::user_config_paths() {
|
||||
if path.is_dir() {
|
||||
config
|
||||
.load_dir(ConfigSource::User, &path)
|
||||
.map_err(|_| Error::FailedReadingConfig)?;
|
||||
} else if path.exists() {
|
||||
config
|
||||
.load_file(ConfigSource::User, path)
|
||||
.map_err(|_| Error::FailedReadingConfig)?;
|
||||
}
|
||||
}
|
||||
UserSettings::from_config(config).map_err(|_| Error::FailedReadingConfig)
|
||||
}
|
||||
|
||||
/// Resolves user config file paths following the same logic as the jj CLI:
|
||||
/// 1. `$JJ_CONFIG` (colon-separated list), if set
|
||||
/// 2. `~/.jjconfig.toml` (if it exists, or no platform config dir is available)
|
||||
/// 3. Platform config dir `/jj/config.toml` (e.g. `~/.config/jj/config.toml`)
|
||||
/// 4. Platform config dir `/jj/conf.d/` (if it exists)
|
||||
fn user_config_paths() -> Vec<PathBuf> {
|
||||
if let Ok(paths) = std::env::var("JJ_CONFIG") {
|
||||
return std::env::split_paths(&paths)
|
||||
.filter(|p| !p.as_os_str().is_empty())
|
||||
.collect();
|
||||
}
|
||||
|
||||
let home_dir = etcetera::home_dir().ok();
|
||||
let config_dir = etcetera::choose_base_strategy()
|
||||
.ok()
|
||||
.map(|s| s.config_dir());
|
||||
|
||||
let home_config = home_dir.as_ref().map(|h| h.join(".jjconfig.toml"));
|
||||
let platform_config = config_dir
|
||||
.as_ref()
|
||||
.map(|c| c.join("jj").join("config.toml"));
|
||||
let platform_conf_d = config_dir.as_ref().map(|c| c.join("jj").join("conf.d"));
|
||||
|
||||
let mut paths = Vec::new();
|
||||
|
||||
if let Some(path) = home_config
|
||||
&& (path.exists() || platform_config.is_none())
|
||||
{
|
||||
paths.push(path);
|
||||
}
|
||||
if let Some(path) = platform_config {
|
||||
paths.push(path);
|
||||
}
|
||||
if let Some(path) = platform_conf_d
|
||||
&& path.exists()
|
||||
{
|
||||
paths.push(path);
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
@@ -312,4 +366,155 @@ mod tests {
|
||||
fn accepts_executor(_: impl JjExecutor) {}
|
||||
accepts_executor(lib);
|
||||
}
|
||||
|
||||
mod user_config_paths_tests {
|
||||
use super::*;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
/// Serialize all tests that mutate environment variables to prevent
|
||||
/// data races: env vars are process-global in Rust's test runner.
|
||||
static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
|
||||
|
||||
/// RAII guard that restores an environment variable on drop.
|
||||
struct EnvGuard {
|
||||
key: &'static str,
|
||||
original: Option<String>,
|
||||
}
|
||||
|
||||
impl EnvGuard {
|
||||
fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {
|
||||
let original = std::env::var(key).ok();
|
||||
// SAFETY: tests hold ENV_LOCK, so no concurrent env mutation
|
||||
unsafe { std::env::set_var(key, value) };
|
||||
Self { key, original }
|
||||
}
|
||||
|
||||
fn remove(key: &'static str) -> Self {
|
||||
let original = std::env::var(key).ok();
|
||||
// SAFETY: tests hold ENV_LOCK, so no concurrent env mutation
|
||||
unsafe { std::env::remove_var(key) };
|
||||
Self { key, original }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvGuard {
|
||||
fn drop(&mut self) {
|
||||
match &self.original {
|
||||
// SAFETY: tests hold ENV_LOCK, so no concurrent env mutation
|
||||
Some(v) => unsafe { std::env::set_var(self.key, v) },
|
||||
None => unsafe { std::env::remove_var(self.key) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jj_config_single_path_returned_directly() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let config_path = temp.path().join("config.toml");
|
||||
|
||||
let _jj = EnvGuard::set("JJ_CONFIG", &config_path);
|
||||
|
||||
assert_eq!(JjLib::user_config_paths(), vec![config_path]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jj_config_multiple_paths_all_returned() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let path_a = temp.path().join("a.toml");
|
||||
let path_b = temp.path().join("b.toml");
|
||||
let combined = std::env::join_paths([&path_a, &path_b]).unwrap();
|
||||
|
||||
let _jj = EnvGuard::set("JJ_CONFIG", &combined);
|
||||
|
||||
assert_eq!(JjLib::user_config_paths(), vec![path_a, path_b]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jj_config_empty_segments_are_filtered_out() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let config_path = temp.path().join("config.toml");
|
||||
// Wrap the path with empty segments (Unix colon separators)
|
||||
let value = format!(":{}:", config_path.display());
|
||||
|
||||
let _jj = EnvGuard::set("JJ_CONFIG", &value);
|
||||
|
||||
assert_eq!(JjLib::user_config_paths(), vec![config_path]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_config_toml_always_included_when_xdg_set() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
|
||||
let _jj = EnvGuard::remove("JJ_CONFIG");
|
||||
let _xdg = EnvGuard::set("XDG_CONFIG_HOME", temp.path());
|
||||
|
||||
let paths = JjLib::user_config_paths();
|
||||
|
||||
assert!(paths.contains(&temp.path().join("jj").join("config.toml")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_config_included_when_it_exists() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let home_config = temp.path().join(".jjconfig.toml");
|
||||
std::fs::write(&home_config, "").unwrap();
|
||||
|
||||
let _jj = EnvGuard::remove("JJ_CONFIG");
|
||||
let _home = EnvGuard::set("HOME", temp.path());
|
||||
let _xdg = EnvGuard::set("XDG_CONFIG_HOME", temp.path().join("xdg"));
|
||||
|
||||
let paths = JjLib::user_config_paths();
|
||||
|
||||
assert!(paths.contains(&home_config));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_config_excluded_when_missing_and_platform_config_is_available() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
// ~/.jjconfig.toml intentionally not created
|
||||
|
||||
let _jj = EnvGuard::remove("JJ_CONFIG");
|
||||
let _home = EnvGuard::set("HOME", temp.path());
|
||||
let _xdg = EnvGuard::set("XDG_CONFIG_HOME", temp.path().join("xdg"));
|
||||
|
||||
let paths = JjLib::user_config_paths();
|
||||
|
||||
assert!(!paths.contains(&temp.path().join(".jjconfig.toml")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conf_d_included_when_directory_exists() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let conf_d = temp.path().join("jj").join("conf.d");
|
||||
std::fs::create_dir_all(&conf_d).unwrap();
|
||||
|
||||
let _jj = EnvGuard::remove("JJ_CONFIG");
|
||||
let _xdg = EnvGuard::set("XDG_CONFIG_HOME", temp.path());
|
||||
|
||||
let paths = JjLib::user_config_paths();
|
||||
|
||||
assert!(paths.contains(&conf_d));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conf_d_excluded_when_directory_is_missing() {
|
||||
let _lock = ENV_LOCK.lock().unwrap();
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
// conf.d intentionally not created
|
||||
|
||||
let _jj = EnvGuard::remove("JJ_CONFIG");
|
||||
let _xdg = EnvGuard::set("XDG_CONFIG_HOME", temp.path());
|
||||
|
||||
let paths = JjLib::user_config_paths();
|
||||
|
||||
assert!(!paths.contains(&temp.path().join("jj").join("conf.d")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user