Skip to content

feat(worktree): cleanup reaps squash/rebase-merged worktrees (P4-GIT-04)#138

Merged
Reederey87 merged 1 commit into
mainfrom
feat/p4-git-04-worktree-gc
Jul 6, 2026
Merged

feat(worktree): cleanup reaps squash/rebase-merged worktrees (P4-GIT-04)#138
Reederey87 merged 1 commit into
mainfrom
feat/p4-git-04-worktree-gc

Conversation

@Reederey87

@Reederey87 Reederey87 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Ships audit finding P4-GIT-04 (worktree GC that reaps squash/rebase-merged worktrees); ledger row moved to Recently shipped.

What

worktree cleanup --merged tested merged-ness with git branch --merged — ancestry only — so a branch merged via GitHub "Squash and merge" (or a rebase-merge) was never an ancestor of base and its worktree was skipped forever.

How

  • New git.Runner.IsSquashMerged(ctx, dir, branch, baseRef) — a current-tree merge probe: git merge-tree --write-tree <base> <branch> (git ≥ 2.38) reports merged only when the simulated merge's tree is identical to the current base tree — the branch would contribute nothing, which is exactly the effect of a squash/rebase/cherry-pick merge.
    • The first draft's git cherry + synthetic-commit + patch-id --stable chain was replaced in dual review: both reviewers independently reproduced that historical patch-id equivalence matches a change that was merged then reverted on base — reaping genuinely-unmerged work. The merge-tree probe compares against the current tree and is immune to the revert class.
    • Conservative: a conflicting simulated merge, an older git without --write-tree, or any error → not merged. Never reap on doubt.
    • Documented accepted limitation (inherent to any content-equivalence test): a branch whose net change also landed via an unrelated, coincidentally identical commit reads as merged — pinned by a tripwire test; mitigated by the reap breadcrumb below.
  • Cleanup wiring: ancestry stays the first check; only its misses consult IsSquashMerged. Reaps are labeled merged vs merged (squash) and print the deleted branch's tip SHA (branch <name> was at <sha>) so git branch <name> <sha> restores it until gc. refreshWorktreeBase best-effort fetches the recorded remote/branch base under the repo lock, deduped per (project, base ref) per run (review finding); warn + continue offline. Reaped worktrees also get git branch -D (warn-only). Dirty worktrees still skip unless --force.
  • Forge-API (gh pr list --state merged) cross-checks are a documented non-goal — cleanup works offline.

Tests

Real-git: TestIsSquashMergedDetectsSquashMerge, ...DetectsRebaseMerge, ...FalseForUnmerged, ...ConservativeOnContentDivergence, ...FalseAfterRevertOnBase (the dual-review repro), ...MatchesCoincidentallyIdenticalDiff (documented-limitation tripwire); CLI e2e TestWorktreeCleanupReapsSquashMergedWorktree.

Specs

spec/08 (cleanup semantics + conservative rule + limitation + breadcrumb), spec/16, spec/18 work log, docs/audits/README.md ledger reconciliation.

Validation

gofmt clean · golangci-lint run 0 issues · go test -race ./... ok · spec-drift --base origin/main pass · dual review (coordinator line-by-line + gpt-5.5 implementation + opus reviewer + Codex review; the shared Major redesigned pre-merge).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • worktree cleanup --merged now recognizes squash/rebase-style merges via an offline content-equivalence check, with ancestry as the first path.
    • Cleanup output now includes a recovery breadcrumb (deleted branch tip SHA).
    • Base refs are best-effort refreshed with deduplicated remote fetches per project/base.
  • Bug Fixes
    • “Merged” is handled more conservatively when detection is uncertain or errors occur.
    • After cleanup, the underlying Git branch is deleted when possible; failures produce warnings.
  • Documentation
    • Updated specs, CLI daemon docs, and shipped audit/work-log entries for the new behavior.
  • Tests
    • Added unit and integration/e2e coverage for squash-merge detection and conservative “false” cases.

@Reederey87 Reederey87 enabled auto-merge (squash) July 6, 2026 11:08
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6fecaf80-64bf-4bad-9113-df56b4b7845b

📥 Commits

Reviewing files that changed from the base of the PR and between 62e3f81 and 4568ea3.

📒 Files selected for processing (9)
  • docs/audits/README.md
  • internal/cli/worktree.go
  • internal/cli/worktree_test.go
  • internal/git/git.go
  • internal/git/git_test.go
  • spec/08_GIT_MATERIALIZATION_AND_WORKTREES.md
  • spec/13_CLI_DAEMON_API.md
  • spec/16_TEST_PLAN.md
  • spec/18_WORK_LOG.md
✅ Files skipped from review due to trivial changes (3)
  • spec/18_WORK_LOG.md
  • spec/16_TEST_PLAN.md
  • docs/audits/README.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/git/git.go
  • internal/git/git_test.go
  • internal/cli/worktree_test.go
  • spec/13_CLI_DAEMON_API.md
  • internal/cli/worktree.go

📝 Walkthrough

Walkthrough

