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
2 changes: 1 addition & 1 deletion docs/src/content/docs/hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The most common creation hook is `post-start` — it runs background tasks (dev
| `pre-remove` | Cleanup before worktree deletion: saving test artifacts, backing up state. Runs in the worktree being removed |
| `post-remove` | Stopping dev servers, removing containers, notifying external systems. Template variables reference the removed worktree |

During `wt merge`, hooks run in this order: pre-commit → post-commit → pre-merge → pre-remove → post-remove + post-merge. See [`wt merge`](/merge/#pipeline) for the complete pipeline.
During `wt merge`, the blocking hooks run in this order: pre-commit → pre-merge → pre-remove. The `post-*` hooks all start together once the merge finishes, each in the worktree it is anchored on — post-merge, post-switch and post-remove in the destination, post-commit in the worktree the commit was made in. So `post-commit` can't be relied on for a merge that removes that worktree — the worktree is gone by the time the hook would start. Use `pre-remove` for work that must finish there, or `--no-remove` to keep the worktree. See [`wt merge`](/merge/#pipeline) for the complete pipeline.

# Security

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ $ wt merge --no-commit --no-rebase

`wt merge` runs these steps:

1. **Commit** — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in background. Skipped when squashing (the default) — changes are staged during the squash step instead. With `--no-squash`, this is the only commit step.
1. **Commit** — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in the background, and can't be relied on when step 7 removes the worktree they are anchored on. This step is skipped when squashing (the default) — changes are staged during the squash step instead. With `--no-squash`, this is the only commit step.
2. **Squash** — Combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. Working-tree changes swept into the squash are backed up first to `refs/wt-backup/<branch>`. With `--no-squash`, individual commits are preserved.
3. **Rebase** — Rebases onto target, skipping when nothing needs replaying ([`wt step rebase`](/step/#wt-step-rebase) gives the conditions). A conflict stops the merge with the rebase left open in the worktree, to resolve or abort. With `--no-rebase`, the graph produced by earlier commit/squash steps is preserved and the target must be able to fast-forward to its tip.
4. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](/hook/).
Expand Down
2 changes: 1 addition & 1 deletion plugins/worktrunk/skills/worktrunk/reference/hook.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion plugins/worktrunk/skills/worktrunk/reference/merge.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion skills/worktrunk/reference/hook.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion skills/worktrunk/reference/merge.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1446,7 +1446,7 @@ $ wt merge --no-commit --no-rebase

`wt merge` runs these steps:

1. **Commit** — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in background. Skipped when squashing (the default) — changes are staged during the squash step instead. With `--no-squash`, this is the only commit step.
1. **Commit** — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in the background, and can't be relied on when step 7 removes the worktree they are anchored on. This step is skipped when squashing (the default) — changes are staged during the squash step instead. With `--no-squash`, this is the only commit step.
2. **Squash** — Combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. Working-tree changes swept into the squash are backed up first to `refs/wt-backup/<branch>`. With `--no-squash`, individual commits are preserved.
3. **Rebase** — Rebases onto target, skipping when nothing needs replaying ([`wt step rebase`](/step/#wt-step-rebase) gives the conditions). A conflict stops the merge with the rebase left open in the worktree, to resolve or abort. With `--no-rebase`, the graph produced by earlier commit/squash steps is preserved and the target must be able to fast-forward to its tip.
4. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](/hook/).
Expand Down Expand Up @@ -1593,7 +1593,7 @@ The most common creation hook is `post-start` — it runs background tasks (dev
| `pre-remove` | Cleanup before worktree deletion: saving test artifacts, backing up state. Runs in the worktree being removed |
| `post-remove` | Stopping dev servers, removing containers, notifying external systems. Template variables reference the removed worktree |

During `wt merge`, hooks run in this order: pre-commit → post-commit → pre-merge → pre-remove → post-remove + post-merge. See [`wt merge`](/merge/#pipeline) for the complete pipeline.
During `wt merge`, the blocking hooks run in this order: pre-commit → pre-merge → pre-remove. The `post-*` hooks all start together once the merge finishes, each in the worktree it is anchored on — post-merge, post-switch and post-remove in the destination, post-commit in the worktree the commit was made in. So `post-commit` can't be relied on for a merge that removes that worktree — the worktree is gone by the time the hook would start. Use `pre-remove` for work that must finish there, or `--no-remove` to keep the worktree. See [`wt merge`](/merge/#pipeline) for the complete pipeline.

# Security

Expand Down
21 changes: 21 additions & 0 deletions src/commands/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,27 @@ pub fn handle_merge(opts: MergeOptions<'_>) -> anyhow::Result<()> {
// (from auto-commit or squash), post-remove + post-switch (from worktree
// removal), and post-merge share a single `◎ Running …` line flushed at
// the end.
//
// Every background hook runs in the worktree it is anchored on, and only
// post-commit is anchored on the feature worktree (the other three anchor
// on the destination). Flushing after `finish_after_merge` therefore spawns
// post-commit into a path the removal has already emptied — the fast path
// renames the worktree into `.git/wt/trash/` and leaves an empty
// placeholder there (torn down by the detached `sleep 1 && rmdir`). Where
// nothing above that placeholder is a git repository, the runner logs
// `failed to open repository for pipeline` to
// `.git/wt/logs/<branch>/user|project/post-commit/runner.log`, which
// nothing reads back; where the worktree is nested inside the repo,
// discovery walks up to the primary worktree instead and the steps run
// there, in a directory about to vanish. Either way the
// `◎ Running post-commit` line is the only trace.
// Accepted rather than fixed: the commit it would fire on is squashed and
// rebased before the merge lands, `pre-remove` already covers work that
// must finish in the feature worktree, and spawning early only narrows the
// window (removal succeeds against a live cwd). post-commit still runs where
// the worktree survives — `--no-remove`, merging on the target branch,
// merging from the primary worktree — and on `wt step commit` /
// `wt step squash`.
let mut announcer = HookAnnouncer::new(repo, false);

// The project commit-append is gated independently of hook approval:
Expand Down
16 changes: 12 additions & 4 deletions tests/integration_tests/user_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,10 @@ sync = "echo merged"
/// post-commit phase should join post-remove + post-switch + post-merge on
/// one combined announce line — the non-squash sibling of
/// [`test_merge_squash_combines_post_commit_post_remove_post_switch_post_merge`].
///
/// The announce line is all this pins. post-commit is announced but can't be
/// relied on to run on a merge that removes the worktree, which
/// `handle_merge`'s announcer comment covers.
#[rstest]
fn test_merge_auto_commit_combines_post_commit_post_remove_post_switch_post_merge(
mut repo: TestRepo,
Expand Down Expand Up @@ -1081,10 +1085,14 @@ sync = "echo merged"
);
}

/// `wt merge --squash` fires post-commit (from the squash phase), post-remove,
/// post-switch (from worktree removal), and post-merge. All four should share
/// one `Running …` announce line so the user sees a single status line for
/// the whole command, not four.
/// `wt merge --squash` announces post-commit (from the squash phase),
/// post-remove, post-switch (from worktree removal), and post-merge. All four
/// should share one `Running …` announce line so the user sees a single status
/// line for the whole command, not four.
///
/// That line is all this pins. post-commit is announced but can't be relied on
/// to run on a merge that removes the worktree, which `handle_merge`'s
/// announcer comment covers.
#[rstest]
fn test_merge_squash_combines_post_commit_post_remove_post_switch_post_merge(mut repo: TestRepo) {
// Squash needs >1 commit ahead of main to actually run.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ $ wt merge --no-commit --no-rebase

`wt merge` runs these steps:

1. **Commit** — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in background. Skipped when squashing (the default) — changes are staged during the squash step instead. With `--no-squash`, this is the only commit step.
1. **Commit** — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in the background, and can't be relied on when step 7 removes the worktree they are anchored on. This step is skipped when squashing (the default) — changes are staged during the squash step instead. With `--no-squash`, this is the only commit step.
2. **Squash** — Combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. Working-tree changes swept into the squash are backed up first to `refs/wt-backup/<branch>`. With `--no-squash`, individual commits are preserved.
3. **Rebase** — Rebases onto target, skipping when nothing needs replaying ([`wt step rebase`](/step/#wt-step-rebase) gives the conditions). A conflict stops the merge with the rebase left open in the worktree, to resolve or abort. With `--no-rebase`, the graph produced by earlier commit/squash steps is preserved and the target must be able to fast-forward to its tip.
4. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](/hook/).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ Preserve the exact clean commit graph and tip:

wt merge runs these steps:

1. Commit — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in background. Skipped when squashing (the default) — changes are staged during the squash step instead. With --no-squash, this is the only commit step.
1. Commit — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in the background, and can't be relied on when step 7 removes the worktree they are anchored on. This step is skipped when squashing (the default) — changes are staged during the squash step instead. With --no-squash, this is the only commit step.
2. Squash — Combines all commits since target into one (like GitHub's "Squash and merge"). Use --stage to control what gets staged: all (default), tracked, or none. Working-tree changes swept into the squash are backed up first to refs/wt-backup/<branch>. With --no-squash, individual commits are preserved.
3. Rebase — Rebases onto target, skipping when nothing needs replaying (wt step rebase gives the conditions). A conflict stops the merge with the rebase left open in the worktree, to resolve or abort. With --no-rebase, the graph produced by earlier commit/squash steps is preserved and the target must be able to fast-forward to its tip.
4. Pre-merge hooks — Hooks run after rebase, before merge. Failures abort. See wt hook.
Expand Down
Loading