feat(prune): `--apply` — safe, worktree-aware branch/worktree removal
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 5
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Summary
skiffs prune today classifies local branches read-only. This adds an opt-in executor,
skiffs prune --apply, that removes integrated/merged branches and the worktrees holding
them — behind a durability guard that never destroys unique work. Read-only stays the default
(a plan / apply split).
Design
Durability guard — block removal if any of:
- Working tree not clean — untracked / modified / staged files.
- Un-PR'd commit — a commit in
default..branchthat is not an ancestor of the default
branch, not patch-equivalent to one already in default (rebase-&-merge, detected via
git cherry), and not covered by a MERGED PR (squash-&-merge). A CLOSED-unmerged
PR does not make a branch safe. - Precious ignored content — gitignored-but-present files that a plain
git worktree removewould silently destroy. Opted in via--anti-gitignore <path>(file of verbatim
.gitignorekeys) or--no-ignore(blanket: every ignored+present file is precious).
Removal routing (per --git-common-dir):
- default → native
git worktree remove+git branch -D - worktrunk-managed repo (
.config/wt.tomlpresent or anyworktrunk.*key in
.git/config) → delegate towt remove(preserves itspre-removehooks) - worktrunk-managed but
wtnot onPATH→ skip + report, never native-fallback
(fail-closed)
Interaction:
- read-only by default (bare
skiffs prune= the report / "plan") --apply→ interactive[y/N/a/q](Enter=skip,a=all,q=quit);-y/--yes= all--applyrefuses when the scan spans more than one--git-common-dir(single-repo
safety boundary; the primary + all its worktrees still count as one repo)- the per-branch verdict is additive to the existing
PruneResultJSON —
safeToDelete/needsReviewunchanged
Tasks
Dependency-ordered. T1 unblocks the parallel block T2–T5. Each task ≤ ~5 files, test-first.
-
T1 — Guard verdict types + pure
Guard()- Acceptance:
report/guard.godefinesVerdict{Branch, Blocks []BlockReason, Route},
BlockReason(DirtyWorktree|UnPushedCommits|PreciousIgnored),Route
(NativeGit|WorktrunkDelegate|SkipWtUnavailable),BranchState,GuardConfig.
Guard(BranchState, GuardConfig) Verdict;Removable == len(Blocks)==0. Additive —
PruneResultslices untouched. - Verify:
go test ./report/— table tests: each block reason alone, combined, clean case. - Files:
report/guard.go,report/guard_test.go.
- Acceptance:
-
T2 —
gh: MERGED-only signal- Acceptance:
internal/ghaddsMergedPRBranches(ctx, dir, limit) (map[string]bool, error)
filteringstate == MERGEDonly. ExistingMergedClosedPRBranchesunchanged. - Verify:
go test ./internal/gh/— fixture asserts CLOSED excluded, MERGED included. - Files:
internal/gh/gh.go,internal/gh/gh_test.go.
- Acceptance:
-
T3 —
git: durability collectors- Acceptance:
WorktreeDirty(dir) (bool, FileStatus)(reuseGetState);
UnDurableCommits(ctx, dir, defaultRef, branch string, mergedPR bool) ([]string, error)
combiningmerge-base --is-ancestor,git cherry defaultRef branch(+= candidate,
-= upstream), and the MERGED-PR cover; ignores empty commits and in-range merges. - Verify:
go test ./internal/git/—t.TempDir()repos for merge-commit, rebase-&-merge,
squash (+MERGED), and CLOSED-unmerged; assert removable vs held per the matrix. - Files:
internal/git/prune.go,internal/git/prune_test.go.
- Acceptance:
-
T4 —
internal/ignore:.anti-gitignore- Acceptance: new package.
Parse(path) ([]string, error)(non-empty, non-#verbatim
keys);PreciousPresent(ctx, dir string, keys []string, blanket bool) ([]string, error)=
ignored+present files (git ls-files --others --ignored --exclude-standard) matched by a
listed key (verbatim, viadoublestar), or all whenblanket. Reports per-key match count. - Verify:
go test ./internal/ignore/— temp repo with a gitignored dir + noise; scoped key
matches only its dir, blanket matches all, a typo'd key → 0 (surfaced, not silent). - Files:
internal/ignore/ignore.go,internal/ignore/ignore_test.go.
- Acceptance: new package.
-
T5 —
internal/worktrunk: detect + route- Acceptance: new package.
IsManaged(ctx, dir) bool(.config/wt.tomlpresent or
git config --get-regexp '^worktrunk\.'non-empty).Available() bool
(exec.LookPath("wt")).Route(managed, available bool) report.Route:
managed+avail→WorktrunkDelegate; managed+!avail→SkipWtUnavailable; else→NativeGit. - Verify:
go test ./internal/worktrunk/— temp repos with/without each signal; assert
fail-closed route whenwtunavailable. - Files:
internal/worktrunk/worktrunk.go,internal/worktrunk/worktrunk_test.go.
- Acceptance: new package.
-
T6 — Executor interface (real + fake)
- Acceptance:
Executorinterface{ RemoveWorktree(dir) error; DeleteBranch(name) error; WtRemove(branch) error }. Real impl shellsgit worktree remove/git branch -D/
wt remove --foreground.fakeExecutorrecords calls without running them. - Verify:
go test ./internal/git/— integration (temp repo) for realRemoveWorktree+
DeleteBranch; fake used by later routing tests. - Files:
internal/git/executor.go,internal/git/executor_test.go.
- Acceptance:
-
T7 —
cmd/prune: per-branch Verdict in the read-only report- Acceptance:
runPrunecollects state (T2–T5), builds aBranchStateper candidate, runs
Guard, renders held reasons in the table + addsverdictto JSON additively. No
--applyyet → still fully read-only. - Verify:
go test ./cmd/— safety-regression: barepruneperforms zero mutations
(refs+worktrees unchanged); JSON still containssafeToDelete/needsReviewplusverdict. - Files:
cmd/prune.go,cmd/prune_test.go.
- Acceptance:
-
T8 —
cmd/prune:--apply(single-repo gate +[y/N/a/q]+ dispatch)- Acceptance: flags
--apply(bool),--no-ignore(bool),--anti-gitignore <path>,
-y/--yes. Single-repo gate: if--applyand discovered branches span >1
--git-common-dir, error before any prompt. Interactive loop matches[y/N/a/q]
(Enter=skip,y,a=all,q=quit);-y= all. OnlyRemovableverdicts offered; held
listed with reason. Each confirmed removal dispatched viaRoute→Executor. - Verify:
go test ./cmd/— (a) multi-common-dir--applyrefused; (b) interaction parity
with scripted stdin; (c)SkipWtUnavailablenever calls native executor (fake asserts);
(d) temp-repo end-to-end: one loose branch + one worktree removed. - Files:
cmd/prune.go,cmd/prune_test.go.
- Acceptance: flags
-
T9 — Docs + help text
- Acceptance: README documents the plan/apply model, the durability guard, and all flags;
cobra long-help forpruneupdated.gofmt -l .clean,go vet ./...clean. - Verify:
go run . prune --helpshows the new flags;go test ./...green. - Files:
README.md,cmd/prune.go.
- Acceptance: README documents the plan/apply model, the durability guard, and all flags;
Parallelization
T1 ─┬─ T2 ─┐
├─ T3 ─┼─ T6 ─┐
├─ T4 ─┤ ├─ T8 ─ T9
└─ T5 ─┘ T7 ─┘
(T7 needs T1–T5; T8 needs T6+T7)
Out of scope
- Touching
NeedsReview,CLOSED-unmerged branches, or the primary/current worktree. - Glob re-interpretation in
.anti-gitignore— keys match verbatim against.gitignore. - Making the bare
skiffs prunecommand destructive (read-only stays the default). - Non-GitHub remotes: branches whose squash-safety can't be proven via a MERGED PR are held,
never auto-removed.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the dependency-ordered task you select, beginning with report/guard.go and report/guard_test.go for the pure guard types and table tests. Read the referenced package files and run the task's listed Go test command first. Done means the task's acceptance criteria pass without changing the read-only default; the complete feature also requires cmd/prune.go integration, end-to-end tests, and README/help updates.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, github, go
- Domain
- cli, developer-experience, devtools
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100