首页
/ LobeHub Git Worktree 清理技能:确定性审计与陈旧本地分支的安全清理

LobeHub Git Worktree 清理技能:确定性审计与陈旧本地分支的安全清理

2026-09-04 18:02:40作者:滕妙奇

在多 worktree 协作的开发模式下,长期积累的"已完成合并"或"远端已被删除"的本地分支会不断侵蚀 git worktree list 的可读性。LobeHub 在仓库的 Agent 技能体系中提供了一个名为 cleanup-git-worktrees 的技能(SKILL.md),它用一份确定性脚本来完成"审计—分类—审批—清理—复验"的完整闭环。读完本文,你可以掌握该技能的分类语义、audit/clean 两个子命令的全部参数,以及其底层 Bash 实现中的关键守护逻辑。

一、为什么 LobeHub 需要这个技能

LobeHub 的分支策略决定了 worktree 的周转速度。根据 AGENTS.md 中 "Git Workflow" 一节:

  • canary 是开发分支(对应云端生产环境),main 是发布分支(定期从 canary cherry-pick);
  • 新分支应从 canary 创建,PR 目标是 canary
  • 分支命名格式为 <type>/<feature-name>,即典型的 feat/xxxfix/xxx 短生命周期分支。

短生命周期分支 + 并行开发意味着大量 worktree 在完成 PR 后不再使用。这些残留 worktree 的问题在于:

  1. git worktree list 输出越来越长,无法一眼看出哪些可以清理;
  2. 直接 git branch -d 会因"未合并"(相对 main 而言)而失败——因为 LobeHub 的合并目标是 canary 而非 main
  3. 远端分支被 squash 合并后删除,本地分支的 upstream 会变成 [gone],但 [gone] 并不等于已合并(rebase 合并同理),必须区分处理。

该技能正是为解决这三点而设计的:它把"哪些 worktree 可以安全删除"这个模糊判断,收敛为一组可枚举、可复现的分类标签。

二、技能的结构与设计哲学

技能目录 cleanup-git-worktrees/ 包含三个文件:

文件 职责
SKILL.md 面向 Agent 的操作规程:工作流、分类语义、安全红线
scripts/cleanup.sh 唯一的执行入口,216 行 Bash,负责确定性的解析与守护
agents/openai.yaml 技能对 Agent 的接口元数据(显示名、默认提示词)

SKILL.md 开篇就定下总原则(原文明确要求):使用捆绑脚本让分类保持确定性,不要临时手搓解析和守护逻辑("Use the bundled script to make classification deterministic … do not recreate its parsing and guard logic ad hoc");同时把清理视为破坏性操作——先审计、展示精确候选项、获得用户明确批准后再执行删除,除非用户当前请求已经点名了具体目标。

agents/openai.yaml 提供了技能发现所需的元数据:

interface:
  display_name: 'Cleanup Git Worktrees'
  short_description: 'Safely audit and clean completed Git worktrees'
  default_prompt: 'Use $cleanup-git-worktrees to audit and safely remove completed worktrees and stale local branches.'

三、audit 子命令:只读审计与参数细节

3.1 用法

