Git Worktree:同一份仓库,同时签出多个分支

目录


一、从一个熟悉的打断说起

你在 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 内部原理」是理解本文第四节的基础

关联笔记


最后更新:2026-10-11