删掉分支后 Git Worktree 变成幽灵

分支或目录被删除后 git worktree 损坏,git worktree list 报错。用 prune 清理悬空注册、用 repair 修复被移动的 worktree、抢救未提交改动——基于 Git 2.36+ 实测命令。

你合并了一个功能 PR、删掉了分支,结果 ../project-feature 这个 linked worktree 坏了。git worktree list 把它显示成 [detached HEAD] 或标注为 prunablecd ../project-feature && git status 直接报 fatal: not a git repository。又或者反过来:你想重建它,却被告知 fatal: 'feature/login' is already checked out at '../project-feature'。所有这些情况的根因是同一个——每个 worktree 把自己的元数据存在主仓库的 .git/worktrees/<name>/ 里,而当分支被删、或目录被手动移除时,Git 从不自动清理这份元数据。

最快的修法: 如果目录已经没了、只剩注册记录,在主仓库里运行 git worktree prune 就完事了。如果目录还在、但 worktree 打不开(你移动过它,或移动过主仓库),那就改用 git worktree repair——它会重新建立链接,不删任何文件。本文剩下的部分,是为那些需要先抢救未提交改动、或先解锁的情况准备的。

先判断你属于哪一类

在主仓库里运行下面这条,把输出对照到对应的修法:

git worktree list --porcelain
你看到的输出原因跳到
某个 worktree 行后面跟着 prunable gitdir file points to non-existent location目录被手动删除(原因 1)git worktree prune——Step 2
显示 detached,且旧分支已不存在分支在 checkout 状态下被删(原因 2/3)抢救后重建分支——Step 5
cd 进去后报 fatal: not a git repository,但文件都在.git 链接文件损坏,或主仓库被移动(原因 4)git worktree repair——Step 4
locked 标注worktree 被刻意 lock,或位于已卸载的设备上解锁——Step 3b
目录是空的 / 只有一个 .gitadd 过程中磁盘写满(原因 5)先 prune 再 re-add——Step 2、6

prunable 是 Git 用来表示「这个 worktree 的目录不在了,所以我愿意删掉它的注册记录」的确切措辞。分支清理之后,这是最常见的一类。

常见原因

按命中率从高到低排列。

1. 没用 git worktree remove 就删了目录

有人直接 rm -rf ../project-feature,或者磁盘清理脚本把它清掉了。但 .git/worktrees/project-feature/ 里的元数据还留在主仓库里,于是 Git 一直在报告一个已经没有工作目录的 worktree。

怎么判断git worktree list --porcelain 会在那一条下面打印 prunable gitdir file points to non-existent location,而 ls -la ../project-feature 返回 “No such file or directory”。

2. worktree 还 checkout 着,分支就被删了

git branch -d feature/login(或 git fetch --prune 把已删除的远端分支的 tracking ref 清掉了)。worktree 目录还在磁盘上,但它指向的分支没了,于是 HEAD 进入 detached 状态。

怎么判断:在 worktree 里 git status 显示 HEAD detached at abc1234;从主仓库 git worktree list 里那条路径显示 [detached HEAD]

注意:Git 在这里其实会主动保护你。如果分支还在某个 worktree 里 checkout 着,你执行 git branch -d feature/login 会被拒绝并报 error: Cannot delete branch 'feature/login' checked out at '<path>'。会进入 detached 状态,通常是因为分支是从另一个克隆里删的,或者远端分支被 prune 掉了。

3. worktree 建在了 remote-tracking 分支上

git worktree add ../project-feature origin/feature/login 一开始就会创建一个 detached-HEAD 的 worktree。当 origin/feature/login 在上游被删除后,git fetch --prune 会在本地把它清掉,于是这个 worktree 停留在一个没有分支的 SHA 上,成了孤儿。

怎么判断git -C ../project-feature branch -vv 会把上游显示成 [gone];或者 cat .git/worktrees/<name>/HEAD 里是一串裸 SHA,而不是 ref: refs/heads/...

4. worktree 的 .git 链接文件坏了,或主仓库被移动

每个 linked worktree 里都有一个 .git 文件(不是目录),里面只有一行:gitdir: /path/to/main/.git/worktrees/<name>。如果你手动移动了 worktree、移动或重命名了主仓库,或这个文件被覆盖,worktree 就找不到它的父仓库了——反过来父仓库也找不到它。

怎么判断cat ../project-feature/.git 里的 gitdir: 路径已经不存在;或者 cd ../project-feature && git statusfatal: not a git repository,可源码文件明明都在。这种情况要用 git worktree repair 修,而不是 prune——见 Step 4。

5. git worktree add 期间磁盘写满

add 跑到一半,在 .git/worktrees/ 里写下了元数据,但因为磁盘满了,checkout 没完成。结果你得到一个空目录加一个只注册了一半的 worktree。

怎么判断ls -A ../project-feature/ 什么都没有、或只有一个 .git,而 df -h . 显示当时磁盘几乎满了。

最短修复路径

Step 1:列出所有 worktree

git worktree list --porcelain

porcelain 形式会为每个坏掉的条目打印 prunable 标注和具体的 gitdir 原因。标了 prunable 的都可以安全删除;能用 repair 救回来的则应该保留。

Step 2:清理目录已不存在的注册记录

git worktree prune --dry-run --verbose   # 预览:"Removing worktrees/project-feature: gitdir file points to non-existing location ..."
git worktree prune --verbose             # 真正执行清理

