Skip to content

refactor(remove): extract branch-deletion policy into a use case#17

Merged
epodivilov merged 2 commits into
mainfrom
refactor/branch-deletion-policy
Jun 19, 2026
Merged

refactor(remove): extract branch-deletion policy into a use case#17
epodivilov merged 2 commits into
mainfrom
refactor/branch-deletion-policy

Conversation

@epodivilov

@epodivilov epodivilov commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • Extracts branch-deletion logic from src/cli/commands/remove.ts (three diverged sites) into a single deleteBranch use case returning a typed DeleteBranchOutcome union (deleted / not-merged / failed) with a nested RemoteDeletionOutcome for the optional remote-delete step.
  • remove.ts is now responsible only for rendering typed results — no retry-on-error logic in the CLI layer. The single-mode interactive prompt and the multi-mode deferred batch prompt remain in the CLI (UI concerns) and both invoke the same use case.
  • Use case is unit-tested against FakeGit; the contract-tested git error codes (BRANCH_NOT_MERGED, REMOTE_REF_NOT_FOUND, BRANCH_NOT_FOUND) verified by the fake-vs-real-git contract suite (#35) ensure fidelity to real git.
  • Behavior: preserved, with one deliberate exception. Single-worktree path still shows the "not merged" warning and the "Force deleting" step under --force/--yes (the use case attempts the plain delete first; only the explicit force step falls back); completion messages and warnings are unchanged. In the multi-worktree path, a force-delete that itself fails is now reported as "branch delete failed" with the git error, instead of being relabelled "kept — not fully merged" and offered a retry that just fails again — the only intentional behavior change. The transient "Deleting remote branch..." spinner hint is gone in the single, multi, and deferred paths (not only multi, as originally noted), since the local and remote deletes are now sequenced inside the use case; final messages are unchanged. Internal: dropped the unused mode: "normal" | "forced" field from DeleteBranchOutcome — no caller read it.

Test plan

  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm format (no changes)
  • pnpm test — 478 pass / 0 fail (12 unit tests for deleteBranch + 1 single-path CLI regression test)
  • Tests cover: clean delete, not-merged-no-force (skips remote attempt), not-merged-with-force (forced fallback), generic delete failure (BRANCH_NOT_FOUND), force-fallback failure, remote=deleted, remote=not-found, remote=failed (warning), and that failed/not-merged outcomes never invoke deleteRemoteBranch
  • All three former call sites in remove.ts now delegate to the use case

… typed outcomes

src/cli/commands/remove.ts had three diverged copies of the
delete-and-classify step (single-worktree path, multi-worktree first pass,
multi-worktree deferred force pass). Any fix had to be made three times,
and the destructive policy lived in the untestable CLI layer.

Extract a single deleteBranch use case that:
- attempts the delete and falls back to deleteBranchForce when force is true
- reports a typed DeleteBranchOutcome union (deleted/not-merged/failed) with
  a nested RemoteDeletionOutcome (deleted/not-found/failed/skipped)
- relies on the contract-tested git error codes (BRANCH_NOT_MERGED,
  REMOTE_REF_NOT_FOUND) verified by the fake-vs-real-git contract suite

remove.ts now renders the typed result at each former call site; retry-on-error
logic is gone from the CLI. Interactive prompting (the single-mode immediate
prompt and the multi-mode deferred batch prompt) stays in the CLI because
it is a UI concern; both paths invoke the same use case.

No user-visible behavior changes. New unit tests cover all five outcome
shapes plus negative cases (no remote attempt on not-merged/failed).
…op dead surface

Review follow-ups on the deleteBranch extraction:

- single-worktree path: pass force:false on the first delete so the
  "not merged" warning and the "Force deleting" step are still shown under
  --force/--yes (the prior code force-deleted inside the use case on the
  first attempt, silently swallowing the unmerged signal — notably under
  --yes). shouldForce is computed from force||yes as before.
- multi-worktree deferred pass: remove the now-unreachable `if (yes)` arm.
  unmergedBranches is only populated on a not-merged outcome, which requires
  force:false, i.e. yes===false — so that branch was dead.
- drop the unused `mode: "normal" | "forced"` field from DeleteBranchOutcome;
  no caller reads it and tests already prove the force fallback via
  status:"deleted" on a known-unmerged branch. Simplifies the use case body.
- tests: drop mode assertions, fix a stale comment, and add a single-path
  CLI regression test (the fake spinner now records its messages) so the
  --force-on-unmerged messaging can't silently regress again.

Behavior change (documented): in the multi path a force-delete that itself
fails is now reported as "branch delete failed" instead of being relabelled
"kept — not fully merged" and offered a pointless retry.
@epodivilov epodivilov merged commit aebb1a2 into main Jun 19, 2026
1 check passed
@epodivilov epodivilov deleted the refactor/branch-deletion-policy branch June 19, 2026 22:58
epodivilov added a commit that referenced this pull request Jun 20, 2026
…adder

PR #17 extracted the local branch-deletion policy (deleteBranch →
BRANCH_NOT_MERGED fallback to deleteBranchForce) into a dedicated use
case, but cleanup-worktrees.ts still carried a 4th copy of the same
ladder that was missed during that refactor.

Migrate that call site to deleteBranch({ branch, force: true,
deleteRemote: false }) and map the typed outcome onto the existing
cleanup reports:
- deleted → cleaned / branch-only (unchanged)
- failed  → error with the git error message

cleanup never touches the remote, so deleteRemote stays off; force=true
mirrors the previous unconditional fallback, so observable behavior of
wt cleanup is identical. Existing cleanup tests stay green. Added one
new test exercising the failed-mapping branch.
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