This PR adds offline squash/rebase-merge detection, wires it into worktree cleanup --merged with base refresh and branch deletion, adds tests, and updates related specs and audit records.

Changes

Worktree cleanup reaps squash/rebase-merged branches

Layer / File(s) Summary
IsSquashMerged detection logic
internal/git/git.go, internal/git/git_test.go
Adds Runner.IsSquashMerged and integration coverage for squash-merge, rebase/cherry-pick-style, unmerged, divergence, revert, and indistinguishable-history cases.
CLI cleanup integration: base refresh, merge check, branch deletion
internal/cli/worktree.go, internal/cli/worktree_test.go
Adds per-run base refresh deduplication, refresh helper logic, squash-merge fallback handling in cleanup, branch deletion after worktree removal, branch-tip SHA reporting, and an end-to-end cleanup test with JSON/state helpers.
Spec, docs, and audit log updates
spec/08_GIT_MATERIALIZATION_AND_WORKTREES.md, spec/13_CLI_DAEMON_API.md, spec/16_TEST_PLAN.md, spec/18_WORK_LOG.md, docs/audits/README.md
Documents mergedness rules, conservative fallback behavior, recovery breadcrumbs, test coverage, work log details, and the shipped status for P4-GIT-04.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

  • Reederey87/DevStrap#37: Both PRs touch the worktree cleanup implementation in internal/cli/worktree.go, so they overlap on the same command path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: worktree cleanup now reaps squash/rebase-merged worktrees and references the shipped audit finding.
Description check ✅ Passed The description covers the summary, tests, and safety considerations, and includes the main implementation details required by the template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/p4-git-04-worktree-gc

Comment @coderabbitai help to get the list of available commands.

@Reederey87 Reederey87 force-pushed the feat/p4-git-04-worktree-gc branch from 00c8dc5 to 8ea724e Compare July 6, 2026 11:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/cli/worktree.go (2)

505-505: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Warnings mix output channels.

The existing skip warnings ("stat failed", "dirty-state check failed") go through logging.Logger(cmd.Context()).Warn(...), but the two new warnings (refresh failure, branch-delete failure) are written directly to stdout via fmt.Fprintf. This mixes structured, filterable logs with ad-hoc user-facing text in the same command, making both harder to consume consistently (e.g. scripts scraping stdout for "Removed worktree" lines will also see these warnings interleaved).

♻️ Suggested consistency fix
 if err := refreshWorktreeBase(cmd.Context(), opts, r, project, repoPath, wt.BaseRef); err != nil {
-    _, _ = fmt.Fprintf(stdout, "warning: could not refresh %s for worktree %s: %v; using local ref\n", wt.BaseRef, wt.ID, err)
+    logging.Logger(cmd.Context()).Warn("worktree cleanup: base refresh failed, using local ref", "worktree", wt.ID, "base_ref", wt.BaseRef, "error", err.Error())
 }
...
 if _, err := r.Run(cmd.Context(), repoPath, "branch", "-D", wt.Branch); err != nil {
-    _, _ = fmt.Fprintf(stdout, "warning: failed to delete branch %s for removed worktree %s: %v\n", wt.Branch, wt.ID, err)
+    logging.Logger(cmd.Context()).Warn("worktree cleanup: branch delete failed", "worktree", wt.ID, "branch", wt.Branch, "error", err.Error())
 }

Also applies to: 511-511, 521-521, 547-547

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/worktree.go` at line 505, The worktree cleanup warnings are
inconsistent because `cleanupRemovedWorktree` logs some failures with
`logging.Logger(cmd.Context()).Warn(...)` but writes the newer refresh and
branch-delete failures directly to stdout. Update those warning paths to use the
same structured logger as the existing “stat failed” and “dirty-state check
failed” messages so all warnings are emitted consistently through
`logging.Logger(cmd.Context())` and not mixed with ad-hoc stdout text.

518-557: 🩺 Stability & Availability | 🔵 Trivial

Repo lock only guards the fetch, not the branch delete/removal.

refreshWorktreeBase acquires acquireRepoLock for the fetch, but the subsequent branch --merged, IsSquashMerged, WorktreeRemove, and branch -D calls on the same repoPath run unlocked. If another process (daemon materialization, concurrent worktree new) mutates the same repo between the fetch and the branch deletion, there's a narrow window for surprising interactions. This mirrors the existing pattern in newWorktreeRemoveCommand (also unlocked), so it isn't a regression introduced here, but worth confirming that repo-level git mutations outside of fetch are expected to be safe unlocked in this codebase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/worktree.go` around lines 518 - 557, The worktree removal flow
in the command handling logic is only locking the fetch path via
refreshWorktreeBase, while branch inspection and deletion in the same repoPath
(`branch --merged`, `IsSquashMerged`, `WorktreeRemove`, and `branch -D`) still
run unlocked. Update the removal sequence in the worktree command to either hold
the same repo lock across the full git mutation window or explicitly confirm and
document that these repo-level operations are safe to run without locking,
keeping the behavior consistent with `newWorktreeRemoveCommand` and the rest of
the git repo access pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@spec/13_CLI_DAEMON_API.md`:
- Line 356: Remove the retired merged-ness fallback chain from the CLI spec
paragraph. Update the `P4-GIT-04` description to match the shipped
`git.Runner.IsSquashMerged` behavior: it should describe the `merge-tree`-based
check, and state that older Git versions or any error/ambiguity fail closed as
not merged. Keep the rest of the `worktree cleanup --merged` text intact, but
delete references to `git cherry`, the synthetic-commit recipe, and `patch-id
--stable`.

---

Nitpick comments:
In `@internal/cli/worktree.go`:
- Line 505: The worktree cleanup warnings are inconsistent because
`cleanupRemovedWorktree` logs some failures with
`logging.Logger(cmd.Context()).Warn(...)` but writes the newer refresh and
branch-delete failures directly to stdout. Update those warning paths to use the
same structured logger as the existing “stat failed” and “dirty-state check
failed” messages so all warnings are emitted consistently through
`logging.Logger(cmd.Context())` and not mixed with ad-hoc stdout text.
- Around line 518-557: The worktree removal flow in the command handling logic
is only locking the fetch path via refreshWorktreeBase, while branch inspection
and deletion in the same repoPath (`branch --merged`, `IsSquashMerged`,
`WorktreeRemove`, and `branch -D`) still run unlocked. Update the removal
sequence in the worktree command to either hold the same repo lock across the
full git mutation window or explicitly confirm and document that these
repo-level operations are safe to run without locking, keeping the behavior
consistent with `newWorktreeRemoveCommand` and the rest of the git repo access
pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d0086ef-5472-4e30-be6c-2cafe443ce1a

📥 Commits

Reviewing files that changed from the base of the PR and between e29389b and 00c8dc5.

📒 Files selected for processing (9)
  • docs/audits/README.md
  • internal/cli/worktree.go
  • internal/cli/worktree_test.go
  • internal/git/git.go
  • internal/git/git_test.go
  • spec/08_GIT_MATERIALIZATION_AND_WORKTREES.md
  • spec/13_CLI_DAEMON_API.md
  • spec/16_TEST_PLAN.md
  • spec/18_WORK_LOG.md

Comment thread spec/13_CLI_DAEMON_API.md Outdated
@Reederey87 Reederey87 force-pushed the feat/p4-git-04-worktree-gc branch from 8ea724e to 62e3f81 Compare July 6, 2026 11:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@spec/08_GIT_MATERIALIZATION_AND_WORKTREES.md`:
- Around line 300-302: The cleanup flow in the worktree mergedness logic is too
permissive when refreshing the recorded base ref fails, because it continues
using the stale local ref and may delete worktrees that are no longer merged
upstream. Update the `devstrap worktree cleanup --merged` behavior described in
this spec so the fetch-failure path fails closed (treat as not merged and skip
reaping) or otherwise align the text with the real fallback behavior. Make sure
the wording around the mergedness check, the base-ref refresh step, and the
“never reaps on doubt” guarantee is consistent with the actual implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b93ccd90-4da1-450c-aa54-abf070db5e28

📥 Commits

Reviewing files that changed from the base of the PR and between 8ea724e and 62e3f81.

📒 Files selected for processing (9)
  • docs/audits/README.md
  • internal/cli/worktree.go
  • internal/cli/worktree_test.go
  • internal/git/git.go
  • internal/git/git_test.go
  • spec/08_GIT_MATERIALIZATION_AND_WORKTREES.md
  • spec/13_CLI_DAEMON_API.md
  • spec/16_TEST_PLAN.md
  • spec/18_WORK_LOG.md
✅ Files skipped from review due to trivial changes (4)
  • spec/18_WORK_LOG.md
  • spec/13_CLI_DAEMON_API.md
  • docs/audits/README.md
  • spec/16_TEST_PLAN.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/cli/worktree_test.go
  • internal/git/git_test.go
  • internal/cli/worktree.go
  • internal/git/git.go

Comment thread spec/08_GIT_MATERIALIZATION_AND_WORKTREES.md Outdated
worktree cleanup --merged only tested ancestry, so squash- and
rebase-merged branches were skipped forever. New git.Runner.IsSquashMerged
uses a current-tree merge probe (git merge-tree --write-tree, git >= 2.38):
merged iff the simulated merge's tree equals the current base tree. The
first-draft git cherry/patch-id chain was replaced in dual review —
historical patch-id equivalence matches merged-then-reverted changes and
would reap unmerged work. Conservative on conflict/error; the inherent
coincidental-identical-diff limitation is tripwire-pinned and every reap
prints the deleted branch's tip SHA as the recovery breadcrumb. Base
fetched under the repo lock (deduped per project/ref); reaped branches
get git branch -D. Ledger: P4-GIT-04 -> Recently shipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Reederey87 Reederey87 force-pushed the feat/p4-git-04-worktree-gc branch from 62e3f81 to 4568ea3 Compare July 6, 2026 11:53
@Reederey87 Reederey87 merged commit b326c56 into main Jul 6, 2026
7 checks passed
@Reederey87 Reederey87 deleted the feat/p4-git-04-worktree-gc branch July 6, 2026 11:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant