Skip to content

[bug] rimba clean --merged deletes freshly-created clean worktrees (no own commits) #335

Description

@lugassawan

Surface affected

  • Worktree operations / hooks (internal/operations/)
  • Git operations (internal/git/)
  • Command (cmd/*)

Specific surface: internal/operations/clean.go (FindMergedCandidates), internal/git/branch.go (MergedBranches), reached via rimba clean --merged and the installed post-merge hook (internal/hook/hook.go).

rimba version

rimba 1.9.6 (commit 96a2883, go1.26.4)

Environment

  • OS: macOS (Darwin 25.5.0, arm64)
  • Shell: zsh
  • git version: 2.54.0

Reproduction steps

  1. Install the post-merge hook: rimba hook install (it runs rimba clean --merged --force on the default branch after a merge).
  2. Create a fresh worktree from the default branch: rimba add foo → branch feature/foo at the same commit as main, no commits of its own.
  3. In a different worktree, do work and merge it into main (e.g. git merge feature/bar or a PR merge) — this fires the post-merge hook.
  4. Observe that the still-empty feature/foo worktree has been removed and its branch force-deleted.

Expected behavior

A freshly-created worktree whose branch has no commits of its own should NOT be considered "merged" and should be left untouched by rimba clean --merged.

Actual behavior

feature/foo is removed and its branch is force-deleted (git branch -D), even though nothing was ever merged from it. With the post-merge hook this happens silently (the hook passes --force, so there is no confirmation prompt).

Root cause

git branch --merged <ref> lists every branch reachable from <ref> — including a brand-new branch whose tip is the base commit (an ancestor, trivially "merged"). FindMergedCandidates (internal/operations/clean.go:70-74) adds any such branch to the removal set with no check for whether the branch has any commits of its own:

for _, e := range git.FilterEntries(entries, mainBranch) {
    if mergedSet[e.Branch] {   // fresh worktree branch matches here — no own-commits guard
        result.Candidates = append(result.Candidates, CleanCandidate{Path: e.Path, Branch: e.Branch})
        continue
    }
    // squash fallback already guards the empty-branch case (branch.go:147-149)
    ...
}

The squash-merge path (IsSquashMerged, internal/git/branch.go:147-149) already guards this exact case via mergeBase == tip; the plain --merged ancestor path does not.

Proposed fix

Add an own-commits guard to the --merged ancestor path in FindMergedCandidates: before treating a mergedSet hit as a candidate, skip it when the branch tip sits on the base branch's first-parent (mainline) history — i.e. the branch never contributed a commit. This protects fresh/empty worktrees while still removing genuinely merged branches:

  • fresh worktree (tip on main's first-parent chain) → skip (protected)
  • squash-merged (tip has unique commits) → remove
  • merge-commit-merged (tip is the merge's second parent, off mainline) → remove
  • fast-forward-merged (tip on mainline) → skip — an accepted, safe false-negative (kept rather than wrongly deleted)

Notes:

  • Put the guard in candidate finding, not removal, so --force (used by the post-merge hook) cannot bypass it.
  • Reuse existing helpers where possible (git.MergeBase, git.IsMergeBaseAncestor in internal/git/sync.go); add a small first-parent containment check in internal/git/.
  • Scope to --merged only. rimba clean --stale can wrongly remove a fresh worktree branched from an old base too, but that is a separate concern and out of scope for this issue.

Regression test

Add a case to internal/operations/clean_test.go (alongside TestFindMergedCandidatesNormalMerge): a worktree whose branch is in git branch --merged output but has no own commits (tip on base's first-parent chain) must NOT appear in result.Candidates. This test fails on main today and passes after the guard.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions