首页
/ RTK 的 /clean-worktrees 命令:自动批量清理已合并 Git Worktree 的完整实现剖析

RTK 的 /clean-worktrees 命令:自动批量清理已合并 Git Worktree 的完整实现剖析

2026-09-05 12:39:29作者:伍霜盼Ellen

本文围绕 rtk 仓库中 .claude/commands/clean-worktrees.md 命令定义展开:它是一条面向 Claude Code 的自动化清理命令,用于一键删除所有已合并进 master 分支的 git worktree,全程无需交互确认。读完本文,你能完整理解这条命令的三步执行流程(prune 失效引用、识别已合并 worktree、安全删除)、--dry-run 预览机制、五重安全护栏的实现细节,以及它与 rtk worktree 工作流中 /worktree/worktree-status/clean-worktree 等命令如何衔接成一套完整的隔离开发环境生命周期管理方案。

背景:rtk 的 worktree 工作流为什么需要自动清理

rtk 是一个用 Rust 编写的 CLI 代理工具(详见 CLAUDE.md 的项目概述),其贡献工作流重度依赖 git worktree 做隔离开发:/worktree 命令会按照约定把分支 feature/new-filter 创建到 .worktrees/feature-new-filter 目录(分支名中的 / 替换为 - 作为目录名),并在后台异步运行 cargo check/worktree-status 命令负责查询后台检查结果。

当 PR 陆续合并回 master 后,这些 .worktrees/ 目录会不断堆积,占用磁盘空间并让 git worktree list 的输出日益冗长。人工逐个执行 git worktree remove 既低效又容易误删。.claude/commands/clean-worktrees.md 正是为此设计的"无人值守"清理命令——它的 frontmatter 明确声明:

---
model: haiku
description: Clean all merged worktrees automatically (no interaction)
---

model: haiku 表明该命令交给轻量模型执行(脚本本身就是确定性的 bash,不需要强推理),这也是 Claude Code custom command 的常见写法:文档即 prompt,内嵌脚本即实现。

命令对比:/clean-worktrees 与 /clean-worktree

仓库中同时存在两条清理命令,定位不同,文档开头的对比表是理解边界的关键:

命令 定义文件 交互方式 适用场景
/clean-worktree(单数) .claude/commands/clean-worktree.md 交互式,删除前询问 [y/N] 确认 想逐项审查后再清理
/clean-worktrees(复数) .claude/commands/clean-worktrees.md 全自动,一次删除所有已合并分支的 worktree 合并 PR 后批量清理、定时维护

此外还有 /tech:remove-worktree 用于删除指定单个 worktree(支持未合并分支的强删确认),三者共同构成"创建 → 检查 → 单个删除 → 批量清理"的闭环。

使用方法

原文档定义的用法非常简洁:

/clean-worktrees              # Remove all merged worktrees
/clean-worktrees --dry-run    # Preview what would be deleted
  • 不带参数:实际执行删除;
  • --dry-run:只预览"将要删除什么",不做任何修改,适合在创建新 worktree 前评估磁盘状况。

完整实现脚本剖析

命令文档的核心是"Execute this script"——把以下 bash 脚本作为可执行规范交给 Claude Code 运行。下面完整继承原文档脚本,并按其内部逻辑分三段讲解:

#!/bin/bash
set -euo pipefail

DRY_RUN=false
if [[ "${ARGUMENTS:-}" == *"--dry-run"* ]]; then
  DRY_RUN=true
fi

echo "Cleaning Worktrees"
echo "=================="
echo ""

# Step 1: Prune stale git references
echo "1. Pruning stale git references..."
PRUNED=$(git worktree prune -v 2>&1)
if [ -n "$PRUNED" ]; then
  echo "$PRUNED"
  echo "Stale references pruned"
else
  echo "No stale references found"
fi
echo ""

# Step 2: Find merged worktrees
echo "2. Finding merged worktrees..."
MERGED_COUNT=0
MERGED_BRANCHES=()
CURRENT_DIR="$(pwd)"

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" = "$CURRENT_DIR" ] && 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)

if [ $MERGED_COUNT -eq 0 ]; then
  echo "No merged worktrees found"
  echo ""
  echo "Current worktrees:"
  git worktree list
  exit 0
fi

echo ""
echo "Found $MERGED_COUNT merged worktree(s)"
echo ""

if [ "$DRY_RUN" = true ]; then
  echo "DRY RUN - No changes will be made"
  echo ""
  echo "Would delete:"
  for item in "${MERGED_BRANCHES[@]}"; do
    branch=$(echo "$item" | cut -d'|' -f1)
    path=$(echo "$item" | cut -d'|' -f2)
    echo "  - $branch"
    echo "    Path: $path"
  done
  echo ""
  echo "Run without --dry-run to actually delete"
  exit 0
fi

# Step 3: Remove merged worktrees
echo "3. Removing merged worktrees..."
REMOVED_COUNT=0

for item in "${MERGED_BRANCHES[@]}"; do
  branch=$(echo "$item" | cut -d'|' -f1)
  path=$(echo "$item" | cut -d'|' -f2)

  echo ""
  echo "Removing: $branch"

  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

  if git branch -d "$branch" 2>/dev/null; then
    echo "  Local branch deleted"
  else
    echo "  Local branch already deleted"
  fi

  if git ls-remote --heads origin "$branch" 2>/dev/null | grep -q "$branch"; then
    echo "  Remote branch exists: origin/$branch (not auto-deleted)"
  fi

  REMOVED_COUNT=$((REMOVED_COUNT + 1))
done

echo ""
echo "Cleanup complete"
echo ""
echo "Summary:"
echo "  Removed: $REMOVED_COUNT worktree(s)"
echo ""
echo "Remaining worktrees:"
git worktree list
echo ""

WORKTREES_SIZE=$(du -sh .worktrees/ 2>/dev/null | awk '{print $1}' || echo "N/A")
echo "Worktrees disk usage: $WORKTREES_SIZE"

参数解析:从 $ARGUMENTS 到 DRY_RUN 开关

Claude Code 会把命令后跟的参数注入 ${ARGUMENTS} 变量。脚本开头用 [[ "${ARGUMENTS:-}" == *"--dry-run"* ]] 做子串匹配——注意 :- 默认值写法保证了不带参数时不会触发 set -u 的 unbound variable 错误。这是 bash 严格模式(set -euo pipefail)下处理可选参数的典型防御写法。

Step 1:git worktree prune -v 清理失效引用

worktree 的元数据记录在 .git/worktrees/ 下,但如果 worktree 目录被外部手段(如直接 rm -rf)删除过,git 仍会保留一条"僵尸"记录。git worktree prune -v 会在正式扫描前把这些陈旧引用清掉,且用 -v 把被清除的条目打印出来——如果输出非空([ -n "$PRUNED" ]),脚本会明确提示 "Stale references pruned",否则报告 "No stale references found"。这一步确保 Step 2 遍历的 git worktree list 结果是干净可信的。

Step 2:解析 worktree 列表,筛出已合并分支

这是整个脚本的识别核心,逐行拆解其解析策略:

  • 数据来源git worktree list 的人类可读输出。每行形如 /path/to/worktree [branch-name](主仓库行末标注 (main repo),无分支方括号)。
  • 路径提取awk '{print $1}' 取第一个空白分隔字段作为 worktree 物理路径。
  • 分支提取grep -oE '\[.*\]' | tr -d '[]' 从方括号中提取分支名。末尾的 || true 很关键——在 set -o pipefail 下,主仓库那一行没有方括号时 grep 会返回非零退出码,若不加 || true 整个脚本会直接终止;随后 [ -z "$branch" ] && continue 把无分支的行(即主仓库)跳过。
  • 三重跳过保护:空分支、master/main(受保护主干)、$CURRENT_DIR(当前工作目录,防止把用户正站在其中的仓库删掉)。
  • 合并判定git branch --merged master 列出所有已合入 master 的本地分支,再用 grep -q "^[* ] ${branch}$" 做行锚定精确匹配(* 表示当前分支前缀, 表示普通分支,$ 防止前缀误匹配,如 fix/a 误中 fix/a-b)。

命中的条目以 分支|路径 的形式存入 MERGED_BRANCHES 数组,同时实时打印 - $branch (merged) 清单。若一个都没找到,脚本打印当前 git worktree list 后以 exit 0 正常结束——这是"幂等"设计:重复执行无副作用。

--dry-run 分支:只报告,不执行

识别完成后,若 DRY_RUN 为 true,脚本进入纯预览分支:逐项输出 Would delete: 清单(分支名 + 物理路径),提示 "Run without --dry-run to actually delete",然后 exit 0。此时不会触碰任何 worktree、分支或磁盘数据,可以安全地用于"先看后删"的评估场景。

