Skip to content
Merged
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
18 changes: 17 additions & 1 deletion src/config/user/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,18 @@ impl UserConfig {
/// (via [`deep_merge_table`]) so sibling keys survive —
/// `--config-set list.full=true` leaves `list.branches` untouched.
///
/// Each fragment first runs through the same deprecation migration as a
/// config file ([`super::deprecation::migrate_content`] — structural and
/// silent rewrites, no `UpdateOnly`), so a deprecated key such as
/// `merge.no-ff` is canonicalized to `merge.ff` *before* the fragment is
/// parsed and merged, instead of falling through as an unknown field.
/// Migrating the fragment
/// (not the merged document) keeps layer precedence intact: the
/// canonicalized override wins over a lower layer's canonical key, rather
/// than colliding with it. The rewrite is silent — there is no file to
/// materialize, so the file-scoped "run `wt config update`" warning would
/// have nothing to act on.
///
/// The whole layer degrades as a unit: if any fragment fails to parse, or
/// the merged result fails to deserialize or validate, every override is
/// dropped and a [`LoadError::CliOverride`] is recorded, so a bad override
Expand All @@ -542,7 +554,11 @@ impl UserConfig {

let base = merged_table.clone();
for raw in overrides {
match raw.parse::<toml::Table>() {
// `migrate_content` returns the fragment unchanged when it is not
// valid TOML, so the parse below still catches a malformed fragment
// and drops the whole layer with an attributed warning.
let migrated = super::deprecation::migrate_content(raw);
match migrated.parse::<toml::Table>() {
Ok(fragment) => deep_merge_table(merged_table, fragment),
Err(err) => {
warnings.push(LoadError::CliOverride {
Expand Down
35 changes: 35 additions & 0 deletions src/config/user/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2556,6 +2556,41 @@ fn test_cli_override_validation_failure_drops_layer() {
assert_eq!(config.list.branches, Some(true)); // base preserved
}

#[test]
fn test_cli_override_migrates_deprecated_keys() {
// A deprecated key passed via `--config-set` runs through the same
// deprecation migration as a config file, so it is canonicalized and takes
// effect instead of falling through as an unknown field (which serde
// silently drops). The rewrite is silent — there is no file to materialize,
// so no deprecation warning is recorded.

// merge.no-ff = true → merge.ff = false
let (table, warnings) = apply_overrides(toml::Table::new(), &["merge.no-ff = true"]);
assert!(warnings.is_empty(), "merge.no-ff: {warnings:?}");
let config: UserConfig = toml::Value::Table(table).try_into().unwrap();
assert_eq!(config.merge.ff, Some(false));

// switch.no-cd = true → switch.cd = false
let (table, warnings) = apply_overrides(toml::Table::new(), &["switch.no-cd = true"]);
assert!(warnings.is_empty(), "switch.no-cd: {warnings:?}");
let config: UserConfig = toml::Value::Table(table).try_into().unwrap();
assert_eq!(config.switch.cd, Some(false));
}

#[test]
fn test_cli_override_deprecated_key_wins_over_lower_canonical() {
// Each fragment is migrated *before* it is merged, so the canonicalized
// override replaces a lower layer's canonical key rather than colliding
// with it. (Migrating the merged document instead would leave both
// `merge.ff` and `merge.no-ff` present, and the migration would keep the
// lower layer's `ff`, silently dropping the override.)
let base: toml::Table = "[merge]\nff = true\n".parse().unwrap();
let (table, warnings) = apply_overrides(base, &["merge.no-ff = true"]);
assert!(warnings.is_empty());
let config: UserConfig = toml::Value::Table(table).try_into().unwrap();
assert_eq!(config.merge.ff, Some(false)); // override (no-ff=true → ff=false) wins
}

#[test]
fn test_cli_override_empty_is_noop() {
let (table, warnings) = apply_overrides(toml::Table::new(), &[]);
Expand Down
25 changes: 25 additions & 0 deletions tests/integration_tests/list_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,31 @@ fn test_list_config_cli_override_after_subcommand(repo: TestRepo) {
});
}

/// A *deprecated* key passed via `--config-set` is run through the same
/// deprecation migration as a config file (`merge.no-ff` → `merge.ff`), so the
/// override layer is accepted rather than dropped as unknown. The migration is
/// silent — an inline override has no file for `wt config update` to rewrite —
/// so no deprecation or `--config-set` warning appears and `list` runs
/// normally. (That the migrated value takes effect is covered by the
/// `apply_cli_overrides` unit tests and
/// `test_switch_config_set_migrates_deprecated_no_cd`; `list` output does not
/// reflect a `merge` key.)
#[rstest]
fn test_list_config_cli_override_deprecated_key_silently_migrated(repo: TestRepo) {
fs::write(repo.test_config_path(), "[list]\nbranches = true\n").unwrap();
repo.run_git(&["branch", "feature"]);

let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.args(["--config-set", "merge.no-ff = true", "list"])
.current_dir(repo.root_path());

assert_cmd_snapshot!(cmd);
});
}

/// Bad values in non-section fields (projects, skip-*-prompt) must still be
/// attributed to the file, not to env vars.
#[rstest]
Expand Down
18 changes: 18 additions & 0 deletions tests/integration_tests/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6420,6 +6420,24 @@ fn test_switch_no_cd_flag_explicit(repo: TestRepo) {
);
}

/// A *deprecated* key passed via `--config-set` is migrated to its canonical
/// form before it is applied (`switch.no-cd` → `switch.cd`), so it takes effect
/// end-to-end instead of being dropped as an unknown field. The migrated
/// `switch.cd = false` suppresses the cd directive, so the output omits the
/// "Cannot change directory" line a cd-enabled switch prints. The migration is
/// silent: an inline override has no file for `wt config update` to rewrite, so
/// no deprecation warning appears.
#[rstest]
fn test_switch_config_set_migrates_deprecated_no_cd(repo: TestRepo) {
repo.run_git(&["branch", "dep-no-cd"]);

snapshot_switch(
"switch_config_set_migrates_deprecated_no_cd",
&repo,
&["dep-no-cd", "--config-set", "switch.no-cd = true"],
);
}

/// Test that worktrunk works correctly when `worktree.useRelativePaths` is enabled.
///
/// Git 2.48+ supports `worktree.useRelativePaths`, which stores relative paths in the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
source: tests/integration_tests/list_config.rs
info:
program: wt
args:
- "--config-set"
- merge.no-ff = true
- list
env:
APPDATA: "[TEST_CONFIG_HOME]"
CLICOLOR_FORCE: "1"
COLUMNS: "500"
GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z"
GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z"
GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]"
GIT_CONFIG_SYSTEM: /dev/null
GIT_TERMINAL_PROMPT: "0"
HOME: "[TEST_HOME]"
LANG: C
LC_ALL: C
LLVM_PROFILE_FILE: "[LLVM_PROFILE_FILE]"
MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]"
OPENCODE_CONFIG_DIR: "[TEST_OPENCODE_CONFIG]"
PATH: "[PATH]"
TERM: alacritty
USERPROFILE: "[TEST_HOME]"
WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]"
WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]"
WORKTRUNK_TEST_BASH_INSTALLED: "0"
WORKTRUNK_TEST_CLAUDE_INSTALLED: "0"
WORKTRUNK_TEST_CODEX_INSTALLED: "0"
WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1"
WORKTRUNK_TEST_EPOCH: "1735776000"
WORKTRUNK_TEST_FISH_INSTALLED: "0"
WORKTRUNK_TEST_GEMINI_INSTALLED: "0"
WORKTRUNK_TEST_NUSHELL_ENV: "0"
WORKTRUNK_TEST_OPENCODE_INSTALLED: "0"
WORKTRUNK_TEST_POWERSHELL_ENV: "0"
WORKTRUNK_TEST_POWERSHELL_INSTALLED: "0"
WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
WORKTRUNK_TEST_ZSH_INSTALLED: "0"
XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
---
success: true
exit_code: 0
----- stdout -----
Branch Status HEAD± main↕ Remote⇅ Path Commit Age Message
@ main ^| | . 05a4a45d 16h Initial commit
+ feature-a ↑ ↑1 ../repo.feature-a 1b87d473 16h Add feature-a file
+ feature-b ↑ ↑1 ../repo.feature-b f62940fc 16h Add feature-b file
+ feature-c ↑ ↑1 ../repo.feature-c 345c7c93 16h Add feature-c file
/ feature /_ 05a4a45d 16h Initial commit