脚本头部的 usage 定义了两种调用形态(cleanup.sh#L5-L13):

cleanup.sh audit [--fetch] [--base <ref>]
cleanup.sh clean [--base <ref>] --branch <name> [--branch <name> ...] [--apply]

标准审计命令(SKILL.md 推荐写法):

bash .agents/skills/cleanup-git-worktrees/scripts/cleanup.sh audit --fetch --base origin/canary

参数语义如下:

  • --base <ref>:作为"已合并"判断基准的 ref。脚本默认值即 origin/canarycleanup.sh#L31),与 LobeHub 的 PR 目标分支一致。执行前脚本会校验该 ref 存在——依次检查 refs/remotes/<base>refs/heads/<base>,都不存在则直接报错退出(cleanup.sh#L66-L68)。
  • --fetch:触发 git fetch --prune <remote>,其中 remote 从 base ref 的前缀解析(origin/canaryorigin,无前缀则回退为 origin),见 cleanup.sh#L70-L74--prune 是关键:它会同步清理本地已失效的远端跟踪引用,使 [gone] 标记真实反映远端状态。只有在网络不可用时才应省略 --fetch,且必须向用户披露"远端状态可能已过期"(SKILL.md 第 18 行原文要求)。

除可选的 fetch 外,audit 是完全只读的。

3.2 输出格式:两个 scope 的 TSV 报告

audit 输出的是一张 TSV 表,表头为(cleanup.sh#L139):

scope  path  branch  dirty  upstream  track  merged_into_base  classification

字段含义:

字段 来源 说明
scope worktree(绑定 worktree 的分支)或 branch(游离本地分支)
path git worktree list --porcelain worktree 目录;游离分支显示 -
branch 解析 branch refs/heads/ 本地分支名
dirty `git -C wc -l`
upstream git for-each-ref%(upstream:short) 配置的 upstream,无则 none
track %(upstream:track) 领先/落后情况;upstream 已消失时为 [gone]
merged_into_base git merge-base --is-ancestor <branch> <base> yes/no
classification 见下节 七个标签之一

audit 的枚举分两遍(cleanup.sh#L138-L180):第一遍用 awk 解析 git worktree list --porcelain 的块状输出(RS="" 分块、提取 worktree branch refs/heads/ 两行),列出所有"分支名已绑定到某个 worktree"的条目,并用临时文件记录这些分支名;第二遍遍历 git for-each-ref refs/heads,用 grep -Fxq 跳过已绑定的,只报告剩余的自由分支——因此一张表能同时回答"哪些 worktree 可删"和"哪些游离分支可删"。

四、七个分类标签:classify() 的判定优先级

分类是整个技能的核心。classify() 按固定优先级从上到下短路求值,顺序本身就是安全策略的一部分:

protected-branch → protect-current → protect-dirty → candidate-merged → candidate-gone → review-no-upstream → active

各标签的判定实现与 SKILL.md 的语义对照如下:

标签 判定条件(源码) 语义
protected-branch 分支名为 maincanary,或等于 base ref 全名/短名(is_protected_branch() 永不删除。即使 origin/main 上的内容与 canary 高度重合,脚本也不允许通过本路径触碰主干
protect-current worktree 路径经 pwd -P 规范化后与当前 CWD 相同(cleanup.sh#L123 保护正在运行本命令的 worktree,防止自删
protect-dirty dirty > 0 存在已修改或未跟踪文件,保留。脏文件数来自 git status --porcelain 的行数
candidate-merged git merge-base --is-ancestor <branch> <base> 成功(is_merged() 干净且完全包含于 base 分支——这是最强清理候选
candidate-gone %(upstream:track) 输出 [gone] 干净且配置的 upstream 已被 prune。注意 SKILL.md 原文强调:这是陈旧,但不是 squash/rebase 合并后的已合并证明
review-no-upstream 无 upstream 且未被 base 包含 干净但需要人工判断,进入 review 队列
active 其余情况 保留,除非用户明确声明该分支已完成

注意两个关键实现细节:

  1. 优先级即护栏protect-dirty 排在 candidate-merged 之前——一个分支即使已完全合并进 canary,只要工作区有未提交改动就绝不进入候选集,这与 SKILL.md 安全规则"绝不能因为分支已合并或 [gone] 就丢弃脏 worktree"一一对应。
  2. [gone] 与 merged 是两个独立信号audit 报告把它们放在不同列(trackmerged_into_base),SKILL.md 也要求呈现结果时把 candidate-mergedcandidate-gone 分开列示,"不要把 [gone] 描述成已合并";当这一区分真的影响决策时,应去查 PR 平台的实际状态(原文建议查 GitHub PR state)。

按 SKILL.md 第 30 行,向用户呈现审计结果时应给出一张紧凑表格,至少包含:path、branch、dirty 数、upstream 状态、base 包含关系、分类,并遵守上述"merged 与 gone 分列"的要求。

五、clean 子命令:dry-run 默认与删除前再审计

获得批准后,用精确分支名调用 clean(SKILL.md 第 34-40 行示例):

bash .agents/skills/cleanup-git-worktrees/scripts/cleanup.sh clean \
  --base origin/canary \
  --branch feat/example \
  --branch fix/example \
  --apply

clean() 的执行流程:

  1. 参数校验:至少一个 --branch,缺失即 die 'clean requires at least one --branch';每个分支名必须对应真实存在的本地分支(git show-ref --verify),否则整体中止。
  2. 删除前即时再审计:这是脚本最重要的守护。对每个目标分支,clean 会重新解析其绑定的 worktree(branch_worktree())、重新统计 dirty 数、重新调用同一个 classify()。这与 SKILL.md"脚本会在删除前对每个目标重新审计"的描述一致——从审计到删除之间的时间窗口内,分支状态若发生任何变化(比如被 push 了新提交、工作区变脏),分类结果会随之改变,从而被第 3 步拦截。
  3. 白名单拦截:只允许 candidate-mergedcandidate-gone 通过,其余任何分类(包括 protect-dirtyactivereview-no-upstream)都会触发 die "<branch> is <classification>; refusing cleanup" 并整体退出。换言之,无法通过该脚本强制删除"既未包含于 base 也非 [gone]"的分支,这正是 SKILL.md 安全规则第 3 条的落地。
  4. dry-run 默认:不带 --apply 时逐行输出 DRY-RUN <branch> <worktree|-> <classification> 后结束,不产生任何变更;带 --apply 才执行真实删除。
  5. 删除动作:先 git worktree remove <path>(若有绑定 worktree),再 git branch -D <branch>,并分别输出 REMOVED-WORKTREE / REMOVED-BRANCH 标记行,便于自动化解析结果。

选择 git worktree remove 而非目录级递归删除不是随意的:git worktree remove 会同时注销 worktree 注册信息,而绕过它直接删目录会留下需要 git worktree prune 才能回收的"孤儿注册"。SKILL.md 安全规则对此有明确禁令:"绝不使用递归删除来绕过 git worktree remove。如果 Git 部分删除了目录,停下并检查确切路径,再决定如何恢复。"

六、完整安全规则(原文继承)

SKILL.md 的 "Safety rules" 一节共 6 条,是执行该技能时必须全程遵守的红线:

  • 绝不能因为分支已合并或 [gone] 就丢弃脏 worktree;
  • 绝不用递归删除绕过 git worktree remove;若 Git 部分删除了目录,立即停下并检查确切路径后再决定恢复方式;
  • 绝不通过本脚本强制删除"既不在 base 内、也非 [gone]"的分支;
  • 上游被删除表示陈旧,而非必然已合并;当区分二者影响决策时使用 PR 平台状态;
  • 保留无关的用户改动与并行的 worktree;
  • 分类前优先执行 git fetch --prune origingit worktree prune 只用于目录已经消失的注册项。

其中最后一条解释了脚本内没有自动执行 git worktree prune 的原因:prune 只应针对"目录已缺失"的注册项,属于状态修复而非候选删除,是否执行由操作者按实际情况决定。

七、推荐的五步工作流

综合 SKILL.md 的 "Workflow" 一节,一次完整的清理应遵循:

  1. 审计:在仓库任意 worktree 内执行 audit --fetch --base origin/canary;无网络时省略 --fetch 并披露远端状态可能过期;
  2. 解读分类:按第四节的语义表理解每个标签,注意 candidate-gone 不等于已合并;
  3. 呈现候选:给出含 path / branch / dirty / upstream 状态 / base 包含关系 / 分类的紧凑表格,candidate-mergedcandidate-gone 分列;
  4. 批准后清理:把精确分支名传给 clean,不加 --apply 先跑一次 dry-run 核对输出,再加 --apply 执行;
  5. 复验并汇报:再次运行 audit,报告——已删除的 worktree 与分支、被保留的 dirty/current 目标、剩余的 worktree 与本地分支数量、以及任何部分删除或 Git 错误。

八、小结:确定性、幂等与最小破坏面

这个技能的价值不在于命令本身(git worktree remove + git branch -D 并不复杂),而在于把清理决策工程化:

  • 确定性:分类完全由脚本从 git 原始输出推导,同一状态下重复执行得到同一结论,Agent 无需"凭感觉"判断;
  • 最小破坏面:默认 dry-run、删除前再审计、白名单式放行、保护链(protected → current → dirty)全部排在候选链之前;
  • 可审计:TSV 报告与 DRY-RUN/REMOVED-* 标记行都是机器可读的,清理结果可复验、可追溯。

对于采用 LobeHub 这种"canary 开发 + 短生命周期功能分支 + 多 worktree 并行"的团队,这套"先分类、后批准、再删除、终复验"的模式可以直接作为其他仓库 worktree 治理的参考模板。

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