rtk 的 /tech:clean-worktrees 命令:自动清理陈旧 Git worktree 的脚本设计与实现剖析
本文以 rtk 仓库中的 Claude Code 斜杠命令 clean-worktrees.md 为主体,完整解析其“无人值守清理已合并 worktree”的设计:从 --dry-run 预览模式、三步式 bash 脚本(prune 引用 → 识别已合并分支 → 删除 worktree/分支)到五重安全边界。读完你可以掌握一套可直接复用的 worktree 自动化清理脚本,并理解 rtk 代理层(rtk git worktree)如何对这类 git 命令做输出压缩与失败透传。
命令定位:自动版 vs 交互式版
rtk 仓库的 .claude/commands/tech/ 目录下围绕 Git worktree 提供了一组配套命令,/tech:clean-worktrees 是其中的“批量自动清理”角色。frontmatter 中 model: haiku 声明该命令由 haiku 模型执行,description 为 “Auto-clean all stale worktrees (merged branches)”。
它与同目录的交互式命令 clean-worktree.md 的分工是:
| 命令 | 交互性 | 清理范围 | 适用场景 |
|---|---|---|---|
/tech:clean-worktree |
交互式,删除前要求输入 y/Y 确认 |
已合并进 master 的 worktree |
希望人工过目的场合 |
/tech:clean-worktrees |
全自动,无交互(仅限已合并分支,天然安全) | 同上,一次性批量 | 合并 PR 后、每周例行维护 |
值得注意的细节:自动版脚本额外增加了一条交互式版没有的保护规则——[ "$path" = "$(pwd)" ] && continue,即当前所在目录(主仓库)无论如何都不会被移除;而交互式版 clean-worktree.md 的脚本只跳过 master/main 分支。此外,仓库根目录还有一份 clean-worktrees.md,是去掉 emoji 输出的精简变体,逻辑一致。
用法与参数
原文档给出的调用方式只有两条:
/tech:clean-worktrees # Clean all merged worktrees(清理所有已合并 worktree)
/tech:clean-worktrees --dry-run # Preview what would be deleted(预览将被删除的内容,不落盘)
从脚本实现看,参数解析只认一个开关:
DRY_RUN=false
if [[ "${ARGUMENTS:-}" == *"--dry-run"* ]]; then
DRY_RUN=true
fi
即只要 Claude Code 传入的 $ARGUMENTS 中包含 --dry-run 子串即进入预览模式;该模式下脚本只打印 “Would delete:” 清单并 exit 0,不执行任何删除。
实现脚本逐步剖析
完整脚本以 set -euo pipefail 开头(出错即停、未定义变量报错、管道任一环节失败即失败),整体分为三步。以下逐段说明,行号对应 clean-worktrees.md 中的 Implementation 代码块。
第一步:git worktree prune -v 清理悬空引用
PRUNED=$(git worktree prune -v 2>&1)
if [ -n "$PRUNED" ]; then
echo "$PRUNED"
echo "✅ Stale references pruned"
else
echo "✅ No stale references found"
fi
git worktree prune 会删除那些物理目录已经不存在、但 git 元数据中还登记着的 worktree 记录(典型成因:手动 rm -rf 过 worktree 目录、或机器迁移)。-v 让 git 打印实际被清理的条目,脚本据此向用户区分“清理过”和“本来就干净”两种输出。这一步先于扫描执行,保证后续 git worktree list 的结果不含僵尸条目。
第二步:解析 git worktree list 并筛选已合并分支
while IFS= read -r line; do
path=$(echo "$line" | awk '{print $1}')
branch=$(echo "$line" | grep -oE '\[.*\]' | tr -d '[]' || true)
[ -z "$branch" ] && continue
[ "$branch" = "master" ] && continue
[ "$branch" = "main" ] && continue
[ "$path" = "$(pwd)" ] && continue
if git branch --merged master | grep -q "^[* ] ${branch}$" 2>/dev/null; then
MERGED_COUNT=$((MERGED_COUNT + 1))
MERGED_BRANCHES+=("$branch|$path")
echo " ✓ $branch (merged)"
fi
done < <(git worktree list)
这段是整个命令的筛选核心,逐条拆解:
- 数据格式:
git worktree list每行形如/path/to/worktree abc1234 [branch](detached 时方括号内是 commit 描述)。脚本用awk '{print $1}'取路径,用grep -oE '\[.*\]'再tr -d '[]'提取方括号内的分支名;|| true防止 grep 无匹配时触发set -e退出。 - 三重跳过规则:分支名为空的行(detached HEAD)跳过;
master与main作为受保护分支跳过;路径等于当前工作目录(主仓库本身)跳过。 - 合并判定:
git branch --merged master | grep -q "^[* ] ${branch}$"。git branch --merged列出所有已并入master的本地分支,^[* ]同时兼容当前分支(*前缀)和普通分支(空格前缀),末尾$锚定避免feature/a误匹配feature/a-bugfix。 - 候选记录:命中的 worktree 以
分支|路径形式存入MERGED_BRANCHES数组,供第三步遍历。
一个隐含前提值得注意:合并判定硬编码以 master 为集成基线。可以推断,若项目实际以 main 为合并目标,此脚本只能保护 main 不被删、却不会把“已并入 main 的分支”识别为 merged。迁移脚本时应把两处 master 替换为实际基线分支。
若无任何命中,脚本直接打印 “No merged worktrees found”、附上 git worktree list 现状并 exit 0——零副作用退出。
第三步:删除 worktree 与本地分支,报告远端状态
非 dry-run 模式下,对每个候选执行“三连击”:
# 1) 删 worktree,失败则强制兜底
if git worktree remove "$path" 2>/dev/null; then
echo " ✅ Worktree removed"
else
echo " ⚠️ Git remove failed, forcing..."
rm -rf "$path" 2>/dev/null || true
git worktree prune 2>/dev/null || true
echo " ✅ Worktree forcefully removed"
fi
# 2) 删本地分支(-d 只会删已合并分支,与前置判定自洽)
if git branch -d "$branch" 2>/dev/null; then
echo " ✅ Local branch deleted"
else
echo " ⚠️ Local branch already deleted"
fi
# 3) 探测远端分支:只报告,不自动删
if git ls-remote --heads origin "$branch" 2>/dev/null | grep -q "$branch"; then
echo " 🌐 Remote branch exists: $branch"
echo " (Skipping auto-delete - use /tech:remove-worktree for manual removal)"
fi
三个设计点:
- force 兜底:
git worktree remove在目录含未跟踪文件时会拒绝。脚本退化为rm -rf+git worktree prune手工清元数据。这一兜底之所以“安全”,是因为目标已经被第二步判定为 merged——前提是第二步的合并判定可信。 - 分支删除用
-d而非-D:git branch -d本身会拒绝删除未合并分支,形成第二道保险;失败分支被统一解释为“已删除”,不计为错误。 - 远端只报告不删除:删除远端分支会影响他人(fork 跟踪、CI 引用),脚本通过
git ls-remote --heads origin探测后仅提示“可用/tech:remove-worktree手动处理”,把破坏半径限定在本地。
循环结束后打印汇总:Removed: $REMOVED_COUNT worktree(s)、剩余 worktree 列表,以及 .worktrees/ 目录占用:
WORKTREES_SIZE=$(du -sh .worktrees/ 2>/dev/null | awk '{print $1}' || echo "N/A")
echo "💾 Worktrees disk usage: $WORKTREES_SIZE"
从脚本结构看,声明的 FAILED_COUNT 在删除循环中并未被任何分支自增,因此汇总中的 “Failed” 行实际不会触发——失败信息是通过每个 worktree 的 “⚠️ … forcing…” 现场输出传达的。这属于可读性冗余而非功能缺陷,但复写脚本时可考虑让 force 兜底时递增该计数。
安全机制逐条核对
原文档 “Safety Features” 一节列出的五条,均可在脚本中找到对应实现:
| 安全特性 | 脚本对应实现 |
|---|---|
| 只动已合并分支,绝不触碰未合并工作 | 第二步 git branch --merged master 前置过滤;第三步 git branch -d 二次确认 |
| 受保护分支 | [ "$branch" = "master" ] && continue、[ "$branch" = "main" ] && continue |
| 主仓库永不被删 | [ "$path" = "$(pwd)" ] && continue |
| 远端分支只报告不自动删 | git ls-remote 探测后仅 echo 提示 |
| dry-run 预览 | --dry-run 时打印 “Would delete:” 清单后 exit 0 |
配套的 remove-worktree.md 命令则处理自动版刻意不覆盖的场景:删除单个指定 worktree(含未合并分支的交互式确认、可选的远端分支删除 git push origin --delete),可视为自动版的逃生舱口。
使用时机与配套命令
原文档给出的三个触发时机:
- 合并 PR 进 master 之后:feature 分支的生命周期结束,worktree 成为纯磁盘垃圾;
- 每周例行维护:积累的陈旧 worktree 会膨胀
.worktrees/占用; - 创建新 worktree 之前:先跑一次
/tech:clean-worktrees --dry-run预览,避免“worktree already exists”冲突。
它与 worktree.md(创建工作树 + 后台 cargo check)、worktree-status.md(查后台检查状态)构成完整闭环:/tech:worktree 创建 → /tech:worktree-status 查看 → 开发合并 → /tech:clean-worktrees 收尾。
对于未合并分支的手动强删,原文档指路 /tech:remove-worktree <branch>;裸 git 等价操作为:
git worktree remove --force <path>
git branch -D <branch_name>
git worktree prune
rtk 代理层视角:rtk git worktree 如何压缩输出
rtk 本身的定位是“减少 LLM token 消耗的 CLI 代理”,而上述清理脚本的每一步(prune、list、remove)都经过 rtk git 时会被专门处理。从 git.rs 的源码看,调用链由 gt_cmd.rs 的 "worktree" => run(GitCommand::Worktree, ...) 路由到 run_worktree(git.rs#L2135),分两种模式:
- 动作模式:参数含
add/remove/prune/lock/unlock/move之一时,原样透传给真实 git;成功仅输出一个ok,失败则打印FAILED: git worktree ...及 stderr 并透传原始退出码——这保证清理脚本里set -euo pipefail与if git worktree remove ...的成败判断在 rtk 代理下依然成立。 - 列表模式(默认):执行
git worktree list后由filter_worktree_list(git.rs#L2208)压缩输出——按空白切分路径/短hash/[分支]三段,把绝对路径开头的$HOME替换为~,缩短每行长度;随后再经过never_worse守护,确保“过滤后输出绝不会比原始输出更长”,否则回退为原始输出。
测试侧有对应覆盖:test_filter_worktree_list 验证路径截断与格式归一,test_run_worktree_list_propagates_failure(对应 issue #2497)验证在仓库外执行 git worktree list 的非零退出码必须向上传播——这正是清理脚本依赖的失败语义。
换言之,若你在 rtk 环境里运行本命令,git worktree list 的每行路径会短一截(~ 前缀),而脚本解析只取 $1 路径与方括号分支名,两种长度下行为一致;prune -v 的 -v 输出在动作模式透传下也原样可见。
小结
/tech:clean-worktrees 的价值不在脚本本身有多复杂,而在把“可安全自动执行”的边界划得非常清楚:合并判定(--merged)+ 受保护分支 + 主仓库豁免三重前置过滤决定“能不能删”,-d 软删与远端只报告决定“删多狠”,--dry-run 决定“删不删”。这套“前置过滤 + 软操作 + 预览模式”的三层结构,是写任何批量清理类 Agent 命令时都值得照搬的模板。完整脚本与参数说明见 clean-worktrees.md,交互式版本见 clean-worktree.md,代理层实现见 git.rs。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00