Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

config: also use XDG_CONFIG_HOME on MacOS #3466

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
ckoehler marked this conversation as resolved.
Show resolved Hide resolved
ckoehler marked this conversation as resolved.
Show resolved Hide resolved
ckoehler marked this conversation as resolved.
Show resolved Hide resolved
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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, these two error variants can be combined as AmbiguousSource(Vec<PathBuf>). The error message could be formatted as something like "Configs found in multiple locations: {}", .0.iter().map(|path| path.display()).join(", ") (untested)

#[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());
ckoehler marked this conversation as resolved.
Show resolved Hide resolved

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