Repository navigation
[BUG] 删除 worktree 被拒(uncommitted or untracked changes):拒绝信息零细节、无 force 出口,且工作区 133 个已跟踪源文件在删除前已消失 #102
Description
现象
在 myworktree 里删除一个 worktree 时被拒:
Error: worktree has uncommitted or untracked changes; delete is refused
但用户侧确认在这次删除操作发生前,该工作区没有任何未提交内容。事后(用外部 agent 人工介入排查)发现该 worktree 的工作区里,133 个已跟踪源文件已经消失,且它们恰好构成两个完整的顶层目录。
即:报错本身是"诚实"的(git status --porcelain 确实非空),但它把一个**"工作区内容被预先破坏"的异常,呈现成了"用户有未提交改动所以拒绝删除"**的日常提示。用户无法从这个错误里看出真实发生了什么。
复现条件(本次实例)
| 项 | 值 |
|---|---|
| myworktree 版本 | v0.5.1-beta.3 (8cd4d0d59f37),built 2026-10-06 |
| 平台 | macOS 12 (darwin 22.6.0, x64) |
| 被删 worktree | id 01f6****(脱敏),注册名 <repo>-<suffix> |
| worktree 实际 HEAD | 已完全并入 origin/<default>(即该分支内容没有任何未推送的损失) |
| 该 worktree 当时挂载的实例 | 2 个(dsh-web + pty,均 running) |
关键点:该 worktree 的分支已在 worktree 内部被 git switch 改过,myworktree 侧记录的分支与磁盘实际分支不一致(见下方 P4)。
证据(已脱敏)
1. 消失范围:恰好两个完整顶层目录,其余完好
按顶层目录聚合 git status --porcelain(在被删 worktree 的 HEAD 上复算):
| 顶层树 | HEAD 跟踪文件数 | 状态 |
|---|---|---|
firmware/ |
117 | 全部 133 个中的 117 个:工作区已删除,未暂存 |
tools/ |
16 | 16 个:工作区已删除,未暂存 |
docs/ |
210 | 完好 |
reference/ |
142 | 完好 |
顶层文件(AGENTS.md 等) |
2 | 完好 |
| 合计 | 487 | 其中 133 个呈 D |
133 = 117 + 16,且 133 + 210 + 142 + 2 = 487 校验通过。
这不是一次无差别的 rm -rf:4 个顶层树里 2 个被清空、2 个分毫未动;被清空的两个是"完整目录"而不是零散文件。
2. 状态码是纯删除,无新增/修改/重命名
git status --porcelain 中 133 条全部为 D(工作区删除、未暂存);??(未跟踪)0 条;非删除类改动(M / A / R)0 条。
3. 时间线
| 时刻 | 事件 |
|---|---|
| 删除操作之前 | 用户认为工作区干净 |
约 2026-10-08 13:24(本地) |
worktree 父目录 mtime 更新;此刻前后文件已消失 |
约 2026-10-08 13:28 |
用户在 myworktree 触发删除 → 报错拒绝 |
约 13:30 |
外部 agent 介入,检查到上述 133 个文件已缺失 |
即:文件消失早于第一次删除尝试,不是删除流程自身造成的(见下)。
4. 手工救援:89 个 gitignore 文件里,64 个是"单点副本"
该 worktree 内有 89 个 gitignore 命中的文件(构建缓存、*.log 实测日志、NVS 参数备份 mfg_<board>_pre*.bin × 8 份等),主工作区不存在其中 64 个的副本。
关键点:Delete() 用的是 git status --porcelain(不带 --ignored),所以这 89 个文件不会阻止删除。一旦按提示改用 force 或手动删除,这 64 个文件将不可恢复——本次是人工用 rsync --ignore-existing 抢救出来的,删完 worktree 后主工作区从 38→48、28→82 个文件。
代码定位(v0.5.1-beta.3 / 8cd4d0d)
internal/worktree/worktree.go:321 Manager.Delete(id):
if _, err := os.Stat(wt.Path); err != nil {
if !errors.Is(err, os.ErrNotExist) { return ... }
// 路径已不存在 → 直接 git worktree prune,删除成功
} else {
cmdStatus := gitx.GitCommand(10*time.Second, wt.Path, "status", "--porcelain")
statusOut, err := cmdStatus.Output()
if err != nil { return fmt.Errorf("git status failed: %w", err) }
if strings.TrimSpace(string(statusOut)) != "" {
return errors.New("worktree has uncommitted or untracked changes; delete is refused") // :357
}
cmd := gitx.GitCommand(10*time.Second, m.GitRoot, "worktree", "remove", wt.Path)
...
}已排查排除的假设(供缩小范围):
Delete()在脏检查之前不做任何文件删除;internal/app/app.go:1129handleWorktreeDelete是薄封装,无前置清理;- 全仓
os.RemoveAll仅 2 处,均作用于实例数据目录(dsh_web/driver.go:474、reasonix/driver.go:628),不触碰 worktree 路径; - 全仓无针对 worktree 内容的 cleanup / reclaim / prune 功能(
Cleanup系列只清实例与 portal 数据); - 在该项目的 agent 会话记录中未检索到对应的
rm -rf/git clean/git checkout --记录。
⇒ 133 个文件的消失不是 Delete() 路径造成的,需要排查其它来源(实例侧脚本、agent 会话、外部命令等)。本次 issue 的确定性 bug 是下面 P1~P3;P4 是顺带发现的状态不一致。
建议
P1(主诉)拒绝信息零细节,不可诊断
worktree has uncommitted or untracked changes; delete is refused 不含任何数量或路径,用户无法区分:
- "我留了个临时文件"(低风险)
- "133 个已跟踪源文件被删了"(工作区已被破坏,应当先恢复而非删除)
建议:错误里带上摘要,例如 ... refused: 133 uncommitted entries (133 deleted, 0 modified, 0 untracked); first paths: firmware/CMakeLists.txt, ...;Web UI 里可折叠展开完整 git status --porcelain 输出,并区分「已跟踪改动」与「gitignore 文件」两类风险。
P2 没有 force 删除出口,被拒后只能绕到终端
internal/worktree/ 与 handleWorktreeDelete 中均无 force 选项(已 grep 确认)。被拒后用户在 UI 里没有任何出路。本次最终是我在外部手动执行 git worktree remove --force 才删掉的,而 Manager.Delete() 并不感知这件事——删除后 state 里仍留有该 worktree 记录(id 01f6****,path 已不存在),要靠用户再点一次删除、走 os.Stat → ErrNotExist → prune 分支才自愈。
建议:给 Delete 加 force bool(HTTP body + CLI --force),并明确语义。
P3 force 路径必须显式警告 gitignore 文件会一并消失
如证据 4:脏检查不带 --ignored,因此大量本地唯一文件(NVS 参数备份、实测日志)不构成任何阻拦。当前错误文案也没有传达这一点,force 后它们直接蒸发。
建议:拒绝信息里单独列出「另有 N 个 gitignore 文件,force 删除将不可恢复」,force 时要求二次确认。
P4(顺带)state 记录的分支与 worktree 实际分支不一致
- myworktree
state.json记录:branch = feature/<A> - 磁盘上 worktree 实际检出:
feature/<B>(worktree 内部被git switch过)
Delete() 只用 wt.Path,所以不影响删除,但 UI 上展示的分支是陈旧/错误的。
备注:残留清理(顺手反馈)
git worktree list里有 2 条prunable失效条目(gitdir file points to non-existent location),需要手动git worktree prune才会消失。Delete()只在"自己管的 worktree 路径消失"时顺带 prune,不覆盖这类外部残留。- 外部 AI CLI 工具(omo/codegraph)在 worktree 根留下的
.codegraph符号链接指向~/.omo/...,worktree 删除后成为孤立缓存(本次 11 MB),myworktree 不感知也不清理。
Activity
chore: gofmt the issue #102 tests
fix(worktree): detailed delete refusal, force exit, gitignored-file w…
chore: gofmt the issue #102 tests
fix(worktree): review-round fixes — trailing --force, force skips the…
fix(worktree): second review round — omit ignored_destroyed on force,…
test(cli): resolve symlinks on temp paths — macOS CI failure (issue #102
fix(worktree): the delete refusal now details the dirty state, warns …
linletian commentedon Oct 8, 2026
Fixed by #105 (merged into develop). 拒绝信息现带结构化明细(分类计数 + first paths + 完整 porcelain,200 行封顶),HTTP/MCP/CLI 三处均提供 force 出口,并会警告 force 将不可恢复地销毁 gitignored 文件(干净删除也会透报 ignored_destroyed 计数)。P4 经核实无需改动(Manager.List 已实时解析分支)。两轮独立评审的发现均已修复并有测试钉住。