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 18, 2024
1 parent 75c817d commit 8d7a13b
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 26 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
89 changes: 75 additions & 14 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),
#[error(transparent)]
Expand Down Expand Up @@ -226,21 +228,29 @@ fn create_config_file(path: &Path) -> std::io::Result<std::fs::File> {
// 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() -> Result<Self, ConfigError> {
// 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());
#[cfg(target_os = "macos")]
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(),
})
Expand All @@ -250,7 +260,13 @@ impl ConfigEnv {
/// exists; or None if there are none; or an Error if there exist more
/// than one, or something else went wrong.
fn actual_configs(&self) -> Result<Option<PathBuf>, ConfigError> {
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");
Expand All @@ -261,7 +277,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 @@ -276,6 +292,11 @@ impl ConfigEnv {
paths[0].clone(),
paths[1].clone(),
)),
3 => Err(ConfigError::AmbiguousSource3(
paths[0].clone(),
paths[1].clone(),
paths[2].clone(),
)),
_ => unreachable!(),
}
}
Expand Down Expand Up @@ -312,7 +333,7 @@ impl ConfigEnv {
// otherwise, create one and then return it
} else {
// 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 @@ -796,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 @@ -810,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 @@ -819,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 @@ -865,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 @@ -879,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 @@ -902,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 @@ -917,6 +952,31 @@ 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().existing_config_path(),
Err(ConfigError::AmbiguousSource3(_, _, _))
);
assert_matches!(
cfg.clone().new_or_existing_config_path(),
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 @@ -944,7 +1004,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 8d7a13b

Please sign in to comment.