cleanup
清掉发布过程留下的东西——PR 已合并的本地与远程分支、这些分支对应的 worktree,以及移动文件时被忽略规则挡住、因而遗留在原处的残留(比如 git mv 看不见的 __pycache__)。每一次删除都会先向代码托管平台核实,任何未合并或来历不明的东西只报告、不删除。不适用于丢弃未提交的工作、重置分支,或删除你尚未看过的未跟踪文件。
- 分组
- 开发
- 版本
- 0.8.4
- 许可证
- MIT
- 参数
- [--base=<branch>] [--branches] [--remote] [--worktrees] [--residue] [--no-remote] [--dry-run]
安装
/plugin install dev@misoto22然后这样调用:
/dev:cleanup这个技能存在的理由是:rebase 和 squash 合并都会重写提交,所以合并之后 git 自己已经无法判断一个分支是否已经合并——`git branch --merged` 不会列出它。因此每一次删除前,它都去问代码托管平台那个 PR 的真实状态,只有平台报告 MERGED 才动手。它还处理一类 git 不会提醒你的情况:`git mv` 只移动被跟踪的文件,`__pycache__`、`node_modules` 这类被忽略的目录会留在原地并让父目录继续存在,而 `git status` 依然显示干净。
什么时候会触发
CI 实际用来评测这个技能的 prompt——所以它们不会和技能的真实行为脱节。
会触发
- Clean up the branches that have already merged.
- 清理一下仓库,收拾下没用的分支
- Remove the worktrees I am no longer using.
- 清掉已经合并过的分支
- There are a dozen merged branches still sitting on GitHub. Prune them.
- 远程还留着一堆合并过的分支,清一下
不会触发
- Discard my uncommitted changes.
- Reset this branch to origin and throw away my commits.
- Delete all untracked files in here.
- 把我没提交的改动都扔掉
Cleanup
Remove what shipping left behind. Nothing else.
Read shared/git.md first — in particular that a rebase or squash merge rewrites commits, so git branch --merged does not list a branch that landed. Every deletion here is verified against the forge, never against git alone.
Default to --dry-run reasoning even without the flag: list everything, then delete. With no scope flag, all four passes run. With any of --branches, --remote, --worktrees, --residue, only those. --no-remote drops the remote pass and keeps the rest — for a fork you cannot push to, or when you only want the local side tidied.
0. Inventory, before deleting anything
git fetch --prune
git branch -vv
git worktree list
git status --porcelain
git ls-remote --heads origin
git fetch --prune only deletes local origin/* tracking refs. It does not touch
a single branch on the remote, which is why the remote list is read separately —
a branch missing from git branch -vv may still be sitting on the forge.
Print one table. Nothing is removed until it is printed:
Cleanup <repo> → <base>
branch <name> <merged #12 | unmerged: N commits | no PR, contained in base> → <delete | keep: reason>
remote <name> <merged #12 | open #22 | no PR> → <delete | keep: reason>
worktree <path> <clean, branch merged | dirty> → <remove | keep: reason>
residue <path> <N ignored files, no tracked sibling> → <remove | keep: reason>
Stop here if --dry-run.
1. Branches
A local branch is deletable when its pull request reports MERGED:
gh pr list --head <branch> --state merged --json number,mergedAt
- Merged →
git branch -D <branch>.-drefuses after a rebase merge, for the reason inshared/git.md;-Dis correct here precisely because the SHAs were rewritten. - No pull request, but every commit is already on the base → delete. Prove it rather than assuming it:
A branch merged by hand, or one whose pull request was deleted, lands here. Nothing is lost, so the missing pull request is not a reason to keep it — but report which test cleared it, because "no PR" and "deleted" together look alarming in a report.git merge-base --is-ancestor <branch> <base> - Marked
[gone]bygit branch -vv, no merged pull request, and commits not on the base → the remote branch was deleted without merging. Keep it and say so. That is either abandoned work or someone else's mistake, and it is not recoverable once the local copy is gone. - No pull request, commits not on the base → unmerged local work. Keep, report.
- The base branch → never.
- A branch checked out in any worktree → not deletable while it is checked out. Two cases, and neither is "keep it forever":
- Checked out in a worktree this pass is about to remove → step 2 removes the worktree, then this rule is re-applied to the branch. Do not decide it before step 2 runs.
- Checked out in the primary repo, and merged, and its tree is clean → the checkout itself is the leftover. Say so and offer to
git checkout <base>and delete it. Never switch someone's checkout without asking; a branch name is often the only record of what they were in the middle of.
2. Worktrees
Per git worktree list, skipping the primary checkout:
- The worktree you are running in is never removed. Report it as kept, with that as the reason. Changing directory does not make it safe: a session's tooling, its scratch state, and its open file handles all live there, and the deletion cannot be undone from inside it. Whoever recycles that worktree does it from outside, after the session ends.
git -C <path> status --porcelain— anything at all, including untracked files, means keep. Say what is dirty.- Its branch must be deletable by the rule in step 1.
- Remove from the primary checkout, never from inside the worktree:
Ongit worktree remove <path>Directory not empty, retry with--forceonly if step 2 found the tree clean — the residue is ignored files, which is step 4's business. git worktree prune.- Re-apply step 1 to each branch just released. It was held back as "checked out", not as unmerged, and nothing else will come back for it.
A worktree outside this repository's own directory — another tool's session directory, say — is clean and merged like any other, but a live session may still be standing in it. List it and ask rather than removing it unprompted.
3. Remote branches
Runs by default. Skip only on --no-remote, or when a scope flag other than --remote was passed.
The forge is asked twice, because merged and open are not opposites — a branch can carry a merged pull request and a newer open one, and deleting it would close that open one. Per shared/git.md, GitHub closes any pull request whose head branch is deleted.
gh pr list --head <branch> --state merged --json number
gh pr list --head <branch> --state open --json number
- Merged, and no open pull request → delete.
- Any open pull request → keep, and name the pull request. This is the one case where a merged branch is still in use.
- No pull request at all → keep. A remote branch nobody opened a pull request for is someone else's work in progress, and it is not yours to guess about.
- The base branch, and any branch the remote protects → never.
Delete them in one push rather than one per branch — a stale-branch backlog is usually a dozen, and each push is a round trip:
git push origin --delete <branch> <branch> …
If the push is rejected for permissions, stop and report it: a fork, or a repository where you have read access only. Do not retry per branch — the rejection is the same every time.
Local and remote are decided independently. A branch can be deletable on the remote while its local copy is held back by a worktree, and reporting them as one line hides that.
4. Residue
Directories holding nothing but ignored files, left behind because git mv moves only what git tracks. git status stays clean, which is why these survive for months.
find . -type d -name __pycache__ -not -path './.git/*'
find . -type d -empty -not -path './.git/*'
For each candidate, the test is whether anything tracked still lives under it:
git ls-files --error-unmatch <dir> >/dev/null 2>&1
- No tracked files, and every file inside is ignored → residue. Remove.
- Any tracked file under it → not residue, whatever it looks like. Keep.
- Ignored files beside tracked ones — a live
__pycache__next to its.py,node_modulesnext topackage.json— are working state, not residue. Keep.
Never remove .git, .venv, node_modules, or anything named in .gitignore that sits beside tracked files. Deleting a build cache costs a rebuild; deleting .venv costs an afternoon. If a directory is large and expensive to recreate, list it and ask instead.
5. Verify
git branch -vv
git worktree list
git status --porcelain
git ls-remote --heads origin
The working tree must be exactly as clean as it was in step 0. If cleanup made it dirty, something tracked was removed — restore it with git restore and stop.
Reporting
Cleaned <repo>.
branches <deleted: a, b | none>
remote <deleted: a, b | none>
worktrees <removed: path | none>
residue <removed: path (N files) | none>
kept <name — reason; …>
kept is the important half. Every line in it is something that looked removable and was not, and each needs its reason stated — an unmerged branch, a remote branch with an open pull request, a dirty worktree, the worktree this session is running in, an ignored directory with tracked siblings.
Anything held back only because a human has to decide — a merged branch checked out in the primary repo, a worktree belonging to another tool's session — is reported as a question, not filed under kept and forgotten.