Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* `jj git import` in non-colocated repositories no longer imports commits from a
detached Git HEAD branch.

* `jj config {edit,set,unset} --user` now targets the first loaded user
configuration file (e.g. `~/.config/jj/config.toml` or the first file in
`conf.d/`) instead of prompting interactively when multiple files exist.
Use `--file <PATH>` to target a specific config file.

### Deprecations

### New features
Expand All @@ -33,6 +38,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
back to prompting the user if the heuristics are inconclusive. It can also run
in non-interactive mode, which aborts if prompting would be needed.

* `jj config {edit,set,unset}` now support a `--file <PATH>` option to
target a specific configuration file (such as files inside a `conf.d/`
directory or loaded via `--config-file`). This allows precise file targeting
and avoids interactive prompts when multiple config files exist.

### Fixed bugs

* `jj arrange` now scrolls the viewport to keep the selected commit visible
Expand Down
6 changes: 3 additions & 3 deletions cli/src/commands/config/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use jj_lib::config::ConfigLayer;
use tracing::instrument;

use super::ConfigLevelArgs;
use super::ConfigTargetArgs;
use crate::cli_util::CommandHelper;
use crate::command_error::CommandError;
use crate::command_error::print_error_sources;
Expand All @@ -28,7 +28,7 @@ use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
pub struct ConfigEditArgs {
#[command(flatten)]
pub level: ConfigLevelArgs,
pub target: ConfigTargetArgs,
}

#[instrument(skip_all)]
Expand All @@ -38,7 +38,7 @@ pub async fn cmd_config_edit(
args: &ConfigEditArgs,
) -> Result<(), CommandError> {
let editor = command.text_editor()?;
let file = args.level.edit_config_file(ui, command)?;
let file = args.target.edit_config_file(ui, command)?;
if !file.path().exists() {
file.save()?;
}
Expand Down
66 changes: 47 additions & 19 deletions cli/src/commands/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,46 +100,74 @@ impl ConfigLevelArgs {
panic!("No config_level provided")
}
}
}

#[derive(clap::Args, Clone, Debug)]
#[group(id = "config_target", multiple = false, required = true)]
pub(crate) struct ConfigTargetArgs {
/// Target the user-level config
#[arg(long)]
user: bool,

/// Target the repo-level config
#[arg(long)]
repo: bool,

/// Target the workspace-level config
#[arg(long)]
workspace: bool,

/// Target the config file specified by the given path
///
/// The path must point to a valid configuration file location recognized
/// by Jujutsu (such as a user/repo/workspace config, a file inside a
/// `conf.d/` directory, or any file loaded via system configs,
/// `$JJ_CONFIG`, or `--config-file`).
///
/// Unlike the global `--config-file` option (which loads an extra config
/// file when running commands), this option specifies which file to
/// inspect, edit, or modify on disk.
///
/// If the file does not exist, commands like `set` and `edit` will create
/// it and any missing parent directories.
#[arg(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)]
file: Option<PathBuf>,
}

impl ConfigTargetArgs {
fn edit_config_file(
&self,
ui: &Ui,
command: &CommandHelper,
) -> Result<ConfigFile, CommandError> {
let config_env = command.config_env();
let config = command.raw_config();
let pick_one = |mut files: Vec<ConfigFile>, not_found_error: &str| {
if files.len() > 1 {
let mut choices = vec![];
let mut formatter = ui.stderr_formatter();
for (i, file) in files.iter().enumerate() {
writeln!(formatter, "{}: {}", i + 1, file.path().display())?;
choices.push((i + 1).to_string());
}
drop(formatter);
let index =
ui.prompt_choice("Choose a config file (default 1)", &choices, Some(0))?;
return Ok(files[index].clone());
}
files.pop().ok_or_else(|| user_error(not_found_error))
let pick_first = |files: Vec<ConfigFile>, not_found_error: &str| {
files
.into_iter()
.next()
.ok_or_else(|| user_error(not_found_error))
};
if self.user {
pick_one(
if let Some(file) = &self.file {
let path = command.cwd().join(file);
config_env.resolve_file_to_edit(ui, config, &path)
} else if self.user {
pick_first(
config_env.user_config_files(config)?,
"No user config path found to edit",
)
} else if self.repo {
pick_one(
pick_first(
config_env.repo_config_files(ui, config)?,
"No repo config path found to edit",
)
} else if self.workspace {
pick_one(
pick_first(
config_env.workspace_config_files(ui, config)?,
"No workspace config path found to edit",
)
} else {
panic!("No config_level provided")
panic!("No config_target provided")
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions cli/src/commands/config/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use jj_lib::config::ConfigValue;
use jj_lib::repo::Repo as _;
use tracing::instrument;

use super::ConfigLevelArgs;
use super::ConfigTargetArgs;
use crate::cli_util::CommandHelper;
use crate::cli_util::WorkspaceCommandHelper;
use crate::command_error::CommandError;
Expand Down Expand Up @@ -53,7 +53,7 @@ pub struct ConfigSetArgs {
value: ConfigValue,

#[command(flatten)]
level: ConfigLevelArgs,
target: ConfigTargetArgs,
}

/// Denotes a type of author change
Expand All @@ -68,10 +68,10 @@ pub async fn cmd_config_set(
command: &CommandHelper,
args: &ConfigSetArgs,
) -> Result<(), CommandError> {
let mut file = args.level.edit_config_file(ui, command)?;
let mut file = args.target.edit_config_file(ui, command)?;

// If the user is trying to change the author config, we should warn them that
// it won't affect the working copy author
// If the user is trying to change the author config, we should warn them
// that it won't affect the working copy author
if args.name == ConfigNamePathBuf::from_iter(vec!["user", "name"]) {
check_wc_author(ui, command, &args.value, AuthorChange::Name).await?;
} else if args.name == ConfigNamePathBuf::from_iter(vec!["user", "email"]) {
Expand Down
6 changes: 3 additions & 3 deletions cli/src/commands/config/unset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use clap_complete::ArgValueCandidates;
use jj_lib::config::ConfigNamePathBuf;
use tracing::instrument;

use super::ConfigLevelArgs;
use super::ConfigTargetArgs;
use crate::cli_util::CommandHelper;
use crate::command_error::CommandError;
use crate::command_error::user_error;
Expand All @@ -32,7 +32,7 @@ pub struct ConfigUnsetArgs {
name: ConfigNamePathBuf,

#[command(flatten)]
level: ConfigLevelArgs,
target: ConfigTargetArgs,
}

#[instrument(skip_all)]
Expand All @@ -41,7 +41,7 @@ pub async fn cmd_config_unset(
command: &CommandHelper,
args: &ConfigUnsetArgs,
) -> Result<(), CommandError> {
let mut file = args.level.edit_config_file(ui, command)?;
let mut file = args.target.edit_config_file(ui, command)?;
let old_value = file
.delete_value(&args.name)
.map_err(|err| user_error_with_message(format!("Failed to unset {}", args.name), err))?;
Expand Down
89 changes: 89 additions & 0 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ use tracing::instrument;
use crate::command_error::CommandError;
use crate::command_error::config_error;
use crate::command_error::config_error_with_message;
use crate::command_error::user_error;
use crate::ui::Ui;

// TODO(#879): Consider generating entire schema dynamically vs. static file.
Expand Down Expand Up @@ -664,6 +665,78 @@ impl ConfigEnv {
Ok(())
}

/// Returns the configuration file at the specified `path` for modification.
///
/// Validates that the path is a recognized Jujutsu configuration location
/// (such as a loaded layer, a user config path or file in `conf.d/`,
/// or a repo/workspace config path). If the file is already loaded in
/// `config`, its existing [`ConfigFile`] representation is reused;
/// otherwise, a new [`ConfigFile`] is initialized.
pub fn resolve_file_to_edit(
&self,
ui: &Ui,
config: &RawConfig,
path: &Path,
) -> Result<ConfigFile, CommandError> {
let canonical_path = dunce::canonicalize(path).ok();
let matches_path = |p: &Path| p == path || Some(p) == canonical_path.as_deref();

// 1. Check if it matches an already loaded layer (user, system, repo,
// workspace, --config-file, JJ_CONFIG, etc.)
for layer in config.as_ref().layers() {
if let Some(layer_path) = layer.path.as_ref()
&& matches_path(layer_path)
&& let Ok(file) = ConfigFile::from_layer(layer.clone())
{
return Ok(file);
}
}

// 2. Check repo config path (even if not yet created on disk)
if let Ok(Some(repo_path)) = self.repo_config_path(ui)
&& matches_path(&repo_path)
{
if let Some(parent) = path.parent() {
create_dir_all(parent).ok();
}
return Ok(ConfigFile::load_or_empty(ConfigSource::Repo, path)?);
}

// 3. Check workspace config path (even if not yet created on disk)
if let Ok(Some(workspace_path)) = self.workspace_config_path(ui)
&& matches_path(&workspace_path)
{
if let Some(parent) = path.parent() {
create_dir_all(parent).ok();
}
return Ok(ConfigFile::load_or_empty(
ConfigSource::Workspace,
path,
)?);
}

// 4. Check user config paths (e.g. ~/.config/jj/config.toml,
// ~/.jjconfig.toml, or files in conf.d)
for user_path in self.user_config_paths() {
if matches_path(user_path) || is_file_in_config_dir(path, user_path) {
if let Some(parent) = path.parent() {
create_dir_all(parent).ok();
}
return Ok(ConfigFile::load_or_empty(ConfigSource::User, path)?);
}
}

Err(user_error(format!(
"Configuration file '{}' is not a valid jj configuration file location",
path.display()
))
.hinted(
"Valid config locations include user configs (`~/.config/jj/config.toml` or \
`conf.d/*.toml`), repo/workspace configs, or files loaded with the global flag \
`--config-file <PATH>`.",
))
}

/// Resolves conditional scopes within the current environment. Returns new
/// resolved config.
pub fn resolve_config(&self, config: &RawConfig) -> Result<StackedConfig, ConfigGetError> {
Expand Down Expand Up @@ -707,6 +780,22 @@ fn config_files_for(
Ok(files)
}

fn is_file_in_config_dir(file_path: &Path, dir_path: &Path) -> bool {
if file_path.extension() != Some("toml".as_ref()) {
return false;
}
if dir_path.is_file() {
return false;
}
let Some(parent) = file_path.parent() else {
return false;
};
if parent == dir_path {
return true;
}
dunce::canonicalize(parent).ok().as_deref() == Some(dir_path)
}

/// Initializes stacked config with the given `default_layers` and infallible
/// sources.
///
Expand Down
28 changes: 25 additions & 3 deletions cli/tests/cli-reference@.md.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
source: cli/tests/test_generate_md_cli_help.rs
description: "AUTO-GENERATED FILE, DO NOT EDIT. This cli reference is generated by a test as an `insta` snapshot. MkDocs includes this snapshot from docs/cli-reference.md."
---

<!-- BEGIN MARKDOWN-->

# Command-Line Help for `jj`
Expand Down Expand Up @@ -773,7 +774,7 @@ Start an editor on a jj config file.

Creates the file if it doesn't already exist regardless of what the editor does.

**Usage:** `jj config edit <--user|--repo|--workspace>`
**Usage:** `jj config edit <--user|--repo|--workspace|--file <PATH>>`

**Command Alias:** `e`

Expand All @@ -782,6 +783,13 @@ Creates the file if it doesn't already exist regardless of what the editor does.
* `--user` — Target the user-level config
* `--repo` — Target the repo-level config
* `--workspace` — Target the workspace-level config
* `--file <PATH>` — Target the config file specified by the given path

The path must point to a valid configuration file location recognized by Jujutsu (such as a user/repo/workspace config, a file inside a `conf.d/` directory, or any file loaded via system configs, `$JJ_CONFIG`, or `--config-file`).

Unlike the global `--config-file` option (which loads an extra config file when running commands), this option specifies which file to inspect, edit, or modify on disk.

If the file does not exist, commands like `set` and `edit` will create it and any missing parent directories.



Expand Down Expand Up @@ -883,7 +891,7 @@ See `jj config edit` if you'd like to immediately edit a file.

Update a config file to set the given option to a given value

**Usage:** `jj config set <--user|--repo|--workspace> <NAME> <VALUE>`
**Usage:** `jj config set <--user|--repo|--workspace|--file <PATH>> <NAME> <VALUE>`

**Command Alias:** `s`

Expand All @@ -901,14 +909,21 @@ Update a config file to set the given option to a given value
* `--user` — Target the user-level config
* `--repo` — Target the repo-level config
* `--workspace` — Target the workspace-level config
* `--file <PATH>` — Target the config file specified by the given path

The path must point to a valid configuration file location recognized by Jujutsu (such as a user/repo/workspace config, a file inside a `conf.d/` directory, or any file loaded via system configs, `$JJ_CONFIG`, or `--config-file`).

Unlike the global `--config-file` option (which loads an extra config file when running commands), this option specifies which file to inspect, edit, or modify on disk.

If the file does not exist, commands like `set` and `edit` will create it and any missing parent directories.



## `jj config unset`

Update a config file to unset the given option

**Usage:** `jj config unset <--user|--repo|--workspace> <NAME>`
**Usage:** `jj config unset <--user|--repo|--workspace|--file <PATH>> <NAME>`

**Command Alias:** `u`

Expand All @@ -921,6 +936,13 @@ Update a config file to unset the given option
* `--user` — Target the user-level config
* `--repo` — Target the repo-level config
* `--workspace` — Target the workspace-level config
* `--file <PATH>` — Target the config file specified by the given path

The path must point to a valid configuration file location recognized by Jujutsu (such as a user/repo/workspace config, a file inside a `conf.d/` directory, or any file loaded via system configs, `$JJ_CONFIG`, or `--config-file`).

Unlike the global `--config-file` option (which loads an extra config file when running commands), this option specifies which file to inspect, edit, or modify on disk.

If the file does not exist, commands like `set` and `edit` will create it and any missing parent directories.



Expand Down
Loading
Loading