From 863d6903a43c3f9351d358fa605cd6d0d1212113 Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 6 Sep 2026 17:42:41 -0700 Subject: [PATCH 1/2] docs(merge): record that post-commit can't run when the merge removes its worktree `wt merge` announces post-commit and then spawns it into a worktree the cleanup step has already deleted, so the hook never runs and its runner logs `failed to open repository for pipeline`. post-commit is the only hook in the merge's background batch anchored on the feature worktree, and the announcer flushes every pipeline after removal. Documented 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 since removal succeeds against a live cwd. Corrects the two doc lines that claimed otherwise, one of which also said the post-* hooks run in sequence. Co-Authored-By: Claude Opus 5 (1M context) --- docs/src/content/docs/hook.md | 2 +- docs/src/content/docs/merge.md | 2 +- .../worktrunk/skills/worktrunk/reference/hook.md | 2 +- .../skills/worktrunk/reference/merge.md | 2 +- skills/worktrunk/reference/hook.md | 2 +- skills/worktrunk/reference/merge.md | 2 +- src/cli/mod.rs | 4 ++-- src/commands/merge.rs | 15 +++++++++++++++ tests/integration_tests/user_hooks.rs | 16 ++++++++++++---- ...__integration_tests__help__help_md_merge.snap | 2 +- ...integration_tests__help__help_merge_long.snap | 2 +- 11 files changed, 37 insertions(+), 14 deletions(-) diff --git a/docs/src/content/docs/hook.md b/docs/src/content/docs/hook.md index 332332c594..8b11676f73 100644 --- a/docs/src/content/docs/hook.md +++ b/docs/src/content/docs/hook.md @@ -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` doesn't run on a merge that removes that worktree. 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 diff --git a/docs/src/content/docs/merge.md b/docs/src/content/docs/merge.md index 870989c328..da4479a2f7 100644 --- a/docs/src/content/docs/merge.md +++ b/docs/src/content/docs/merge.md @@ -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, unless 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/`. 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/). diff --git a/plugins/worktrunk/skills/worktrunk/reference/hook.md b/plugins/worktrunk/skills/worktrunk/reference/hook.md index 1f19703d32..1e8b0cec48 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/hook.md +++ b/plugins/worktrunk/skills/worktrunk/reference/hook.md @@ -31,7 +31,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`](https://worktrunk.dev/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` doesn't run on a merge that removes that worktree. Use `pre-remove` for work that must finish there, or `--no-remove` to keep the worktree. See [`wt merge`](https://worktrunk.dev/merge/#pipeline) for the complete pipeline. # Security diff --git a/plugins/worktrunk/skills/worktrunk/reference/merge.md b/plugins/worktrunk/skills/worktrunk/reference/merge.md index 79e77492a0..0ff27a4564 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/merge.md +++ b/plugins/worktrunk/skills/worktrunk/reference/merge.md @@ -63,7 +63,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, unless 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/`. With `--no-squash`, individual commits are preserved. 3. **Rebase** — Rebases onto target, skipping when nothing needs replaying ([`wt step rebase`](https://worktrunk.dev/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`](https://worktrunk.dev/hook/). diff --git a/skills/worktrunk/reference/hook.md b/skills/worktrunk/reference/hook.md index 1f19703d32..1e8b0cec48 100644 --- a/skills/worktrunk/reference/hook.md +++ b/skills/worktrunk/reference/hook.md @@ -31,7 +31,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`](https://worktrunk.dev/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` doesn't run on a merge that removes that worktree. Use `pre-remove` for work that must finish there, or `--no-remove` to keep the worktree. See [`wt merge`](https://worktrunk.dev/merge/#pipeline) for the complete pipeline. # Security diff --git a/skills/worktrunk/reference/merge.md b/skills/worktrunk/reference/merge.md index 79e77492a0..0ff27a4564 100644 --- a/skills/worktrunk/reference/merge.md +++ b/skills/worktrunk/reference/merge.md @@ -63,7 +63,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, unless 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/`. With `--no-squash`, individual commits are preserved. 3. **Rebase** — Rebases onto target, skipping when nothing needs replaying ([`wt step rebase`](https://worktrunk.dev/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`](https://worktrunk.dev/hook/). diff --git a/src/cli/mod.rs b/src/cli/mod.rs index c28d687e3b..a2d678cb4b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -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, unless 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/`. 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/). @@ -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` doesn't run on a merge that removes that worktree. 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 diff --git a/src/commands/merge.rs b/src/commands/merge.rs index ffb776484d..86205c248a 100644 --- a/src/commands/merge.rs +++ b/src/commands/merge.rs @@ -322,6 +322,21 @@ 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 directory the removal has just deleted, and its runner + // logs `failed to open repository for pipeline` to + // `.git/wt/logs//user|project/post-commit/runner.log`, which + // nothing reads back — 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: diff --git a/tests/integration_tests/user_hooks.rs b/tests/integration_tests/user_hooks.rs index dc41395aa9..f52f016b0f 100644 --- a/tests/integration_tests/user_hooks.rs +++ b/tests/integration_tests/user_hooks.rs @@ -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 never +/// runs 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, @@ -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 never runs 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. diff --git a/tests/snapshots/integration__integration_tests__help__help_md_merge.snap b/tests/snapshots/integration__integration_tests__help__help_md_merge.snap index ecc11bbd1c..7f6bb97f8e 100644 --- a/tests/snapshots/integration__integration_tests__help__help_md_merge.snap +++ b/tests/snapshots/integration__integration_tests__help__help_md_merge.snap @@ -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, unless 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/`. 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/). diff --git a/tests/snapshots/integration__integration_tests__help__help_merge_long.snap b/tests/snapshots/integration__integration_tests__help__help_merge_long.snap index 13e45cb651..4526d921f9 100644 --- a/tests/snapshots/integration__integration_tests__help__help_merge_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_merge_long.snap @@ -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, unless 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/. 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. From cee49c14b5610ea6776515cfc851624ac4ad1f7a Mon Sep 17 00:00:00 2001 From: Maximilian Roos Date: Sun, 6 Sep 2026 18:22:29 -0700 Subject: [PATCH 2/2] docs(merge): record what the leftover placeholder makes post-commit do Review found the outcome isn't uniform: the fast removal path leaves an empty placeholder at the worktree's path, so a nested worktree's post-commit pipeline resolves discovery up to the primary worktree and runs there, in a directory about to vanish, rather than failing to open a repository. Verified both layouts. Co-Authored-By: Claude Opus 5 (1M context) --- docs/src/content/docs/hook.md | 2 +- docs/src/content/docs/merge.md | 2 +- plugins/worktrunk/skills/worktrunk/reference/hook.md | 2 +- .../worktrunk/skills/worktrunk/reference/merge.md | 2 +- skills/worktrunk/reference/hook.md | 2 +- skills/worktrunk/reference/merge.md | 2 +- src/cli/mod.rs | 4 ++-- src/commands/merge.rs | 12 +++++++++--- tests/integration_tests/user_hooks.rs | 12 ++++++------ ...tion__integration_tests__help__help_md_merge.snap | 2 +- ...on__integration_tests__help__help_merge_long.snap | 2 +- 11 files changed, 25 insertions(+), 19 deletions(-) diff --git a/docs/src/content/docs/hook.md b/docs/src/content/docs/hook.md index 8b11676f73..6f44685019 100644 --- a/docs/src/content/docs/hook.md +++ b/docs/src/content/docs/hook.md @@ -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`, 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` doesn't run on a merge that removes that worktree. 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. +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 diff --git a/docs/src/content/docs/merge.md b/docs/src/content/docs/merge.md index da4479a2f7..efb9b0fdda 100644 --- a/docs/src/content/docs/merge.md +++ b/docs/src/content/docs/merge.md @@ -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 the background, unless 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. +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/`. 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/). diff --git a/plugins/worktrunk/skills/worktrunk/reference/hook.md b/plugins/worktrunk/skills/worktrunk/reference/hook.md index 1e8b0cec48..738d2f7ed5 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/hook.md +++ b/plugins/worktrunk/skills/worktrunk/reference/hook.md @@ -31,7 +31,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`, 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` doesn't run on a merge that removes that worktree. Use `pre-remove` for work that must finish there, or `--no-remove` to keep the worktree. See [`wt merge`](https://worktrunk.dev/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`](https://worktrunk.dev/merge/#pipeline) for the complete pipeline. # Security diff --git a/plugins/worktrunk/skills/worktrunk/reference/merge.md b/plugins/worktrunk/skills/worktrunk/reference/merge.md index 0ff27a4564..ac18a7e178 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/merge.md +++ b/plugins/worktrunk/skills/worktrunk/reference/merge.md @@ -63,7 +63,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 the background, unless 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. +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/`. With `--no-squash`, individual commits are preserved. 3. **Rebase** — Rebases onto target, skipping when nothing needs replaying ([`wt step rebase`](https://worktrunk.dev/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`](https://worktrunk.dev/hook/). diff --git a/skills/worktrunk/reference/hook.md b/skills/worktrunk/reference/hook.md index 1e8b0cec48..738d2f7ed5 100644 --- a/skills/worktrunk/reference/hook.md +++ b/skills/worktrunk/reference/hook.md @@ -31,7 +31,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`, 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` doesn't run on a merge that removes that worktree. Use `pre-remove` for work that must finish there, or `--no-remove` to keep the worktree. See [`wt merge`](https://worktrunk.dev/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`](https://worktrunk.dev/merge/#pipeline) for the complete pipeline. # Security diff --git a/skills/worktrunk/reference/merge.md b/skills/worktrunk/reference/merge.md index 0ff27a4564..ac18a7e178 100644 --- a/skills/worktrunk/reference/merge.md +++ b/skills/worktrunk/reference/merge.md @@ -63,7 +63,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 the background, unless 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. +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/`. With `--no-squash`, individual commits are preserved. 3. **Rebase** — Rebases onto target, skipping when nothing needs replaying ([`wt step rebase`](https://worktrunk.dev/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`](https://worktrunk.dev/hook/). diff --git a/src/cli/mod.rs b/src/cli/mod.rs index a2d678cb4b..ed2eb91919 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -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 the background, unless 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. +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/`. 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/). @@ -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`, 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` doesn't run on a merge that removes that worktree. 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. +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 diff --git a/src/commands/merge.rs b/src/commands/merge.rs index 86205c248a..735d450a90 100644 --- a/src/commands/merge.rs +++ b/src/commands/merge.rs @@ -326,10 +326,16 @@ pub fn handle_merge(opts: MergeOptions<'_>) -> anyhow::Result<()> { // 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 directory the removal has just deleted, and its runner - // logs `failed to open repository for pipeline` to + // 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//user|project/post-commit/runner.log`, which - // nothing reads back — the `◎ Running post-commit` line is the only trace. + // 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 diff --git a/tests/integration_tests/user_hooks.rs b/tests/integration_tests/user_hooks.rs index f52f016b0f..b3d09f3538 100644 --- a/tests/integration_tests/user_hooks.rs +++ b/tests/integration_tests/user_hooks.rs @@ -1049,9 +1049,9 @@ sync = "echo merged" /// 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 never -/// runs on a merge that removes the worktree, which `handle_merge`'s announcer -/// comment covers. +/// 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, @@ -1090,9 +1090,9 @@ sync = "echo merged" /// 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 never runs on a -/// merge that removes the worktree, which `handle_merge`'s announcer comment -/// covers. +/// 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. diff --git a/tests/snapshots/integration__integration_tests__help__help_md_merge.snap b/tests/snapshots/integration__integration_tests__help__help_md_merge.snap index 7f6bb97f8e..701ddeee5b 100644 --- a/tests/snapshots/integration__integration_tests__help__help_md_merge.snap +++ b/tests/snapshots/integration__integration_tests__help__help_md_merge.snap @@ -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 the background, unless 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. +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/`. 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/). diff --git a/tests/snapshots/integration__integration_tests__help__help_merge_long.snap b/tests/snapshots/integration__integration_tests__help__help_merge_long.snap index 4526d921f9..bcf2c639b9 100644 --- a/tests/snapshots/integration__integration_tests__help__help_merge_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_merge_long.snap @@ -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 the background, unless 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. +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/. 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.