○ Showing 4 worktrees, 1 branches, 3 ahead

----- stderr -----
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
source: tests/integration_tests/switch.rs
info:
program: wt
args:
- switch
- dep-no-cd
- "--config-set"
- switch.no-cd = true
env:
APPDATA: "[TEST_CONFIG_HOME]"
CLICOLOR_FORCE: "1"
COLUMNS: "500"
GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z"
GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z"
GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]"
GIT_CONFIG_SYSTEM: /dev/null
GIT_TERMINAL_PROMPT: "0"
HOME: "[TEST_HOME]"
LANG: C
LC_ALL: C
LLVM_PROFILE_FILE: "[LLVM_PROFILE_FILE]"
MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]"
OPENCODE_CONFIG_DIR: "[TEST_OPENCODE_CONFIG]"
PATH: "[PATH]"
TERM: alacritty
USERPROFILE: "[TEST_HOME]"
WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]"
WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]"
WORKTRUNK_TEST_BASH_INSTALLED: "0"
WORKTRUNK_TEST_CLAUDE_INSTALLED: "0"
WORKTRUNK_TEST_CODEX_INSTALLED: "0"
WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1"
WORKTRUNK_TEST_EPOCH: "1735776000"
WORKTRUNK_TEST_FISH_INSTALLED: "0"
WORKTRUNK_TEST_GEMINI_INSTALLED: "0"
WORKTRUNK_TEST_NUSHELL_ENV: "0"
WORKTRUNK_TEST_OPENCODE_INSTALLED: "0"
WORKTRUNK_TEST_POWERSHELL_ENV: "0"
WORKTRUNK_TEST_POWERSHELL_INSTALLED: "0"
WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
WORKTRUNK_TEST_ZSH_INSTALLED: "0"
XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
---
success: true
exit_code: 0
----- stdout -----

----- stderr -----
✓ Created worktree for dep-no-cd @ _REPO_.dep-no-cd
Loading