Step 3:删除 worktree + 本地分支,保留远端

对数组中的每个条目依次执行三级操作,每一级都有降级路径:

  1. git worktree remove "$path":正规删除(会校验目录状态)。失败时降级为强制清理:rm -rf "$path" 后紧跟 git worktree prune 消除残留引用,并打印 "Worktree forcefully removed"。这正是 git 官方对"目录脏了 remove 失败"场景推荐的手工兜底组合。
  2. git branch -d "$branch":只用安全删除(-d,要求已合并,本脚本中该条件必然成立)。失败时不降级为 -D,而是如实报告 "Local branch already deleted"——与 /tech:remove-worktree 中对未合并分支提供 git branch -D 强删形成刻意的安全梯度差异。
  3. 远端分支探测git ls-remote --heads origin "$branch" 查询 origin 上是否还存在同名分支;存在时只报告 "Remote branch exists: origin/$branch (not auto-deleted)",绝不自动删除。远端删除属于不可逆的共享状态变更,被刻意排除在自动流程之外。

收尾输出:可审计的执行摘要

脚本结尾打印三段信息,让每次执行都留下可核查的痕迹:

  • Summary: Removed: N worktree(s)——本次删除计数;
  • git worktree list——清理后的完整存量清单;
  • du -sh .worktrees/ 的磁盘占用(取 du 输出的第一列人类可读大小,失败时回退为 N/A)——直接呼应 rtk worktree 约定统一存放于 .worktrees/ 目录(见 .claude/commands/worktree.mdWORKTREE_DIR="$REPO_ROOT/.worktrees/$WORKTREE_NAME" 的路径构造逻辑)。

安全特性(Safety Features)汇总

原文档列出的五重安全护栏,与脚本代码一一对应:

  1. 只删已合并分支——git branch --merged master 是唯一的入选条件,未合并分支永远不会进入删除队列;
  2. mastermain 受保护——Step 2 中显式 continue 跳过,双主干命名都覆盖;
  3. 绝不删除当前工作目录——[ "$path" = "$CURRENT_DIR" ] && continue 防止脚本运行时把脚下仓库误删;
  4. --dry-run 预览模式——任何删除前都可以零风险验证将要发生的事;
  5. 远端分支只报告不删除——git ls-remote 探测后仅打印提示,远端状态由人决定。

另外从脚本结构看还有两处文档未逐条列出但同样重要的保护:set -euo pipefail 严格模式下任何意外失败都会立即中止而非带着中间错误继续执行;grep 分支匹配使用 ^[* ] ...$ 锚定,避免分支名前缀误伤。

何时使用

原文档给出的三个典型时机:

  • 合并 PR 之后/clean-worktrees 立即回收已交付的开发环境;
  • 每周例行维护:作为固定节奏的磁盘与元数据清理;
  • 创建新 worktree 之前:先跑 /clean-worktrees --dry-run 预览现状,避免 .worktrees/ 下同名目录冲突(.claude/commands/worktree.md 的 Troubleshooting 一节明确列出了 "worktree already exists" 这一常见故障,其根治手段正是先做清理)。

手动删除未合并分支(命令的边界外场景)

/clean-worktrees 有意不碰未合并分支。若确实需要手工清理某个未合并的 worktree,原文档给出了对应的逃生通道:

git worktree remove --force .worktrees/feature-name
git branch -D feature/name
git worktree prune

这三条命令分别强制移除 worktree 目录、强删本地分支(-D 忽略合并状态)、清除 git 残留引用。需要注意的是 --force + -D 组合会丢弃未合并的工作,仓库中交互式的 /tech:remove-worktree 命令正是把这三步封装成了带"未合并工作将被删除 [y/N]"确认提示的安全流程,可优先于裸命令使用。

小结:一条命令背后的工程约定

/clean-worktrees 的价值不止于一段 bash 脚本:它是 rtk 仓库整套 worktree 约定(.worktrees/ 统一目录、category/description 分支命名、.gitignore 中的 .worktrees/ 忽略规则、后台 cargo check)的收尾环节。通过"先 prune 清脏、再精确识别已合并项、最后分级删除且保留远端"的三步设计,它在无人值守执行的前提下,把误删风险压缩到接近零——这正是把 git 的散装命令(worktree list/prune/removebranch --mergedls-remote)编排成可复用的团队级运维原语的思路,对任何重度使用 git worktree 的仓库都有直接的借鉴价值。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384