diff --git a/CHANGELOG.md b/CHANGELOG.md index fa2fc76e1..318375ba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 mode). For example, use `jj move --to @-- foo` to move the changes to file (or directory) `foo` in the working copy to the grandparent commit. +* The `$JJ_CONFIG` environment variable can now point to a directory. If it + does, all files in the directory will be read, in alphabetical order. + ### Fixed bugs * Errors are now printed to stderr (they used to be printed to stdout). diff --git a/src/main.rs b/src/main.rs index 43c0b76a1..8ca25e2e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,8 +21,12 @@ use jujutsu_lib::settings::UserSettings; fn config_path() -> Option { if let Ok(config_path) = env::var("JJ_CONFIG") { + // TODO: We should probably support colon-separated (std::env::split_paths) + // paths here Some(PathBuf::from(config_path)) } else { + // TODO: Should we drop the final `/config.toml` and read all files in the + // directory? dirs::config_dir().map(|config_dir| config_dir.join("jj").join("config.toml")) } } @@ -31,11 +35,29 @@ fn read_config() -> Result { let mut config_builder = config::Config::builder(); if let Some(config_path) = config_path() { - config_builder = config_builder.add_source( - config::File::from(config_path) - .required(false) - .format(config::FileFormat::Toml), - ); + let mut files = vec![]; + if config_path.is_dir() { + if let Ok(read_dir) = config_path.read_dir() { + // TODO: Walk the directory recursively? + for dir_entry in read_dir.flatten() { + let path = dir_entry.path(); + if path.is_file() { + files.push(path); + } + } + } + files.sort(); + } else { + files.push(config_path); + } + for file in files { + // TODO: Accept other formats and/or accept only certain file extensions? + config_builder = config_builder.add_source( + config::File::from(file) + .required(false) + .format(config::FileFormat::Toml), + ); + } }; // TODO: Make the config from environment a separate source instead? Seems diff --git a/tests/common/mod.rs b/tests/common/mod.rs index a5bf3843d..702fab138 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -14,7 +14,6 @@ use std::cell::RefCell; use std::collections::HashMap; -use std::io::Write; use std::path::{Path, PathBuf}; use tempfile::TempDir; @@ -23,8 +22,9 @@ pub struct TestEnvironment { _temp_dir: TempDir, env_root: PathBuf, home_dir: PathBuf, - config_path: PathBuf, + config_dir: PathBuf, env_vars: HashMap, + config_file_number: RefCell, command_number: RefCell, } @@ -34,15 +34,16 @@ impl Default for TestEnvironment { let env_root = tmp_dir.path().canonicalize().unwrap(); let home_dir = env_root.join("home"); std::fs::create_dir(&home_dir).unwrap(); - let config_path = env_root.join("config.toml"); - std::fs::write(&config_path, b"").unwrap(); + let config_dir = env_root.join("config"); + std::fs::create_dir(&config_dir).unwrap(); let env_vars = HashMap::new(); Self { _temp_dir: tmp_dir, env_root, home_dir, - config_path, + config_dir, env_vars, + config_file_number: RefCell::new(0), command_number: RefCell::new(0), } } @@ -62,7 +63,7 @@ impl TestEnvironment { let timestamp = chrono::DateTime::parse_from_rfc3339("2001-02-03T04:05:06+07:00").unwrap(); let mut command_number = self.command_number.borrow_mut(); *command_number += 1; - cmd.env("JJ_CONFIG", self.config_path.to_str().unwrap()); + cmd.env("JJ_CONFIG", self.config_dir.to_str().unwrap()); let timestamp = timestamp + chrono::Duration::seconds(*command_number); cmd.env("JJ_TIMESTAMP", timestamp.to_rfc3339()); cmd.env("JJ_USER", "Test User"); @@ -90,17 +91,18 @@ impl TestEnvironment { &self.home_dir } - pub fn config_path(&self) -> &Path { - &self.config_path - } - - pub fn write_config(&self, content: &[u8]) { - let mut config_file = std::fs::File::options() - .append(true) - .open(&self.config_path) - .unwrap(); - config_file.write_all(content).unwrap(); - config_file.flush().unwrap(); + pub fn add_config(&self, content: &[u8]) { + // Concatenating two valid TOML files does not (generally) result in a valid + // TOML file, so we use create a new file every time instead. + let mut config_file_number = self.config_file_number.borrow_mut(); + *config_file_number += 1; + let config_file_number = *config_file_number; + std::fs::write( + self.config_dir + .join(format!("config{config_file_number:04}.toml")), + content, + ) + .unwrap(); } pub fn add_env_var(&mut self, key: &str, val: &str) { @@ -115,7 +117,7 @@ impl TestEnvironment { // Simplified TOML escaping, hoping that there are no '"' or control characters // in it let escaped_diff_editor_path = diff_editor_path.to_str().unwrap().replace('\\', r"\\"); - self.write_config( + self.add_config( format!( r###" [ui] diff --git a/tests/test_global_opts.rs b/tests/test_global_opts.rs index 72d362a09..351acd1b3 100644 --- a/tests/test_global_opts.rs +++ b/tests/test_global_opts.rs @@ -68,7 +68,7 @@ fn test_color_config() { let mut test_env = TestEnvironment::default(); // Test that color is used if it's requested in the config file - test_env.write_config( + test_env.add_config( br#"[ui] color="always""#, ); @@ -95,13 +95,9 @@ fn test_invalid_config() { // Test that we get a reasonable error if the config is invalid (#55) let test_env = TestEnvironment::default(); - std::fs::write( - test_env.config_path(), - "[section]key = value-missing-quotes", - ) - .unwrap(); + test_env.add_config(b"[section]key = value-missing-quotes"); let stderr = test_env.jj_cmd_failure(test_env.env_root(), &["init", "repo"]); - insta::assert_snapshot!(stderr, @"Invalid config: expected newline, found an identifier at line 1 column 10 in config.toml + insta::assert_snapshot!(stderr.replace('\\', "/"), @"Invalid config: expected newline, found an identifier at line 1 column 10 in config/config0001.toml "); }