diff --git a/src/commands/config/update.rs b/src/commands/config/update.rs index 2da0d7d8c..9b5696eb7 100644 --- a/src/commands/config/update.rs +++ b/src/commands/config/update.rs @@ -40,7 +40,7 @@ pub fn handle_config_update(yes: bool, print: bool) -> anyhow::Result<()> { if let Some(candidate) = check_user_config()? { candidates.push(candidate); } - if let Some(candidate) = check_project_config()? { + if let Some(candidate) = check_project_config(print)? { candidates.push(candidate); } @@ -177,7 +177,9 @@ fn check_user_config() -> anyhow::Result> { })) } -fn check_project_config() -> anyhow::Result> { +/// `print` is `--print`, which renders the migrated content to stdout and +/// writes nothing. +fn check_project_config(print: bool) -> anyhow::Result> { let repo = match Repository::current() { Ok(repo) => repo, Err(_) => return Ok(None), @@ -192,6 +194,14 @@ fn check_project_config() -> anyhow::Result> { } let is_linked = repo.current_worktree().is_linked().unwrap_or(true); + // The main-worktree requirement exists to keep the *rewrite* off a linked + // checkout, so `--print` — which writes nothing — is actionable from + // anywhere. Gating it too made `wt config update --print` emit an empty + // stdout plus a stderr hint from a linked worktree, which is where a + // worktree tool's users mostly stand: the pipe silently lost the project + // config, and the hint named `wt -C
config update`, a command that + // writes, in answer to a request that only reads. + let actionable = print || !is_linked; let original = std::fs::read_to_string(&config_path).context("Failed to read project config")?; @@ -199,7 +209,7 @@ fn check_project_config() -> anyhow::Result> { let result = worktrunk::config::check_and_migrate( &config_path, &original, - !is_linked, // only actionable from main worktree + actionable, ConfigFileKind::Project, Some(&repo), false, @@ -209,7 +219,7 @@ fn check_project_config() -> anyhow::Result> { return Ok(None); }; - if is_linked { + if !actionable { let cmd = suggest_command_in_dir(repo.repo_path()?, "config", &["update"], &[]); eprintln!("{}", hint_message("To update project config:")); eprintln!("{}", format_bash_with_gutter(&cmd)); diff --git a/tests/integration_tests/config_show.rs b/tests/integration_tests/config_show.rs index f3e49bffd..5cc54dc5c 100644 --- a/tests/integration_tests/config_show.rs +++ b/tests/integration_tests/config_show.rs @@ -5286,3 +5286,62 @@ fn test_project_config_path_env_var_half_anchored_errors(repo: TestRepo) { ); } } + +/// `wt config update --print` writes nothing, so the main-worktree +/// requirement that guards the *rewrite* must not gate it. Before the fix, +/// running it from a linked worktree emitted an empty stdout — silently +/// dropping the project config from the pipe — plus a stderr hint naming +/// `wt -C
config update`, a writing command, in answer to a read. +#[rstest] +fn test_config_update_print_emits_project_config_from_linked_worktree(repo: TestRepo) { + repo.write_project_config( + r#"pre-start = "ln -sf {{ main_worktree }}/node_modules" +"#, + ); + repo.commit("Add deprecated project config"); + let project_config_path = repo.root_path().join(".config").join("wt.toml"); + let before = fs::read_to_string(&project_config_path).unwrap(); + + let feature_path = repo.root_path().parent().unwrap().join("feature-print"); + repo.run_git(&[ + "worktree", + "add", + feature_path.to_str().unwrap(), + "-b", + "feature-print", + ]); + + let output = repo + .wt_command() + .args(["config", "update", "--print"]) + .current_dir(&feature_path) + .output() + .unwrap(); + + assert!( + output.status.success(), + "config update --print should succeed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("{{ repo }}") && !stdout.contains("{{ main_worktree }}"), + "--print from a linked worktree should emit the migrated project config, got: {stdout}" + ); + assert!( + output.stderr.is_empty(), + "--print must keep stderr empty for pipe-friendliness, got: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + fs::read_to_string(feature_path.join(".config").join("wt.toml")) + .unwrap() + .contains("{{ main_worktree }}"), + "--print must leave the linked worktree's config — the file it actually read — unmigrated" + ); + assert_eq!( + fs::read_to_string(&project_config_path).unwrap(), + before, + "--print must not modify the project config file" + ); +}