🚀 省流助手
- 现象 :想给项目开个隔离副本做重构,跑了
git worktree add .worktrees/xxx -b refactor/yyy,后悔后rm -rf干掉,发现git worktree list还残留、新分支还在、.gitignore还脏 - 根因 :worktree 元数据存在主仓
.git/worktrees/里,手删工作目录 git 不会自动同步;-b创建的分支也是独立产物,不会跟着 worktree 一起销毁 - 解决 :worktree 路径优先放在仓库同级目录 省心;意外搞砸了用
git worktree prune -v+git branch -D <name>一把清干净
故障现象
需求:给一个 monorepo 开个隔离工作区,做两期重构(替换两个子包的 core 依赖),不想动主分支。
第一反应敲:
bash
cd ~/projects/your-app
git worktree add .worktrees/refactor-core-swap -b feat/topic-x
很快后悔------.worktrees/ 在仓库里,git status 看到 untracked,要不要加 .gitignore?要不要 commit 一个 chore?感觉脏,干脆 rm -rf .worktrees,把 .gitignore 改的也撤掉。
理论上应该回到原状,但是:
bash
$ git worktree list
/Users/<user>/projects/your-app abc1234 [dev]
$ git --no-pager branch
feat/feature-x
main
* dev
feat/topic-x ← 这玩意还在
wip/scratch
release
$ git status
On branch dev ← 怎么还显示脏(提示符里有 !)
看起来像是删了,但又没完全删------这种"半残留"是最难受的。
排查路径
假设 1:rm -rf 把目录干掉应该就够了?
直觉是这样,验证:
bash
ls .worktrees # 不存在
git worktree list # 仍可能列出已删 worktree(标 prunable)
不同 git 版本对此行为不一致------有的会保留 prunable 条目,有的 list 时自动检测。但元数据目录 .git/worktrees/<name>/ 大概率还在,这才是关键残留。
假设 2:-b 顺带创建的分支会跟 worktree 一起死吗?
不会。git worktree add <path> -b <branch> 本质是两步合一:
git branch <branch>在主仓里建分支git worktree add <path> <branch>把它 checkout 到指定路径
删 worktree 只撤回第二步,分支照样躺在主仓。
bash
git --no-pager branch
# 输出里仍能看到 feat/topic-x
假设 3:.gitignore 那个改动没 commit,怎么撤?
查 git status 看修改范围,未 commit 的话直接:
bash
git restore .gitignore
回到 HEAD。
假设 4:worktree 命令必须在主仓跑吗?
我一度以为是。验证发现并不是------git worktree 所有子命令读的是共享元数据 ,主仓和任何 linked worktree 内执行效果一致。唯一例外:你不能 remove 自己当前所在的那个 worktree。
关键证据
清理前 git worktree list 的输出(关键 turning point):
javascript/Users/<user>/projects/your-app abc1234 [dev] /Users/<user>/projects/your-app/.worktrees/refactor-core-swap abc1234 [feat/topic-x]
rm -rf .worktrees 之后再跑 git worktree list,第二行消失 或标记为 prunable ------表面看起来"清"了,但 .git/worktrees/refactor-core-swap/ 元数据目录还在硬盘上。这就是所有困惑的源头:git 的 worktree 状态不是看磁盘,是看主仓元数据。
根因
可以一句话总结:
worktree 是 main 仓库
.git/worktrees/<name>/元数据 + 一个 working tree 文件夹的组合体。手删后者 git 不会自动清前者。-b顺带建的分支是独立产物,跟 worktree 没有隶属关系。
展开三点:
1. linked worktree 的 .git 是文件不是目录
进 worktree 看:
bash
$ cat .worktrees/xxx/.git
gitdir: /Users/<user>/projects/your-app/.git/worktrees/xxx
这就是为什么所有 worktree "共享"主仓数据------它们的 git 目录都指回主仓那一份。
2. 元数据残留是设计而非 bug
git 故意不在 rm 时联动清理,因为它无法区分"用户是想删除"还是"目录被外部进程暂时移走"(如挂载点暂时不可达)。所以清理需要显式指令:prune。
3. -b 是糖语法
git worktree add <path> -b <branch> ≡
bash
git branch <branch>
git worktree add <path> <branch>
删 worktree 只逆掉后一步。前一步建的分支仍在 main 仓的 refs/heads/ 下,得 git branch -D 显式清。
解决方案
临时救火(已经搞砸了)
三条命令清干净:
bash
cd ~/projects/your-app # 主仓也行,在哪个 worktree 里都行
git worktree prune -v # 清掉已删目录的元数据
git branch -D feat/topic-x # 清掉 -b 顺带创的分支
git restore .gitignore # 撤销未 commit 的 .gitignore 改动
# 验证回到原状
git --no-pager worktree list # 应只剩主仓一行
git --no-pager branch # 不应再有那个分支
git status # nothing to commit, working tree clean
如果 .gitignore 已 commit 但还没 push:
bash
git reset --hard HEAD~1 # 撤回那一笔
永久方案(一开始就避免)
worktree 放在仓库同级目录 ,命名 <repo>-<branch-slug>:
bash
cd ~/projects/your-app
git worktree add ../your-app-feat-topic-x -b feat/topic-x
cd ../your-app-feat-topic-x
pnpm install # 装依赖(pnpm workspace 在该 worktree 根执行)
好处:
- 主仓
git status看不见它 - 不需要改
.gitignore,没有 chore commit 干扰提交历史 - IDE 文件树里同级展示,"切窗口"就是切上下文
- 删除时也省心:
git worktree remove ../your-app-feat-topic-x自动清元数据
预防建议
写成清单贴到 ~/.claude/CLAUDE.md 或团队 README,新人不再踩:
| 场景 | 该怎么做 |
|---|---|
| 想隔离工作区 | 路径首选仓库同级目录 ,命名 <repo>-<branch> |
| 必须放仓库内 | 先把目录加 .gitignore 并 commit,再 worktree add |
| 创建带新分支 | git worktree add <path> -b <branch>(一步到位) |
| 创建复用现有分支 | git worktree add <path> <existing-branch>(不带 -b) |
| 删除 worktree | git worktree remove <path>(别 手动 rm -rf) |
顺手用了 -b |
删 worktree 后记得 git branch -d/-D <branch> |
| 手动删了想补救 | git worktree prune -v + git branch -D <branch> |
| 不知道在哪跑 prune | 任意 worktree 内都行,不挑位置 |
知识点提炼
主 worktree vs linked worktree
- main worktree :第一次 clone 出来的目录,里面是真正的
.git/目录 - linked worktree :用
git worktree add创建的额外副本,里面是.git文件(gitfile pointer)
linked worktree 共享主仓的 object database、refs、config,不会重新 clone 一份对象,磁盘开销极小。这也是 worktree 比 clone 便宜的核心原因。
用 git rev-parse 判定自己在哪
写脚本想区分主仓 vs linked worktree?两个变量一比就知道:
bash
GIT_DIR=$(git rev-parse --git-dir) # 当前 worktree 的 git 目录
GIT_COMMON=$(git rev-parse --git-common-dir) # 主仓共享的 git 目录
# 主仓中:两者相同
# linked worktree 中:GIT_DIR 是 .git/worktrees/<name>/,GIT_COMMON 是主仓的 .git/
为什么 git add . 会对内嵌 git 仓库报警告
如果 worktree 目录在主仓内、且没 ignore,git add . 会触发:
bash
warning: adding embedded git repository: .worktrees/xxx
这个警告是说:"该目录里有 .git 文件,git 当它是嵌入式仓库(类似 submodule),会以 gitlink 形式添加而不是文件内容。"
提交进去的是一个指向某个 commit 的引用,不是文件本身。这种状态非常容易让 PR diff 看不懂,所以 worktree 一定要么 ignore 要么放仓库外。
可能你已经习惯了为每个 feature 拉新分支然后切来切去------但当你需要两个分支同时编辑 (比如对照参考实现写新代码、或一边跑测试一边修主线 bug),worktree 才是正确解法。git checkout 切换分支会污染 IDE 状态、缓存、未 push 的草稿;worktree 直接给你两份独立工作区,互不打扰。
第一次用难免踩坑,但理顺这套心智模型之后,clone-and-cd 的旧习惯就再也回不去了。