关于 git-cleanup
Git Cleanup 是用于检查并清理本地 Git 分支和 worktree 的技能。它会先归组相关分支、核查合并与工作区状态,并在执行删除前要求用户经过两道确认。
主要功能
- 01审查前归组相关分支
- 02分类已合并、活动及待核查工作
- 03检查合并历史与跟踪状态
- 04检测 worktree 未提交更改
- 05删除前设置两道用户确认
适用场景
- 01检查积累的本地分支
- 02查找远程删除后遗留的跟踪分支
- 03在安全确认下删除已合并分支和 worktree
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trailofbits/ skills --skill git-cleanup安装后在 Claude Code 中输入/git-cleanup确认已加载。
附带文件agents/openai.yamlassets/trail-of-bits-mark.svg
详细介绍
Git 清理
通过将累积的 git worktree 和本地分支分类为以下类别,安全地进行清理:可安全删除(已合并)、可能相关(主题相似)以及正在进行的工作(保留)。
适用场景
- 用户积累了许多本地分支和 worktree 时
- 分支已合并,但尚未在本地清理时
- 远程分支已删除,但本地跟踪分支仍然存在时
不适用场景
- 不要用于远程分支管理(这仅用于本地清理)
- 不要用于 gc 或 prune 等仓库维护任务
- 不适用于无头或非交互式自动化(两个关卡都需要用户确认)
核心原则:安全第一
未经用户明确确认,绝不删除任何内容。 此技能采用设有关卡的工作流,在执行任何破坏性操作前,用户必须逐步批准。
关键实现说明
经 Squash 合并的分支需要强制删除
重要: 对于经 squash 合并的分支,git branch -d 总会失败,因为 git 无法检测到相关工作已被纳入。这是预期行为,并非错误。
将分支识别为经 squash 合并后:
- 从一开始就计划使用
git branch -D(强制删除) - 不要先尝试
git branch -d,再重新询问是否使用-D—— 这会浪费用户的确认机会 - 在确认步骤中,对经 squash 合并的分支显示
git branch -D
分类前先对相关分支分组
必须执行: 对单个分支进行分类前,先按名称前缀对它们分组:
# Extract common prefixes from branch names
# e.g., feature/auth-*, feature/api-*, fix/login-*
共享前缀的分支(例如 feature/api、feature/api-v2、feature/api-refactor)几乎肯定是相关的迭代。将它们作为一个组进行分析:
- 找出提交日期最早和最晚的分支
- 检查较新的分支是否包含较旧分支的提交
- 检查哪些 PR 合并了各分支的工作
- 判断较旧的分支是否已被取代
将相关分支放在一起,并给出明确建议,不要将它们分散到不同类别中。
彻底调查 PR 历史
不要依赖简单的关键词匹配。对于 [gone] 分支:
# 1. Get the branch's commits that aren't in default branch
git log --oneline "$default_branch".."$branch"
# 2. Search default branch for PRs that incorporated this work
# Search by: branch name, commit message keywords, PR numbers
git log --oneline "$default_branch" | grep -iE "(branch-name|keyword|#[0-9]+)"
# 3. For related branch groups, trace which PRs merged which work
git log --oneline "$default_branch" | grep -iE "(#[0-9]+)" | head -20
工作流
阶段 1:全面分析
在进行任何分类前,先收集所有信息:
# Get default branch name
default_branch=$(git symbolic-ref refs/remotes/origin/HEAD \
2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
# Protected branches - never analyze or delete
protected='^(main|master|develop|release/.*)$'
# List all local branches with tracking info
git branch -vv
# List all worktrees
git worktree list
# Fetch and prune to sync remote state
git fetch --prune
# Get merged branches (into default branch)
git branch --merged "$default_branch"
# Get recent PR merge history (squash-merge detection)
git log --oneline "$default_branch" | grep -iE "#[0-9]+" | head -30
# For EACH non-protected branch, get unique commits and sync status
for branch in $(git branch --format='%(refname:short)' \
| grep -vE "$protected"); do
echo "=== $branch ==="
echo "Commits not in $default_branch:"
git log --oneline "$default_branch".."$branch" 2>/dev/null \
| head -5
echo "Commits not pushed to remote:"
git log --oneline "origin/$branch".."$branch" 2>/dev/null \
| head -5 || echo "(no remote tracking)"
done
关于分支名称的说明: Git 分支名称可能包含会导致 shell 展开的字符。在命令中始终将 "$branch" 加上引号。
阶段 2:对相关分支分组
务必在单独分类前执行此步骤。
根据共享前缀识别分支组:
# List branches and extract prefixes
git branch --format='%(refname:short)' | sed 's/-[^-]*$//' | sort | uniq -c | sort -rn
对于包含 2 个或更多分支的每个组:
- 比较提交历史 —— 哪些分支包含其他分支的提交?
- 查找合并证据 —— 哪些 PR 纳入了该组的工作?
- 确定“最终”分支 —— 通常是最新或最完整的分支
- 标记已被取代的分支 —— 工作已进入 main 或较新分支的较早迭代
判定为已被取代必须有证据,不能仅凭前缀相同:
- 某个 PR 将工作合并到了 main,或者
- 较新的分支包含较旧分支的所有提交
- 仅凭名称前缀并不足以判定 —— 名称相似的分支可能包含互不相关的工作
feature/api-* 分支的分析示例:
### Related Branch Group: feature/api-*
| Branch | Commits | PR Merged | Status |
|--------|---------|-----------|--------|
| feature/api | 12 | #29 (initial API) | Superseded - work in main |
| feature/api-v2 | 8 | #45 (API improvements) | Superseded - work in main |
| feature/api-refactor | 5 | #67 (refactor) | Superseded - work in main |
| feature/api-final | 4 | None found | Superseded by above PRs |
**Recommendation:** All 4 branches can be deleted - work incorporated via PRs #29, #45, #67
阶段 3:对其余分支分类
对于不属于相关组的分支,逐个分类:
Is branch merged into default branch?
├─ YES → SAFE_TO_DELETE (use -d)
└─ NO → Is tracking a remote?
├─ YES → Remote deleted? ([gone])
│ ├─ YES → Was work squash-merged? (check main for PR)
│ │ ├─ YES → SQUASH_MERGED (use -D)
│ │ └─ NO → REMOTE_GONE (needs review)
│ └─ NO → Local ahead of remote? (check: git log origin/<branch>..<branch>)
│ ├─ YES (has output) → UNPUSHED_WORK (keep)
│ └─ NO (empty output) → SYNCED_WITH_REMOTE (keep)
└─ NO → Has unique commits?
├─ YES → LOCAL_WORK (keep)
└─ NO → SAFE_TO_DELETE (use -d)
类别定义:
| 类别 | 含义 | 删除命令 |
|---|---|---|
| SAFE_TO_DELETE | 已合并到默认分支 | git branch -d |
| SQUASH_MERGED | 工作通过 squash 合并纳入 | git branch -D |
| SUPERSEDED | 属于某个组,且已通过 PR 或较新分支确认其工作位于 main 中 | git branch -D |
| REMOTE_GONE | 远程分支已删除,且在 main 中未找到相关工作 | 需要审核 |
| UNPUSHED_WORK | 包含尚未推送到远程的提交 | 保留 |
| LOCAL_WORK | 未跟踪的分支,包含独有提交 | 保留 |
| SYNCED_WITH_REMOTE | 与远程保持同步 | 保留 |
第 4 阶段:检测脏状态
检查所有工作树和当前目录是否有未提交的更改:
# For each worktree path
git -C <worktree-path> status --porcelain
# For current directory
git status --porcelain
醒目地显示警告:
WARNING: ../proj-auth has uncommitted changes:
M src/auth.js
?? new-file.txt
These changes will be LOST if you remove this worktree.
门槛 1:呈现完整分析
在一个全面的视图中呈现所有内容。将相关分支归为一组:
## Git Cleanup Analysis
### Related Branch Groups
**Group: feature/api-* (4 branches)**
| Branch | Status | Evidence |
|--------|--------|----------|
| feature/api | Superseded | Work merged in PR #29 |
| feature/api-v2 | Superseded | Work merged in PR #45 |
| feature/api-refactor | Superseded | Work merged in PR #67 |
| feature/api-final | Superseded | Older iteration, diverged |
Recommendation: Delete all 4 (work is in main)
---
### Individual Branches
**Safe to Delete (merged with -d)**
| Branch | Merged Into |
|--------|-------------|
| fix/typo | main |
**Safe to Delete (squash-merged, requires -D)**
| Branch | Merged As |
|--------|-----------|
| feature/login | PR #42 |
**Needs Review ([gone] remotes, no PR found)**
| Branch | Last Commit |
|--------|-------------|
| experiment/old | abc1234 "WIP something" |
**Keep (active work)**
| Branch | Status |
|--------|--------|
| wip/new-feature | 5 unpushed commits |
### Worktrees
| Path | Branch | Status |
|------|--------|--------|
| ../proj-auth | feature/auth | STALE (merged) |
---
**Summary:**
- 4 related branches (feature/api-*) - recommend delete all
- 1 merged branch - safe to delete
- 1 squash-merged branch - safe to delete
- 1 needs review
- 1 to keep
Which would you like to clean up?
使用 AskUserQuestion,并提供清晰的选项:
- 删除所有推荐的分支(分组项 + 已合并 + squash 合并)
- 删除指定的分组/类别
- 让我逐个选择分支
在用户回复前不要继续。
门槛 2:最终确认准确的命令
显示即将运行的确切命令,并使用正确的标志:
I will execute:
# Merged branches (safe delete)
git branch -d fix/typo
# Squash-merged branches (force delete - work is in main via PRs)
git branch -D feature/login
git branch -D feature/api
git branch -D feature/api-v2
git branch -D feature/api-refactor
git branch -D feature/api-final
# Worktrees
git worktree remove ../proj-auth
Confirm? (yes/no)
重要: 这是删除前唯一需要的确认。如果需要使用 -D,不要再添加额外确认。
第 5 阶段:执行
将每项删除作为单独的命令运行,以免部分失败阻止其余删除。报告每项的结果:
git branch -d fix/typo
git branch -D feature/login
git branch -D feature/api
git branch -D feature/api-v2
git branch -D feature/api-refactor
git branch -D feature/api-final
git worktree remove ../proj-auth
如果某项删除失败,报告错误并继续执行其余删除。
第 6 阶段:报告
## Cleanup Complete
### Deleted
- fix/typo
- feature/login
- feature/api
- feature/api-v2
- feature/api-refactor
- feature/api-final
- Worktree: ../proj-auth
### Remaining (4 branches)
| Branch | Status |
|--------|--------|
| main | current |
| wip/new-feature | active work |
| experiment/old | needs review |
安全规则
- 绝不自动调用——仅当用户明确使用
/git-cleanup时才运行 - 仅设置两个确认关卡——先审查分析,再确认删除
- 使用正确的删除命令——已合并的使用
-d,squash 合并/已被取代的使用-D - 绝不触碰受保护的分支——main、master、develop、release/*(以编程方式过滤)
- 阻止移除有未提交更改的工作树——除非用户明确确认接受数据丢失,否则拒绝移除
- 将相关分支归为一组——不要把它们分散到不同类别
应拒绝的合理化说法
以下是可能导致数据丢失的常见捷径。请拒绝:
| 合理化说法 | 错误原因 |
|---|---|
| “这个分支很旧,大概可以安全删除” | 年限并不能说明合并状态。旧分支可能包含尚未合并的工作。 |
| “需要的话,我可以从 reflog 恢复” | reflog 条目会过期。用户通常不知道如何使用 reflog。不要依赖它作为安全网。 |
| “这只是一个本地分支,没什么重要的” | 本地分支可能包含唯一一份尚未推送到任何地方的工作。 |
| “PR 已合并,所以这个分支可以安全删除” | squash 合并不会保留分支历史。请确认具体提交已被纳入。 |
“我只需删除所有 [gone] 分支” | [gone] 只表示远程分支已删除。本地分支可能包含尚未推送的提交。 |
| “用户看起来想删除所有内容” | 始终先呈现分析。让用户选择要删除的内容。 |
| “这个分支有 main 中没有的提交,所以有未推送的工作” | “不在 main 中” ≠ “尚未推送”。分支可以已与其远程分支同步,但尚未合并到 main。始终检查 git log origin/<branch>..<branch>。 |