diff --git a/CHANGELOG.md b/CHANGELOG.md index f1a673d3dcf..f010699ddba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * You can check whether Watchman fsmonitor is enabled or installed with the new `jj debug watchman status` command. +* On MacOS, jj will now look for its config in `$XDG_CONFIG_HOME` in addition + to `~/Library/Application Support/jj/` + ### Fixed bugs * Revsets now support `\`-escapes in string literal. diff --git a/cli/src/config.rs b/cli/src/config.rs index 79ad4153c42..3fb1fff6efa 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -31,6 +31,8 @@ pub enum ConfigError { ConfigReadError(#[from] config::ConfigError), #[error("Both {0} and {1} exist. Please consolidate your configs in one of them.")] AmbiguousSource(PathBuf, PathBuf), + #[error("Configs found in {0}, {1}, and {2}. Please consolidate your configs in one of them.")] + AmbiguousSource3(PathBuf, PathBuf, PathBuf), #[error(transparent)] ConfigCreateError(#[from] std::io::Error), #[error(transparent)] @@ -234,13 +236,20 @@ enum HandleMissingConfig { // The struct exists so that we can mock certain global values in unit tests. #[derive(Clone, Default, Debug)] struct ConfigEnv { - config_dir: Option, + xdg_config_dir: Option, + platform_config_dir: Option, home_dir: Option, jj_config: Option, } impl ConfigEnv { fn new() -> Result { + // get XDG_CONFIG_HOME + let xdg_config_dir = Some( + etcetera::base_strategy::Xdg::new() + .map_err(ConfigError::HomeDirError)? + .config_dir(), + ); // get the current platform's config dir. On MacOS that's actually the data_dir #[cfg(not(target_os = "macos"))] let platform_config_dir = Some(etcetera::choose_base_strategy()?.config_dir()); @@ -248,7 +257,8 @@ impl ConfigEnv { let platform_config_dir = Some(etcetera::base_strategy::Apple::new()?.data_dir()); Ok(ConfigEnv { - config_dir: platform_config_dir, + xdg_config_dir, + platform_config_dir, home_dir: etcetera::home_dir().ok(), jj_config: env::var("JJ_CONFIG").ok(), }) @@ -271,7 +281,13 @@ impl ConfigEnv { return Ok(Some(path)); } // look for existing configs first - let platform_config = self.config_dir.as_ref().map(|config_dir| { + let platform_config = self.platform_config_dir.as_ref().map(|config_dir| { + let mut config_dir = config_dir.clone(); + config_dir.push("jj"); + config_dir.push("config.toml"); + config_dir + }); + let xdg_config = self.xdg_config_dir.as_ref().map(|config_dir| { let mut config_dir = config_dir.clone(); config_dir.push("jj"); config_dir.push("config.toml"); @@ -282,7 +298,7 @@ impl ConfigEnv { config_dir.push(".jjconfig.toml"); config_dir }); - let paths: Vec = vec![&platform_config, &home_config] + let paths: Vec = vec![&platform_config, &xdg_config, &home_config] .into_iter() .unique() .flatten() // pops all T out of Some @@ -297,6 +313,11 @@ impl ConfigEnv { paths[0].clone(), paths[1].clone(), )), + 3 => Err(ConfigError::AmbiguousSource3( + paths[0].clone(), + paths[1].clone(), + paths[2].clone(), + )), _ => unreachable!(), }; @@ -306,7 +327,7 @@ impl ConfigEnv { // otherwise, create one and then return it } else if matches!(handle_missing, HandleMissingConfig::Create) { // we use the platform_config by default - if let Some(mut path) = self.config_dir { + if let Some(mut path) = self.platform_config_dir { path.push("jj"); path.push("config.toml"); create_config_file(&path)?; @@ -794,7 +815,7 @@ mod tests { TestCase { files: vec![], cfg: ConfigEnv { - config_dir: Some("config".into()), + platform_config_dir: Some("config".into()), home_dir: Some("home".into()), ..Default::default() }, @@ -808,7 +829,7 @@ mod tests { TestCase { files: vec!["config/jj/config.toml"], cfg: ConfigEnv { - config_dir: Some("config".into()), + platform_config_dir: Some("config".into()), ..Default::default() }, want: Want::Existing("config/jj/config.toml"), @@ -817,12 +838,12 @@ mod tests { } #[test] - // if no config exists, make one in the config dir location - fn test_config_path_new_default_to_config_dir() -> anyhow::Result<()> { + // if no config exists, make one in the platform_config dir location + fn test_config_path_new_default_to_platform_config_dir() -> anyhow::Result<()> { TestCase { files: vec![], cfg: ConfigEnv { - config_dir: Some("config".into()), + platform_config_dir: Some("config".into()), home_dir: Some("home".into()), ..Default::default() }, @@ -830,6 +851,20 @@ mod tests { } .run() } + #[test] + fn test_config_path_platform_equals_xdg_is_ok() -> anyhow::Result<()> { + TestCase { + files: vec!["config/jj/config.toml"], + cfg: ConfigEnv { + platform_config_dir: Some("config".into()), + xdg_config_dir: Some("config".into()), + home_dir: Some("home".into()), + ..Default::default() + }, + want: Want::Existing("config/jj/config.toml"), + } + .run() + } #[test] fn test_config_path_jj_config_existing() -> anyhow::Result<()> { @@ -863,7 +898,7 @@ mod tests { files: vec!["config/jj/config.toml"], cfg: ConfigEnv { home_dir: Some("home".into()), - config_dir: Some("config".into()), + platform_config_dir: Some("config".into()), ..Default::default() }, want: Want::Existing("config/jj/config.toml"), @@ -877,7 +912,7 @@ mod tests { files: vec!["home/.jjconfig.toml"], cfg: ConfigEnv { home_dir: Some("home".into()), - config_dir: Some("config".into()), + platform_config_dir: Some("config".into()), ..Default::default() }, want: Want::Existing("home/.jjconfig.toml"), @@ -900,7 +935,7 @@ mod tests { let tmp = setup_config_fs(&vec!["home/.jjconfig.toml", "config/jj/config.toml"])?; let cfg = ConfigEnv { home_dir: Some(tmp.path().join("home")), - config_dir: Some(tmp.path().join("config")), + platform_config_dir: Some(tmp.path().join("config")), ..Default::default() }; use assert_matches::assert_matches; @@ -917,6 +952,33 @@ mod tests { Ok(()) } + #[test] + fn test_config_path_ambiguous3() -> anyhow::Result<()> { + let tmp = setup_config_fs(&vec![ + "home/.jjconfig.toml", + "config/jj/config.toml", + "bar/jj/config.toml", + ])?; + let cfg = ConfigEnv { + home_dir: Some(tmp.path().join("home")), + platform_config_dir: Some(tmp.path().join("config")), + xdg_config_dir: Some(tmp.path().join("bar")), + ..Default::default() + }; + use assert_matches::assert_matches; + assert_matches!( + cfg.clone() + .new_or_existing_config_path(HandleMissingConfig::Ignore), + Err(ConfigError::AmbiguousSource3(_, _, _)) + ); + assert_matches!( + cfg.clone() + .new_or_existing_config_path(HandleMissingConfig::Create), + Err(ConfigError::AmbiguousSource3(_, _, _)) + ); + Ok(()) + } + fn setup_config_fs(files: &Vec<&'static str>) -> anyhow::Result { let tmp = testutils::new_temp_dir(); for file in files { @@ -944,7 +1006,8 @@ mod tests { impl TestCase { fn config(&self, root: &Path) -> ConfigEnv { ConfigEnv { - config_dir: self.cfg.config_dir.as_ref().map(|p| root.join(p)), + xdg_config_dir: self.cfg.xdg_config_dir.as_ref().map(|p| root.join(p)), + platform_config_dir: self.cfg.platform_config_dir.as_ref().map(|p| root.join(p)), home_dir: self.cfg.home_dir.as_ref().map(|p| root.join(p)), jj_config: self .cfg diff --git a/docs/config.md b/docs/config.md index 7b9597b4991..d355443807c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -734,7 +734,7 @@ using `jj debug watchman status`. ### User config file -An easy way to find the user config file is: +An easy way to find the user config file (if one exists) is: ```bash jj config path --user @@ -742,17 +742,18 @@ jj config path --user The rest of this section covers the details of where this file can be located. -On all platforms, the user's global `jj` configuration file is located at either -`~/.jjconfig.toml` (where `~` represents `$HOME` on Unix-likes, or -`%USERPROFILE%` on Windows) or in a platform-specific directory. The -platform-specific location is recommended for better integration with platform -services. It is an error for both of these files to exist. - -| Platform | Value | Example | -| :------- | :------------------------------------------------- | :-------------------------------------------------------- | -| Linux | `$XDG_CONFIG_HOME/jj/config.toml` | `/home/alice/.config/jj/config.toml` | -| macOS | `$HOME/Library/Application Support/jj/config.toml` | `/Users/Alice/Library/Application Support/jj/config.toml` | -| Windows | `{FOLDERID_RoamingAppData}\jj\config.toml` | `C:\Users\Alice\AppData\Roaming\jj\config.toml` | +On all platforms, the user's global `jj` configuration file is located at +either `~/.jjconfig.toml` (where `~` represents `$HOME` on Unix-likes, or +`%USERPROFILE%` on Windows), in a platform-specific directory, or +`$XDG_CONFIG_HOME` (if different from the platform-specific directory, like on +MacOS). The platform-specific location is recommended for better integration +with platform services. It is an error for more than one of these files to exist. + +| Platform | Value | Example | +| :------- | ---------------------------------------- | --------------------------------------------- | +| Linux | `$XDG_CONFIG_HOME/jj/config.toml` | `/home/alice/.config/jj/config.toml` | +| macOS | `~/Library/Application Support/jj/config.toml` or `$XDG_CONFIG_HOME/jj/config.toml` | `/Users/alice/Library/Application Support/jj/config.toml` or `/Users/Alice/.config/jj/config.toml` | +| Windows | `{FOLDERID_RoamingAppData}\jj\config.toml` | `C:\Users\Alice\AppData\Roaming\jj\config.toml` | The location of the `jj` config file can also be overridden with the `JJ_CONFIG` environment variable. If it is not empty, it should contain the path