You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(hooks): approve post-switch against the removal's destination worktree (#2748)
When a worktree is removed, the `post-switch` hook runs in the
*destination* worktree — where the user lands — which
`prepare_worktree_removal` records as `RemoveResult.main_path` (the
primary worktree, except cwd when the primary worktree is itself the
removal target). But `wt remove`'s and `wt step prune`'s approval gates
collected the `post-switch` commands to prompt for from
`repo.home_path()` instead. They agree in the common case (`main_path ==
home_path()`), so nobody noticed — but in a bare repo, removing the
default-branch worktree from a *different* worktree, `main_path ==
current_path` while the gate read `home_path()`, so the gate approved
the primary's `post-switch` while the executor ran cwd's — an unapproved
project command could run. (`wt merge` and the picker's
`removal_hooks_approved`, added in #2746, already passed the destination
correctly.)
## What changed
`collect_remove_hook_commands` now takes `(removed_worktree_paths:
&[&Path], destination_paths: &[&Path])` instead of `(primary_repo:
&Repository, removed_worktree_paths)` — it collects `pre-remove` /
`post-remove` from each removed worktree and `post-switch` from each
destination (path-deduped, since the common case is the same primary
repeated). New `RemoveResult::destination_path() -> Option<&Path>`
returns `main_path` for `RemovedWorktree`, `None` for `BranchOnly` — a
sibling of the existing `removed_worktree_path()`. Callers updated:
- `wt remove` (single):
`approve_remove(result.removed_worktree_path().as_slice(),
result.destination_path().as_slice(), …)`. (Multi: the same projections
over the plan list.)
- `wt merge`: passes `&[destination_path]` — matches
`finish_after_merge`'s `RemoveResult { main_path: destination_path, …
}`.
- `wt step prune`: passes `&[home_path()]` — a prune candidate is never
the primary worktree (`gather_check_items` filters non-linked worktrees
and the default-branch worktree), so each candidate's removal
destination is always `home_path()`.
- The picker's `removal_hooks_approved` gains a `main_path` param and
passes `&[main_path]`.
The `commands::hooks` "Which `.config/wt.toml` a hook reads" table row
for `post-switch after a removal` now points at
`RemoveResult::destination_path` and notes the bare-repo cwd case.
## Testing
Behavior-neutral in every case the test suite exercises — `post-switch`
only fires when `changed_directory` (the removed worktree was cwd), and
in all of those sub-cases `main_path` resolves to `home_path()` anyway,
so the old and new gates collect the same commands. The change closes
the latent decoupling. No new test added: the only observable difference
is the bare-repo-remove-the-primary-from-elsewhere corner, and a
dedicated integration test for it would be disproportionate; the new
code paths (`destination_path()` on both variants, the destination loop
in `collect_remove_hook_commands`, the `main.rs` plumbing) are covered
by the existing `wt remove` / `wt merge` / `wt step prune` integration
tests and the `test_do_removal_*` picker unit tests. `cargo run -- hook
pre-merge --yes` is green (3693 tests, lints, fmt, doctests).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: src/commands/hooks.rs
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -25,7 +25,7 @@
25
25
//! |---|---|---|
26
26
//! | `pre-remove`, `post-remove` | the worktree being removed (`pre-remove` reads it on disk; `post-remove` reads a snapshot taken before removal, stashed on [`super::worktree::RemoveResult::RemovedWorktree::removed_project_config`]) | `output::handlers::execute_pre_remove_hooks_if_needed` and `output::handlers::spawn_hooks_after_remove`; the three approval call sites (`main.rs`'s `approve_remove` for `wt remove`, `merge::collect_merge_commands` for `wt merge`, `step::prune::approve_prune_hooks` for `wt step prune`) all delegate to [`super::project_config::collect_remove_hook_commands`] |
27
27
//! | `post-merge` | the merge destination (target branch's worktree if it has one, otherwise the primary) | `worktree::finish::finish_after_merge`; approval in `merge::collect_merge_commands` |
28
-
//! | `post-switch` after a removal | the post-removal working directory (where the user landed) — for `wt remove` / `wt step prune` the primary worktree, for `wt merge` the merge destination | `output::handlers::spawn_hooks_after_remove`; approval through `collect_remove_hook_commands` |
28
+
//! | `post-switch` after a removal | the post-removal working directory the user lands in ([`super::worktree::RemoveResult::destination_path`]) — usually the primary worktree, cwd when the primary worktree is itself removed (bare repo), the merge destination for `wt merge` | `output::handlers::spawn_hooks_after_remove`; approval through `collect_remove_hook_commands` |
29
29
//! | `pre-commit` / `post-commit` | the worktree being committed — the cwd worktree, or `<b>`'s worktree for `wt step commit --branch <b>` | `commands::commit`, `step::commit` (via `CommandEnv::for_branch`) |
30
30
//! | `wt switch`'s `pre-start` / `post-start` / `post-switch` | the new (`--create`) or destination (`wt switch <existing>`) worktree | `worktree::switch` — `hook_repo_for_worktree` (execution); `switch_hook_project_config` (approval / pre-flight, see Snapshot paths below) |
31
31
//! | `pre-switch`, `pre-merge`, `wt hook <type>`, aliases | the worktree `wt` was invoked in | the respective command entry points |
0 commit comments