目录
一、从一个熟悉的打断说起
你在 feature/payment 上改了七八个文件,改到一半,测试还没跑。
然后群里来了条消息:线上有个 bug,赶紧看下。
于是你:
git stash # 先把半成品藏起来
git checkout hotfix/xxx # 切分支(还要等文件重建)
# ... 修 bug、提交、推送 ...
git checkout feature/payment
git stash pop # 把半成品捞回来
麻烦在哪?不是命令多,是状态被打断了。
stash 是个临时抽屉,你往里塞东西的时候是提心吊胆的——怕 pop 的时候冲突,怕忘了 pop 就一直躺在那儿,怕 stash 太多分不清哪个是哪个。
而且这个流程还有个隐患:如果你的构建产物、IDE 索引、本地配置文件没被 git 跟踪,切分支时它们可能被覆盖或者需要重新生成。
问题的根源是:一个仓库只能有一个工作目录。
git worktree 就是来解决这个的。
二、worktree 是什么
一句话:
让同一个仓库同时拥有多个工作目录,每个目录签出不同的分支。
上面那个场景,用 worktree 是这样:
# 一次性:为 hotfix 开一个并存的工作目录
git worktree add ../proj-hotfix -b hotfix/xxx
# 去那个目录修 bug,这边 feature/payment 的改动原封不动
cd ../proj-hotfix
# ... 修完、提交、推送 ...
cd ../proj
git worktree remove ../proj-hotfix
你的 feature/payment 半成品从头到尾没被碰过。 不用 stash,不用切分支,不用等。
proj/ ← 工作目录 1,签出 feature/payment
├── .git/ ← 主仓库
├── src/...
proj-hotfix/ ← 工作目录 2,签出 hotfix/xxx
├── .git ← 注意:这是个文件,不是目录
├── src/...
两个目录共享同一份 Git 历史(.git/objects)。
三、发展路径
3.1 史前时代:一个 shell 脚本
在 git worktree 出现之前,想要类似效果,得用 contrib/workdir/git-new-workdir。
它是个贡献目录里的 shell 脚本,不是 Git 官方命令。原理很粗糙:手动把新目录的 .git 里的一堆文件用符号链接指回原仓库。
能用,但很容易搞坏:
- 你在哪个平台上(能不能用 symlink)
- 删目录时会不会连带删掉原仓库的东西
- 各种边界情况全靠脚本作者自己兜
它一直待在 contrib/ 里没进主线,本身就说明官方对它的态度。
3.2 Git 2.5(2015 年):转正
2015 年 7 月发布的 Git 2.5,正式引入了 git worktree 命令。
当时它还是个实验性特性,只支持最基本的 add 和 list。但设计思路和 git-new-workdir 完全不同——不靠符号链接拼凑,而是在 Git 内部建立了正式的「多工作区」概念(下一节细讲)。
3.3 后续演进:子命令补齐
从 2.5 到现在(本文写作时本机是 Git 2.55),这套命令陆续补齐了完整的生命周期管理。
⚠️ 说明:我查不到每个子命令引入的准确版本号(各来源说法不一),所以不列具体版本。下面是在我本机 Git 2.55 上实测存在的完整命令集:
add 创建 worktree
list 列出所有 worktree
lock 锁定(防止被 prune 误删)
move 移动 worktree 位置
prune 清理已失效的记录
remove 移除 worktree
repair 修复损坏的链接
unlock 解锁
从「只能 add」到「能 lock / move / repair」,这个演进本身就说明它从实验特性长成了日常工具。
3.4 现在
到今天,worktree 已经是 Git 的常规功能,只是在日常开发里被低估了——很多人知道它存在,但没意识到它能替代掉 stash 这个高频操作。
四、底层原理
这一节是本文的重点。下面所有结构都是我在本机实测出来的,不是从文档抄的。
4.1 先看一个实验
mkdir demo && cd demo
git init
echo hello > a.txt && git add . && git commit -m "first commit"
git worktree add ../demo-feature -b feature
现在看两个目录的 .git:
主仓库 demo/.git/ → 一个【目录】(和你熟悉的一样)
worktree demo-feature/.git → 一个【普通文件】!
那个文件里只有一行:
gitdir: /path/to/demo/.git/worktrees/demo-feature
这是理解 worktree 的第一把钥匙:worktree 里的 .git 不是仓库,只是一张「我住在哪」的便条。
Git 一开始是在当前目录找 .git。找到目录就用;找到文件就顺着里面那行路径去找真正的元数据。
4.2 真正的元数据在哪
主仓库的 .git/ 下多了一个 worktrees/ 目录:
demo/.git/worktrees/demo-feature/
├── HEAD ← 这个 worktree 当前在哪个分支(内容: ref: refs/heads/feature)
├── ORIG_HEAD
├── commondir ← 指回共享部分的路径(内容: ../..)
├── gitdir ← 指回 worktree 自己的 .git 文件
├── index ← 独立的暂存区
├── logs/
└── refs/
注意这里没有 objects/。 这是第二把钥匙,也是整个设计的关键。
4.3 什么共享,什么独立
这是最需要记清楚的一张表:
| 组件 | 在哪 | 共享还是独立 | 原因 |
|---|---|---|---|
对象库 objects/ | 主仓库 .git/objects | 共享 | 历史数据只存一份 |
引用 refs/ | 主仓库 .git/refs | 共享 | 分支是全仓库概念,不该每个工作区一份 |
配置 config | 主仓库 .git/config | 共享 | remote、user 等配置是全局的 |
| HEAD | .git/worktrees/<n>/HEAD | 独立 | 各自的当前分支不同 |
暂存区 index | .git/worktrees/<n>/index | 独立 | 各自暂存各自改的文件 |
| 工作目录文件 | worktree 目录 | 独立 | 这就是目的本身 |
一句话概括:
共享「仓库是什么」,独立「你正在干什么」。
历史、分支、配置这些「仓库的定义」是全仓库唯一的;而你当前在哪个分支、暂存了什么、工作区里有什么文件,是每个工作区自己的事。
4.4 为什么几乎不占额外空间
因为对象库是共享的。
我用一个真实的小仓库对比过:
worktree 的额外占用 ≈ 0(只多一个几十字节的 .git 文件 + 一个 index)
clone 一份的 .git = 71K(虽然对象库本体只有 7K)
(数字都很小是因为仓库小,但比例关系成立。)
worktree 不复制历史。 它只是多开了一个「视图」,指向同一份数据。这也是为什么开 worktree 是秒级操作——没有东西需要拷贝。
💡 顺便说:
git clone即使加了--shared也还是有独立的工作目录和一堆配置,而 worktree 是原生的共享机制。另外,因为对象库共享,你在任一 worktree 里
git fetch拿到的对象,其他 worktree 立刻就能用。
4.5 那条限制的由来
第一次用 worktree 的人基本都会撞到这条:
$ git checkout master
fatal: 'master' is already used by worktree at '/path/to/demo'
同一个分支不能同时签出两次。
为什么?回到上面那张表就清楚了:
分支引用 refs/heads/master → 共享的(全仓库唯一)
HEAD → 每个 worktree 独立
index → 每个 worktree 独立
如果两个 worktree 同时签出 master:
- 你在 A 里提交,
refs/heads/master前进 - B 的 HEAD 还指着
master,但工作区文件是旧的,index 也是旧的 - 下一轮操作时,Git 无法判断「B 的 index 到底对应哪个 commit」
一个共享的引用,配两套独立的 index,是自相矛盾的状态。
所以 Git 直接禁止了。这不是限制,是保护。
需要「同一个 commit 开多个工作区」怎么办? 用 detached HEAD:
git worktree add --detach ../demo-tmp HEAD
Detached HEAD 不占用任何分支名,所以可以开任意多个。(实测通过:git status 会显示 ## HEAD (no branch)。)
4.6 删除时会保护你
$ git worktree remove ../demo-feature
fatal: '../demo-feature' contains modified or untracked files, use --force to delete it
工作区有未提交改动时,remove 会拒绝执行,要你显式加 --force。
这个设计是对的——否则一个手滑就删掉没提交的工作,而且是没有 reflog 可救的那种(未提交的改动不进对象库)。
还有一个反直觉但合理的点:删掉 worktree 不会删掉分支。
移除 worktree 后:
.git/worktrees/ → 清空
feature 分支 → 还在 ✓
因为分支引用在共享的 refs/ 里,跟 worktree 的生命周期无关。
五、使用场景
5.1 被打断时切去修 bug(最常用)
第一节那个场景。核心价值不是省几条命令,而是让你不用中断手头的工作。
半成品一直待在原地,回来就能接着改。
5.2 并行跑多个 AI 编码 agent
这是最近一两年冒出来的新场景,而且我觉得是最有意思的一个。
让多个 AI agent 同时改同一个仓库,最大的问题是互相踩:A 在改 user.service.ts,B 也在改,两个进程写同一个文件,结果不可预测。
给每个 agent 开一个 worktree,各自一个分支,物理隔离。
repo/ ← agent A,分支 agent/refactor-auth
repo-feature-b/ ← agent B,分支 agent/add-tests
repo-feature-c/ ← agent C,分支 agent/fix-perf
它们共享同一份历史(省空间、省 clone 时间),但工作目录互不干扰。跑完了再挑合意的合并。
顺带一提:这篇博客本身就是在类似的工作流下写出来的——改动先落在隔离环境里验证,确认没问题再进主目录。
5.3 对比两个分支的行为
需要同时跑两个版本对比结果的时候:
git worktree add ../proj-v1 v1.0
git worktree add ../proj-v2 v2.0
# 两个目录各起各的服务、各跑各的测试,互不影响
这比来回 checkout 快得多,而且能真同时运行。
5.4 长时间构建 / 长时间测试
如果某个分支要跑两小时的构建或压测,用 worktree 把它隔离出去,主目录照常干活。
(注意:同一台机器上并行跑重活还是会抢 CPU 和 IO,worktree 解决的是工作区冲突,不是资源竞争。)
5.5 代码评审
要 review 同事的 PR,但又不想污染自己的分支:
git worktree add ../proj-review origin/feature/xxx
cd ../proj-review
# 随便看、随便跑,看完 remove 掉
5.6 处理紧急发布
hotfix 分支需要基于线上版本而不是当前开发进度:
git worktree add ../proj-release -b hotfix/v1.2.1 v1.2.0
这个新目录直接基于 tag v1.2.0 开始,跟你当前开发到哪一步完全无关。
六、命令速查
# 创建:新分支
git worktree add ../path -b new-branch
# 创建:签出已存在的分支
git worktree add ../path existing-branch
# 创建:基于某个 tag / commit(自动 detached)
git worktree add ../path v1.2.0
# 创建:明确 detached
git worktree add --detach ../path HEAD
# 查看所有工作区(带分支和当前 commit)
git worktree list
git worktree list --porcelain # 机器可读格式
# 移除(有未提交改动会拒绝)
git worktree remove ../path
git worktree remove --force ../path
# 移动位置
git worktree move ../old ../new
# 锁定 / 解锁
git worktree lock ../path --reason "在跑长时间测试"
git worktree unlock ../path
# 清理失效记录
# 场景:直接 rm -rf 删了目录,或目录被 U 盘/网络盘搞丢了
git worktree prune
git worktree prune -n # 只看会删什么,不实际删
# 修复链接
# 场景:整个仓库目录被移动/重命名了,链接全断
git worktree repair
6.1 几个实用参数
| 参数 | 作用 |
|---|---|
--reason "..." | 搭配 lock,说明为什么锁定 |
--expire <time> | 搭配 prune,只清理超过指定时间的记录 |
-f / --force | 强制创建(目录已存在)或强制删除 |
七、常见误区
误区一:以为 worktree 会复制一份仓库。
这是最大的误解,也是很多人不敢用的原因(怕占空间)。
实际上对象库是共享的,额外占用几乎为零。真正被复制的是「工作目录里的文件」——但那是你本来就要有的东西。
误区二:以为 worktree 是「另一个 clone」。
两者差别很大:
| worktree | 独立 clone | |
|---|---|---|
| 对象库 | 共享 | 各自一份 |
| 分支 | 全局可见,可直接切 | 需要 fetch 同步 |
| 创建速度 | 秒级 | 取决于仓库大小 |
| 网络 | 不需要 | 本地 clone 也可能慢 |
误区三:直接 rm -rf 删 worktree 目录。
可以,但要在主仓库补一条 git worktree prune,否则 .git/worktrees/ 里会留下失效记录,git worktree list 会显示幽灵条目。
正确做法是用 git worktree remove。
误区四:想在两个 worktree 里签出同一个分支。
做不到,也不该做(4.5 节讲了原因)。需要的话用 --detach。
误区五:把 worktree 目录建在仓库内部。
git worktree add ./subdir/main # ❌ 别这样
会嵌套在主工作区里,git status 会看到一堆莫名其妙的文件。
建在外面(../proj-hotfix),或者加进 .gitignore。
误区六:以为 worktree 能解决环境依赖冲突。
它隔离的是工作区文件,不是运行环境。
两个 worktree 跑 npm install 会各自装一份 node_modules(如果目录都在本地);两个服务监听同一个端口照样冲突。端口、依赖、构建缓存这些该配还得配。
误区七:觉得有了 worktree 就不需要 stash 了。
不是替代关系。worktree 解决的是「多个分支并行」,stash 解决的是「同一个工作区里临时存一下」。单个分支内部的临时存取,stash 还是更顺手。
八、自检清单
- worktree 目录里的
.git是文件还是目录?里面是什么内容? - 哪些东西是共享的?哪些是每个 worktree 独立的?
- 为什么 worktree 几乎不占额外空间?
- 为什么同一个分支不能同时签出两次?
- 想在两个 worktree 里看同一个 commit,怎么办?
- 删掉 worktree 会删掉分支吗?
-
remove在什么情况下会拒绝执行? - 直接
rm -rf删了 worktree 目录,怎么收拾残局?
附:延伸阅读
相关资料
| 资源 | 说明 |
|---|---|
| git-worktree 官方文档 | 命令全集与参数,写得很清楚 |
| Git 2.5 发布说明(GitHub Blog) | worktree 首次亮相那版的官方介绍 |
| git-worktree 初版介绍(2015) | 刚引入时的用法与设计意图 |
| Pro Git 中文版 | 想补 Git 对象模型的话,第 10 章「Git 内部原理」是理解本文第四节的基础 |
关联笔记
- 把 5 万行 Java 迁移到 Go —— 大规模重构的工程实践,worktree 能帮上忙的那类场景
- 系统扩展:从 0 到 1000 万用户 —— worktree 是本地并行,那篇讲的是线上并行,隔离思路可以对照看
最后更新:2026-10-11