普通的 git worktree prune 会立即清掉所有失效条目,不管它存在多久。(你可能看到过「3 个月宽限期」的说法——那只在 git gc 通过 gc.worktreePruneExpire 调用 prune 时才生效,默认是 3.months.ago手动 prune 并不应用这个宽限期。)执行完之后,git worktree list 里就不该再出现那个不存在的路径了。

Step 3:移除或解锁那些不肯乖乖走的 worktree

# 目录还在,但你想删掉它(有未提交改动时会被拒绝):
git worktree remove ../project-feature

# 当遇到 "is dirty, use --force to delete it" 的拒绝时强制删除(会丢弃未提交改动):
git worktree remove --force ../project-feature

Step 3b——已 lock 的 worktree。 如果 list 显示 locked,或 remove 报告 worktree 被锁,要先清掉锁。单个 --force 并不能覆盖锁,你需要两个 --force,或者显式 unlock:

git worktree unlock ../project-feature
git worktree remove ../project-feature
# 或者一步到位——第二个 --force 才是用来覆盖锁的:
git worktree remove --force --force ../project-feature

Step 4:修复被移动或丢了链接的 worktree(这种别去 prune)

如果文件都还在、但 Git 打不开这个 worktree——你移动过目录、移动过主仓库、或 .git 链接文件被覆盖了——git worktree repair 会重写元数据、让它指回真实位置。这个命令从 Git 2.36(2022 年 4 月)起可用。

# 在被移动的那个 worktree 内部运行:
cd ../moved-feature
git worktree repair

# 或者从主仓库按路径修复一个或多个 linked worktree:
git worktree repair ../moved-feature ../another-worktree

repair 是非破坏性的:它从不删除你的 checkout,只是双向重连 gitdir 指针。只要工作文件还在,就用它,而不是 prune。

Step 5:分支被删后,把 detached 的 worktree 重新接上分支

当 worktree 本身没问题、只是分支没了(原因 2 和 3),趁还没丢掉那个 SHA,先给当前 commit 安个家:

cd ../project-feature
git switch -c feature/login-revived   # 在当前 commit 上建一个新分支
# 或者改为接到一个已有分支:
git switch main

回到主仓库,确认它不再是 detached:

git worktree list   # 那条路径现在应该显示分支名,而不是 [detached HEAD]

Step 6:在干净的新路径上重新 add

# prune/remove 清掉旧注册之后:
git worktree add ../project-feature feature/login-v2

如果 addfatal: '../project-feature' is not an empty directory,说明旧目录还在——把里面有用的文件移出去,删掉目录,再重新 add。

怎么确认已经修好了

git worktree list --porcelain

当每个条目的路径都存在、没有任何一行写着 prunable、而你关心的那个 worktree 显示的是分支(branch refs/heads/...)而非裸的 detached 时,就算修好了。最后再做一次确认:cd 进那个 worktree 跑 git status,它应该报告在某个具名分支上、工作区干净,而不是 fatal: not a git repository

预防建议

  • 删目录或删分支之前,先用 git worktree remove <path> 移除 worktree。合并后的正确清理顺序是:git worktree removegit branch -dgit push origin --delete
  • 永远不要 rm -rf 一个 worktree 目录。如果已经删了,git worktree prune 就是善后命令。
  • 别把 worktree 直接建在 remote-tracking 分支上(原因 3)。先建本地分支:git switch -c feature/login origin/feature/login,再 git worktree add
  • 设置 git config worktree.guessRemote true,这样当有同名远端分支时,git worktree add 会自动创建本地 tracking 分支,避免留下孤立的 detached HEAD。
  • 移动仓库或 worktree 时,用 git worktree move(或事后跑一次 git worktree repair),而不要在文件管理器里拖动目录。
  • 让 worktree 易于查找:统一放在一个地方(比如 ~/worktrees/<repo>/<branch>),目录名按分支命名。
  • 定期审查:每个迭代开始时 git worktree list 看一眼,再配一个定时或 post-merge 的 git worktree prune

常见问答 (FAQ)

Q:git worktree prunegit worktree remove 有什么区别? A:prune 是对注册记录的批量清理——它删除那些目录已不存在的 worktree 的元数据,绝不碰磁盘上的文件。remove 是定点的:它删除某一个 worktree 的工作目录元数据,且在有未提交改动时会拒绝,除非你加 --force

Q:什么时候用 repair 而不是 prune A:当工作文件还在、但 Git 打不开 worktree 时用 repair——你移动过目录、移动过主仓库、或 .git 链接文件坏了。只有目录确实没了才用 prune。对一个你还需要文件的 worktree 执行 prune,会把它变成孤儿。

Q:分支删了,但 worktree 里有未提交改动,还能救吗? A:能。先别 prune、也别强制 remove。在 worktree 里运行 git -C ../project-feature stash(stash 会进到主仓库的 stash 列表,之后用 git stash apply 恢复),或者 git -C ../project-feature switch -c rescue/from-worktree 把 commit 和改动钉到一个新分支上,再去清理。

Q:git worktree addfatal: 'feature/login' is already checked out at '<path>',为什么? A:Git 强制每个分支在所有 worktree 里只能被 checkout 一次,以防工作区分叉。要么 cd 到那个已有的 worktree 直接用它;要么——如果那个 worktree 已经失效——git worktree remove --force <path>(目录没了就用 git worktree prune),然后再 add。

Q:一个仓库最多能开多少个 worktree? A:没有硬性上限。每个 worktree 自带独立的 index、HEAD 和完整 checkout,所以占用的磁盘大致和你的工作区相当。由于同一个分支只能被 checkout 一次,实际上限就是你的分支数量;对多数仓库来说,同时开 3-5 个 worktree 是舒服的。

相关阅读

标签: #git #version-control #排查