Skip to content

Commit

Permalink
config: add XDG_CONFIG_HOME config location for MacOS
Browse files Browse the repository at this point in the history
  • Loading branch information
ckoehler committed Apr 20, 2024
1 parent 8107d8c commit 6e79eb8
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 27 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
91 changes: 76 additions & 15 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down Expand Up @@ -233,13 +235,19 @@ 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<PathBuf>,
xdg_config_dir: Option<PathBuf>,
platform_config_dir: Option<PathBuf>,
home_dir: Option<PathBuf>,
jj_config: Option<String>,
}

impl ConfigEnv {
fn new() -> Self {
// get XDG_CONFIG_HOME
let xdg_config_dir = etcetera::base_strategy::Xdg::new()
.ok()
.map(|c| c.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 = etcetera::choose_base_strategy()
Expand All @@ -251,7 +259,8 @@ impl ConfigEnv {
.map(|c| c.data_dir());

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(),
}
Expand All @@ -274,8 +283,13 @@ impl ConfigEnv {
return Ok(Some(path));
}
// look for existing configs first
// cloning `self.config_dir` because we need it again below
let platform_config = self.config_dir.clone().map(|mut config_dir| {
// cloning `self.platform_config_dir` because we need it again below
let platform_config = self.platform_config_dir.clone().map(|mut config_dir| {
config_dir.push("jj");
config_dir.push("config.toml");
config_dir
});
let xdg_config = self.xdg_config_dir.map(|mut config_dir| {
config_dir.push("jj");
config_dir.push("config.toml");
config_dir
Expand All @@ -284,7 +298,7 @@ impl ConfigEnv {
config_dir.push(".jjconfig.toml");
config_dir
});
let paths: Vec<PathBuf> = vec![&platform_config, &home_config]
let paths: Vec<PathBuf> = vec![&platform_config, &xdg_config, &home_config]
.into_iter()
.unique()
.flatten() // pops all T out of Some<T>
Expand All @@ -299,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!(),
};

Expand All @@ -308,7 +327,7 @@ impl ConfigEnv {
// otherwise, create one and then return it
} else if 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)?;
Expand Down Expand Up @@ -798,7 +817,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()
},
Expand All @@ -812,7 +831,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"),
Expand All @@ -821,19 +840,33 @@ 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()
},
want: Want::ExistingOrNew("config/jj/config.toml"),
}
.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<()> {
Expand Down Expand Up @@ -867,7 +900,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"),
Expand All @@ -881,7 +914,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"),
Expand All @@ -904,7 +937,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;
Expand All @@ -921,6 +954,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<tempfile::TempDir> {
let tmp = testutils::new_temp_dir();
for file in files {
Expand Down Expand Up @@ -948,7 +1008,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
Expand Down
25 changes: 13 additions & 12 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -734,25 +734,26 @@ 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
```

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
Expand Down

0 comments on commit 6e79eb8

Please sign in to comment.