Skip to content

Support merged-worktree cleanup for PRs targeting release branches or stack parents #14758

Description

@tris203

Before submitting

  • I searched existing issues and discussions and did not find an item covering this specific scope.
  • I included enough detail to reproduce or investigate the limitation.

Area

apps/server — automatic merged-worktree cleanup, shared by all clients.

Problem and scope

A feature PR can be merged into a release branch or its stack parent while the project's default branch does not yet contain its commits. Delete merged worktrees retains that feature checkout, even after an ordinary merge that preserves commit ancestry.

This is an intentional, documented policy limitation, not a regression or a claim of prior approval. The current rule requires ancestry in the primary remote's default branch. #14742's triage explicitly excludes PRs merged into release branches or stack parents from its focused squash-merge fix.

This issue requests separate maintainer triage of that non-default-target case, following #14651's request to separate target-selection behavior. It concerns only Delete merged worktrees, not the separate unchanged-worktree rule.

Steps to reproduce

T3 conditions:

  1. Enable Delete merged worktrees; disable inactivity, unchanged-worktree and deleted-thread cleanup so other rules cannot mask the result.
  2. Use an otherwise eligible T3-managed feature worktree: clean, idle, exclusively owned by its thread, without an active session/terminal or protected ignored files.
  3. Open its PR against release, or against a stack parent that has not landed in main.
  4. Merge that feature PR normally, preserving its commits. Keep the feature worktree at the merged PR's head; leave the target unmerged into main.
  5. Allow the cleanup sweep to evaluate the worktree.

The ancestry condition can be reproduced independently without T3 or a GitHub repository. This runs both target cases in new temporary repositories; neither uses squash:

repro_root=$(mktemp -d)
for target in release stack-parent; do
  git init -q -b main "$repro_root/$target"
  cd "$repro_root/$target" || exit 1
  git config user.name 'Cleanup repro'
  git config user.email 'cleanup-repro@example.invalid'
  printf 'base\n' > file
  git add file
  git commit -qm base
  git switch -qc "$target"
  printf 'parent\n' >> file
  git commit -qam parent
  git switch -qc feature
  printf 'feature\n' >> file
  git commit -qam feature
  git switch -q "$target"
  git merge -q --no-ff feature -m 'Merge feature normally'
  git merge-base --is-ancestor feature main
  printf '%s: feature ancestor of main = %s\n' "$target" "$?"
  git merge-base --is-ancestor feature "$target"
  printf '%s: feature ancestor of target = %s\n' "$target" "$?"
done

Observed output:

release: feature ancestor of main = 1
release: feature ancestor of target = 0
stack-parent: feature ancestor of main = 1
stack-parent: feature ancestor of target = 0

Expected behavior / requested triage decision

Consider allowing merged-worktree cleanup for the completed feature PR without waiting for its target to reach the default branch, while preserving the branch and thread history.

The key scope question is whether a freshly confirmed merged PR is enough for its otherwise eligible checkout to be removed when the target is a release branch or an unmerged stack parent, or whether cleanup should continue waiting for default-branch integration. Please establish that policy and the required evidence before implementation. If eligibility depends on reading the target branch, the behavior when that branch has since been deleted also needs to be explicit.

This request does not change unchanged-worktree cleanup, the existing default-branch ancestry fallback, ignored-file protection, or introduce an unrelated remote/default resolver. Existing ownership, clean-checkout, activity and final policy/HEAD checks remain. A stale PR snapshot, closed-unmerged PR or later unmerged local commit must not be treated as proof that the current checkout was merged.

Actual behavior

Cleanup fetches the primary remote's default branch, tests whether the worktree head is its ancestor, and returns on failure before querying the PR's merged state. The completed child checkout stays until its commits reach the default branch or another cleanup rule qualifies it.

The settings description matches this existing policy. The requested behavior would therefore be an intentional extension, independent of squash-merge detection.

Impact

Completed release/stack child worktrees continue occupying disk despite their PR being merged. Work is not blocked and branches/history are not lost.

Version or commit

Upstream main at 54084ae1e6c32809db040e4fa571c80fdf2d8ae4.

Environment and verification limits

Linux; the script above was run in isolated local Git repositories. Current cleanup source and maintainer comments were inspected. An integrated T3 cleanup run and live GitHub PR merges were not performed; the T3 result is source-traced, not a claimed application test.

Workaround

Wait for the target's commits to reach the default branch, or use the existing inactivity cleanup rule. Those have different timing from cleanup of the already-merged feature PR.

Activity

  1. juliusmarminge commented on Oct 2, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks for keeping this separate from the squash-merge fix, @tris203. I confirmed the behavior on main (54084ae). It's the documented policy, not a regression. Delete merged worktrees only goes ahead when the worktree head is an ancestor of the primary remote's default branch, and a non-ancestor returns before the merged pull request is ever read. The settings copy matches that. Your local repro checks out too: after a normal merge into release or a stack parent, git merge-base --is-ancestor feature main fails, while the same check against the target succeeds.

    A possible narrow scope (Julius still needs to approve the direction)

    This would extend only Delete merged worktrees. A completed PR merged into any non-default base, whether a release branch or a stack parent, could qualify without waiting for that target to reach the default branch, with no special-casing by branch name. On top of the existing idle, ownership, clean-checkout, ignored-file, and final policy and HEAD checks, all of these would need to hold:

    • A fresh PR read for the branch says merged, and its head ref is the worktree branch.
    • Its base ref isn't the project's default branch.
    • That base ref is fetched from the same primary remote, and the current HEAD is an ancestor of the fetched tip. GitHub's merged state alone isn't enough proof.

    The worktree would be kept when:

    • the target branch has been deleted, or fetching or resolving it fails
    • HEAD isn't an ancestor of the fetched target, which covers a later local commit, a squash or rebase onto the target, or a target that was reset
    • the PR is open, closed without merging, or missing, the lookup fails, or the head branch doesn't match
    • the base ref isn't on the primary remote, so there's no new target-repo resolver and no change to how the default branch is chosen

    Everything else stays as it is. The unchanged-worktree rule stays a default-branch ancestry check, even when both rules are on. The default-branch path doesn't change. Squash and rebase merges into a non-default target stay out of scope, because the exact-head check from #14742 is only for a default-branch base. Only base refs that are a single branch name safe for the existing fetch refspec would be used.

    If this goes ahead, the settings copy and storage guide would need to describe both proofs. Tests should cover:

    • a normal merge into a release branch or stack parent
    • a deleted target
    • a later local commit
    • a closed-unmerged PR
    • a failed lookup
    • unchanged-only cleanup
    • a squash onto a non-default target still being kept

    Please wait for a maintainer go-ahead on this issue before opening a PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementRequested improvement or new capability.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions