Skip to content

[BUG] 删除 worktree 被拒(uncommitted or untracked changes):拒绝信息零细节、无 force 出口,且工作区 133 个已跟踪源文件在删除前已消失 #102

Closed

Description

@linletian

现象

在 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:1129 handleWorktreeDelete 是薄封装,无前置清理;
  • 全仓 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

  1. linletian commented on Oct 8, 2026

    @linletian
    OwnerAuthor

    Fixed by #105 (merged into develop). 拒绝信息现带结构化明细(分类计数 + first paths + 完整 porcelain,200 行封顶),HTTP/MCP/CLI 三处均提供 force 出口,并会警告 force 将不可恢复地销毁 gitignored 文件(干净删除也会透报 ignored_destroyed 计数)。P4 经核实无需改动(Manager.List 已实时解析分支)。两轮独立评审的发现均已修复并有测试钉住。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions