Git WorkTree(工作树):一个仓库,多个工作区
前置阅读:建议先了解
.git内部结构(objects / refs / HEAD / index),本篇会在此基础上把 worktree 的原理讲透。
一句话介绍
git worktree 让同一个仓库同时拥有多个独立的工作目录。每个目录可以切换到不同分支或处于游离状态(detached HEAD),它们互不干扰地工作,但共享同一套提交对象和大部分仓库数据。
类比理解: 分支是「人设」,worktree 是「房间」。
传统方式:一间房,演员要换人设得先卸妆(stash)
┌─────────────────┐
│ 主工作目录 │
│ 当前:main │ ← 要改 feature-a?先 stash,再 switch
└─────────────────┘
Worktree 方式:多间房,每间房一个人设,同时开工
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 主工作目录 │ │ 工作树 A │ │ 工作树 B │
│ 当前:main │ │ feature-a │ │ hotfix │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
└────────────────┴────────────────┘
共享同一个 .git 对象库
1. 基础用法:常用命令速查
1.1 查看工作树
git worktree list
输出示例:
/path/to/project c786813 [main]
/path/to/wt-feature-a a1b2c3d [feature-a]
/path/to/wt-hotfix d4e5f6a [hotfix]
1.2 创建工作树的三种方式
方式一:创建新分支并关联到新工作树
git worktree add -b feature-a ../wt-feature-a main
# │ └────┬────┘ └──┬──┘ └─┬─┘
# │ 新分支名 路径 起点分支
# └─ 创建新分支的标志
图示:
执行前:
main ────●────●────● (主工作树在此)
执行后:
main ────●────●────● (主工作树)
╲
╲ feature-a ─────● (新工作树 ../wt-feature-a)
方式二:创建工作树并使用已有分支
git worktree add ../wt-existing feature-a
# 前提:feature-a 没有被其他工作树占用
方式三:游离 HEAD(不关联分支)
git worktree add --detach ../wt-review origin/feature-a
# 适合临时查看、构建和测试;也可以提交,但需随后创建分支或标签保存成果
图示:
origin/feature-a ────●────●────●
↑
工作树指向这个提交(游离状态)
不占用任何本地分支
1.3 管理工作树
# 移动/重命名工作树
git worktree move ../wt-a ../wt-a-new
# 删除工作树(需要工作区干净)
git worktree remove ../wt-a-new
# 强制删除(即使有未提交改动)
git worktree remove --force ../wt-a-new
# 清理手动删除的工作树记录
git worktree prune
1.4 核心规则
这里的“切换”要分清两件事:
- 切换到分支:指某个工作树的
HEAD改为指向某个分支,通常通过git switch完成; - 切换工作树:指通过
cd改变当前所在的工作目录。
因此,git worktree add ../wt-existing feature-a 是创建一个新的工作树并让它使用 feature-a,不是把当前工作树切换走。
checkout怎么翻译? 它不是固定等于“切换”或“占用”。在checkout/switch <分支>这种动作语境中,本文译为“切换到分支”;在branch is checked out at <路径>这种状态语境中,译为“该分支正被某个工作树使用(占用)”;对文件执行 checkout 时,则更接近“取出/恢复文件”。
⚠️ 默认情况下,同一个本地分支同一时刻只能被一个工作树占用
✓ 允许:
主工作树 → main
工作树 A → feature-a
工作树 B → feature-b
✗ 不允许:
主工作树 → main
工作树 A → feature-a
工作树 B → feature-a ← 报错:'feature-a' is already checked out
2. 原理深入:Worktree 的文件结构
2.1 普通仓库 vs Worktree 仓库
普通仓库: 工作目录与 .git 目录 1:1 绑定
project/
├── .git/ ← 真实目录
│ ├── HEAD
│ ├── index
│ ├── objects/
│ ├── refs/
│ └── config
├── src/
└── README.md
引入 Worktree 后: 主工作树保持不变,附加工作树使用特殊结构
主工作树 project/
├── .git/ ← 共同 Git 目录(共享数据的中枢)
│ ├── HEAD ← 主工作树的 HEAD
│ ├── index ← 主工作树的暂存区
│ ├── objects/ ← 共享:所有提交对象
│ ├── refs/ ← 共享:所有分支引用
│ ├── config ← 共享:仓库配置
│ └── worktrees/ ← 新增:管理所有附加工作树
│ ├── wt-feature-a/
│ │ ├── HEAD ← wt-feature-a 的 HEAD
│ │ ├── index ← wt-feature-a 的暂存区
│ │ ├── gitdir ← 指回 wt-feature-a/.git 文件
│ │ └── commondir ← 指向 ../..(公共目录)
│ └── wt-hotfix/
│ ├── HEAD
│ ├── index
│ ├── gitdir
│ └── commondir
├── src/
└── README.md
附加工作树 wt-feature-a/
├── .git ← 文件(不是目录!)
│ 内容:gitdir: /path/to/project/.git/worktrees/wt-feature-a
├── src/
└── README.md
附加工作树 wt-hotfix/
├── .git ← 文件(不是目录!)
│ 内容:gitdir: /path/to/project/.git/worktrees/wt-hotfix
├── src/
└── README.md
2.2 核心机制:通过文件重定向实现共享
当你在附加工作树执行 Git 命令时:
1. Git 读取 wt-feature-a/.git 文件
↓
2. 发现它指向 project/.git/worktrees/wt-feature-a/
↓
3. 读取该目录中的 commondir 文件
↓
4. 找到真正的公共 Git 目录 project/.git/
↓
5. 使用公共的 objects/ 和 refs/,但使用私有的 HEAD 和 index
图示:
执行命令:cd wt-feature-a && git status
wt-feature-a/
└── .git (文件) ─────┐
│ 重定向
▼
project/.git/worktrees/wt-feature-a/
├── HEAD ────────────┐ 读取私有状态
├── index ───────────┤
└── commondir ───────┼──┐
│ │ 指向公共目录
┌────────────┘ │
│ │
▼ ▼
project/.git/
├── objects/ ◄────── 共享对象库
└── refs/ ◄────── 共享分支引用
2.3 共享与隔离:一目了然
| 内容 | 存放位置 | 是否共享 | 说明 |
|---|---|---|---|
| 提交对象 | .git/objects/ | ✅ 共享 | 所有工作树看到相同的提交历史 |
| 普通本地分支/标签引用 | .git/refs/ 或 packed-refs | ✅ 通常共享 | 在任意工作树创建/删除后,其他工作树通常可见 |
| 仓库配置 | .git/config | ✅ 默认共享 | 远程仓库配置等;可用 extensions.worktreeConfig 拆分部分配置 |
| 当前分支/提交 | 主工作树为 .git/HEAD;附加工作树为 .git/worktrees/<id>/HEAD | ❌ 各自独立 | 每个工作树可以在不同分支上或处于 detached HEAD |
| 暂存区 | 主工作树为 .git/index;附加工作树为 .git/worktrees/<id>/index | ❌ 各自独立 | git add 的内容互不影响 |
| 工作区文件 | 各工作树目录 | ❌ 各自独立 | 未提交的修改只存在于当前工作树 |
关键理解:
- ✅ 共享提交历史:工作树 A 创建的提交对象会进入同一个仓库,工作树 B 可以通过
git log等命令看到它 - ✅ 共享分支引用:在工作树 A 创建分支,工作树 B 的
git branch通常立即能列出 - ⚠️ 文件不会自动同步:B 的工作目录不会因为 A 提交就自动变成该提交的内容;B 仍需切换/合并/更新到相应提交
- ❌ 不共享工作状态:工作树 A 的
git add和文件修改,工作树 B 完全看不到
2.4 工作树改名与修复
正确方式:使用 Git 命令
git worktree move ../wt-feature-a ../wt-feature-new
一个附加工作树可以看成三个部分:
① 磁盘上的工作树目录
../wt-feature-a/
② 工作树里的 .git 文件
内容:gitdir: /path/to/project/.git/worktrees/<id>
作用:① ──▶ ③
③ 共同 Git 目录中的工作树专属管理目录
project/.git/worktrees/<id>/
└── gitdir
内容:/path/to/wt-feature-a/.git
作用:③ ──▶ ①
这里的 project/.git/ 是仓库的共同 Git 目录;③ 只是其中一棵附加工作树的专属管理记录,并不等于“主仓库本身”。管理目录里的 gitdir 是一个普通文本文件,内容是该工作树根目录下 .git 文件的路径。
更准确的模型是:两个路径指针 + 一个内部 ID。
- ② 让“从附加工作树执行的 Git 命令”找到自己的
HEAD、index等专属管理数据; - ③ 中的
gitdir让共同 Git 目录反向找到实际工作树,供list、move、prune、repair等管理操作使用; <id>就是.git/worktrees/下的目录名。它通常在创建时由工作树路径的末段生成,重名时会追加数字,之后即使移动工作树也保持不变。
所以双向路径不是专门为“改名”设计的;改名和修复只是这套双向定位机制发挥作用的场景。Git 要求路径关系正确,但不要求工作树目录名和 <id> 同名。
执行 git worktree move 后:
工作树目录:
wt-feature-a/ → wt-feature-new/ ✅ 更新
工作树里的 .git 文件:
gitdir: .../.git/worktrees/wt-feature-a ✅ ID 不变
管理目录:
.git/worktrees/wt-feature-a/ ✅ ID 不改名
管理目录中的 gitdir:
.../wt-feature-a/.git → .../wt-feature-new/.git ✅ 路径更新
因此,<id> 不跟着目录改名是正常的。它是创建附加工作树时生成的内部标识,不是工作树目录名的镜像;只要两条路径仍然互相指向,git worktree list 和 git status 就能正常工作。这个 ID 没有面向用户的重命名命令。
不要直接用 mv 代替 git worktree move:
mv ../wt-feature-a ../wt-feature-new
手动移动后,主仓库中的 gitdir 仍可能指向旧路径。此时新目录通常暂时还能执行 Git 命令,但 git worktree list 可能将其标记为 prunable。如果这时先执行 git worktree prune,Git 可能删除对应的管理记录;之后再执行 repair 就可能无法恢复这棵工作树。
正确做法是先修复,再清理:
git worktree repair ../wt-feature-new
repair 会根据新路径重新建立管理目录与工作树之间的连接。若管理记录已经被 prune 删除,通常只能重新创建工作树:
git worktree add ../wt-feature-new <分支或提交>
分支和已经提交的对象仍在共享仓库中,但原工作树独立的 HEAD、暂存区以及未提交修改可能无法恢复。因此可以记住:
优先使用:git worktree move <旧路径> <新路径>
手动 mv 后:立即执行 git worktree repair <新路径>
不要手动改名:.git/worktrees/<id> 目录
特殊情况:
- 主工作树不能用
git worktree move移动(它是仓库的「家」) - 包含 submodule 的附加工作树也不支持此命令
3. 分支与工作树:占用关系详解
3.1 概念澄清:工作树不等于分支
❌ 错误理解:创建工作树 = 创建分支
✓ 正确理解:工作树是「房间」,分支是「人设」
一个工作树可以:
- 切换到某个本地分支 (HEAD → refs/heads/feature-a)
- 处于游离状态(detached HEAD) (HEAD → 4a3f2e1)
- 处于未诞生状态(unborn branch) (空仓库的初始状态)
3.2 占用关系图示
仓库中的分支列表:
main ──────────────────┐
feature-a ─────────────┤
feature-b ─────────────┤
feature-c ─────────────┤
hotfix ────────────────┘
工作树占用情况:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 主工作树 │ │ 工作树 A │ │ 工作树 B │
│ 占用:main │ │ 占用:feature-a│ │ 游离状态 │
└─────────────┘ └─────────────┘ └─────────────┘
未被占用:feature-b, feature-c, hotfix
关键规则:
- ✅ 一个分支可以不被任何工作树占用
- ❌ 一个分支不能同时被多个工作树占用
- ✅ 游离状态(detached HEAD)不占用分支,可以有多个工作树都处于游离状态
- ✅ 工作树与分支不是永久绑定:任一工作树都可以切换到任何未被其他工作树占用的分支
如果两棵工作树当前分别占用 main 和 feature-a,不能直接一步互换,因为双方的目标分支都还被占用。可以先让其中一棵工作树进入游离状态,释放它的分支,再依次切换:
# 假设 project/ 在 main,wt-feature-a/ 在 feature-a
cd ../wt-feature-a && git switch --detach HEAD # 先释放 feature-a
cd ../project && git switch feature-a
cd ../wt-feature-a && git switch main
执行前应先提交或 stash 改动;否则 switch 可能因工作区修改不兼容而失败。分支数量多少并不改变这条规则。
3.3 完整示例:从零到多工作树
初始状态: 只有主工作树,已有分支 main 和 develop
仓库:
main ────●────●────●
develop ─●────●
工作树:
project/ (占用 main)
步骤 1: 创建 feature-a 分支和对应工作树
git worktree add -b feature-a ../wt-feature-a main
仓库:
main ────●────●────●──┐
develop ─●────● │
feature-a ────────────┘ (刚从 main 创建)
工作树:
project/ (占用 main)
wt-feature-a/ (占用 feature-a)
步骤 2: 创建游离状态工作树用于代码审查
git worktree add --detach ../wt-review develop
这里的 develop 是起点:Git 取 develop 当前指向的提交作为 wt-review 的 HEAD。因为显式使用了 --detach,新工作树只是停在该提交上,并没有占用 develop 分支;它也不是基于 main 创建的。
main ────●────●────● ← project/ 占用 main
╲
feature-a ● ← wt-feature-a/ 占用 feature-a
develop ────●────●
↑
└──────── wt-review/ 的 HEAD(detached)
指向 develop 当前提交,但不占用 develop
步骤 3: 尝试创建第二个占用 feature-a 的工作树
git worktree add ../wt-another feature-a
❌ fatal: 'feature-a' is already checked out at '/path/to/wt-feature-a'
解决方法:
方案 1:先删除 wt-feature-a
git worktree remove ../wt-feature-a
方案 2:创建新分支
git worktree add -b feature-a-copy ../wt-another feature-a
方案 3:使用游离状态
git worktree add --detach ../wt-another feature-a
4. 实战场景:解决真实开发问题
场景一:多 Feature 并行开发
传统做法的痛点:
开发流程:
在 feature-a 上开发 → 突然要改 feature-b → stash → switch → 开发 → commit
→ switch 回 feature-a → stash pop → 继续开发 → 又要看 main 的代码 → stash...
使用 Worktree:
# 初始化:主工作树在 main
git worktree add -b feature-a ../wt-feature-a main
git worktree add -b feature-b ../wt-feature-b main
工作流程图:
项目结构:
project/ ← main 分支(稳定版本,用于参考)
wt-feature-a/ ← feature-a(正在开发功能 A)
wt-feature-b/ ← feature-b(正在开发功能 B)
开发流程:
IDE 窗口 1: wt-feature-a/ → 改代码、测试、提交
IDE 窗口 2: wt-feature-b/ → 改代码、测试、提交
终端窗口: project/ → 随时查看 main 的参考代码
无需 stash,无需 switch,互不干扰!
完成后合并:
# Feature A 完成
cd ../wt-feature-a
git add .
git commit -m "feat: 完成功能 A"
# project/ 已经是 main 工作树,无需再切换工作树
cd ../project
git merge feature-a
git push
# 清理工作树和分支
git worktree remove ../wt-feature-a
git branch -d feature-a
场景二:紧急热修复(不打断当前工作)
问题场景: 正在 main 分支大改代码(未提交),突然线上出 bug
传统做法:
git stash # 保存当前修改
git switch -c hotfix # 创建热修复分支
# ... 修复 bug ...
git switch main # 切回去
git stash pop # 恢复之前的修改
使用 Worktree:
# 当前在 project/ 目录,main 分支有大量未提交修改
git fetch origin
git worktree add -b hotfix ../wt-hotfix origin/main
cd ../wt-hotfix
# 修复 bug(完全独立的工作区,不受 project/ 影响)
vim src/auth.js
git add .
git commit -m "fix: 修复登录验证问题"
git push -u origin hotfix
# 在 GitHub 创建 PR,等待合并
# 同时可以回到 project/ 继续开发,完全不冲突!
# 确认 PR 已合并,并按团队流程更新 main 后再清理
cd ../project
git worktree remove ../wt-hotfix
# 只有确认 hotfix 已合入当前 main 后,才删除本地分支
git branch -d hotfix
如果 git branch -d hotfix 报错,通常说明当前分支(例如本地 main)还没有包含 hotfix 的提交。先确认 PR 确实已经合并,再按团队流程更新本地 main,例如在合适的、干净的工作树中执行 git fetch 后合并远程 main。
只有在你明确确认不再需要该分支、只是想跳过 Git 的保护检查时,才考虑:
git branch -D hotfix # 谨慎:只是不检查合并状态,不会替你合并代码
对比图:
传统方式:
main 分支改动 → stash → 切到 hotfix → 修复 → 切回 main → stash pop
▲ │
└──────────────── 工作中断 ─────────────────────┘
Worktree 方式:
project/ main 分支改动(继续进行,不受影响)
wt-hotfix/ hotfix 分支修复(独立环境)
两者完全隔离,互不干扰!
场景三:代码审查与验证
需求: 审查同事的 PR,需要本地运行测试,但不想影响当前工作
# 获取最新的远程分支
git fetch origin
# 创建游离状态的审查工作树
git worktree add --detach ../wt-review origin/feature-xxx
cd ../wt-review
npm install
npm test
npm run build
# 审查完成后直接删除
cd ../project
git worktree remove ../wt-review
如果审查过程中需要做修改:
# 创建本地审查分支
git worktree add -b review/feature-xxx ../wt-review origin/feature-xxx
cd ../wt-review
# 修改代码、提交、推送
git add .
git commit -m "review: 修复发现的问题"
git push -u origin review/feature-xxx
场景四:跨版本测试与实验
需要同时查看不同版本时,可以为 tag 或某个提交创建游离工作树:
git worktree add --detach ../wt-v2 v2.0.0
git worktree add --detach ../wt-experiment main
前者适合跨版本测试,后者适合临时实验。实验成功时,在该目录中执行
git switch -c experiment/refactor 再提交;实验失败则直接删除工作树即可。
5. 分支合并:核心概念澄清
5.1 合并的本质:分支之间的操作
很多人误解 Worktree 与合并的关系。记住:
❌ 错误理解:把工作树 A 合并到工作树 B
✓ 正确理解:在某个工作树中,把分支 X 合并到当前分支 Y
Worktree 只是「房间」,合并的是「人设」(分支)
5.2 合并场景图解
场景: 工作树 A 在 main 分支,工作树 B 在 feature-a 分支
仓库提交历史:
main ────●────●────● (工作树 A 占用)
╲
╲
●────● feature-a (工作树 B 占用)
目标:把 feature-a 的改动合并到 main
步骤:
# 在工作树 A 中操作(当前分支是 main)
cd ../project
git merge feature-a
关键点:
- 不需要
cd到工作树 B - 即使
feature-a被工作树 B 占用,也不影响读取它的提交 - 合并发生在当前所在的分支上
错误示范:
# ❌ 错误:在 feature-a 上执行 git merge feature-a
cd ../wt-feature-a # 当前分支是 feature-a
git merge feature-a # 把 feature-a 合并进 feature-a?无意义!
5.3 多分支合并示例
场景: 需要把多个 Feature 分支合并到 main
初始状态:
main ────●────●
╲ ╲
╲ ●────● feature-a
╲
●────● feature-b
工作树分布:
project/ → main
wt-feature-a/ → feature-a
wt-feature-b/ → feature-b
操作流程:
# 在 main 分支的工作树中操作
cd ../project
# 逐个合并
git merge feature-a
git merge feature-b
# 推送到远程
git push
合并后的历史:
main ────●────●────◆────◆
╲ ╲ ↑ ↑
╲ ●────● │
╲ merge 点
●────●───────┘
5.4 反向合并(同步主分支到 Feature)
场景: main 分支有新提交,需要同步到正在开发的 feature-a
main ────●────●────●────● (有新提交)
╲
●────● feature-a (落后了)
操作:
# 在 feature-a 的工作树中操作
cd ../wt-feature-a # 当前分支是 feature-a
git fetch origin
git merge origin/main # 把远程 main 的最新提交合并进 feature-a
同步后:
main ────●────●────●────●
╲ ╲
●────●────◆ feature-a
要点: 合并的方向由当前所在分支决定,与工作树无关。若要合并本地 main,先确保它已更新,再执行 git merge main。
6. 常见误区与避坑指南
误区 1:未提交的代码会跟着新工作树走
错误认知:
# 在 main 分支修改了文件但未提交
vim src/app.js # 改了很多代码
git worktree add -b feature-a ../wt-feature-a main
cd ../wt-feature-a
cat src/app.js # ❌ 以为能看到刚才的修改
实际情况:
project/src/app.js → 有未提交的修改
wt-feature-a/src/app.js → 干净的 main 分支状态
未提交的修改只存在于当前工作树!
正确做法: 先提交或 stash,再创建工作树
误区 2:删除工作树会删除分支
错误认知:
git worktree remove ../wt-feature-a
# ❌ 以为分支 feature-a 也被删除了
实际情况:
git branch # feature-a 还在!
完整清理:
git worktree remove ../wt-feature-a # 删除工作树
git branch -d feature-a # 删除分支
误区 3:直接删除工作树目录
错误做法:
rm -rf ../wt-feature-a # ❌ 直接删除目录
后果:
git worktree list
# /path/to/wt-feature-a a1b2c3d [feature-a] ← 记录还在,但目录不存在
修复:
git worktree prune # 清理失效的记录
误区 4:混淆工作树 ID 和目录名
现象:
git worktree add ../my-feature feature-a
git worktree move ../my-feature ../my-feature-new
查看内部结构:
my-feature-new/.git
└── gitdir: /path/to/project/.git/worktrees/my-feature
(注意:ID 还是 my-feature,不是 my-feature-new)
project/.git/worktrees/
└── my-feature/ ← ID 不会因为目录改名而改变
要点: 工作树 ID(.git/worktrees/<id>)和工作树目录名可以不一致
误区 5:认为 Worktree 会被 push 到远程
错误认知:
git push # ❌ 以为会把工作树结构推送到远程
实际情况:
本地:
project/ → main
wt-feature-a/ → feature-a
wt-feature-b/ → feature-b
同事 clone 后:
project/ → main (只有一个普通工作树)
要点: Worktree 是纯本地概念,git push 只推送提交和分支引用
误区 6:尝试删除主工作树
错误操作:
git worktree remove ../project # ❌ 报错
原因: 主工作树是仓库的「家」,拥有真正的 .git 目录,不能被删除
图示:
project/.git/ ← 真实目录,是所有工作树的中枢
↑
主工作树(不能删除)
wt-feature-a/.git ← 只是一个文件(可以删除)
误区 7:在同一分支创建多个工作树
错误操作:
git worktree add ../wt-1 feature-a # ✓ 成功
git worktree add ../wt-2 feature-a # ❌ fatal: 'feature-a' is already checked out
解决方案:
方案 1:使用游离状态
git worktree add --detach ../wt-2 feature-a
方案 2:创建新分支
git worktree add -b feature-a-copy ../wt-2 feature-a
方案 3:使用 --force(危险,不推荐)
git worktree add --force ../wt-2 feature-a
为什么不推荐 --force?
wt-1/ 和 wt-2/ 同时在 feature-a 分支上
→ 两个工作树可能产生冲突的提交
→ 难以追踪哪个工作树做了什么
→ 容易导致数据混乱
7. 进阶技巧与最佳实践
7.1 工作树命名规范
推荐的命名模式:
# 已有分支:路径在前,分支名在后
git worktree add ../wt-feature-login feature/login
# 创建新分支:用 -b 指定分支,并从 main 开始
git worktree add -b hotfix/auth ../wt-hotfix-auth main
# 按用途命名
git worktree add --detach ../wt-review-pr-123 origin/pr-123
git worktree add --detach ../wt-test-v2.0 v2.0.0
# 统一前缀便于管理
git worktree add ../wt-dev-feature-a feature-a
git worktree add ../wt-review-feature-b feature-b
上面最后两条命令的前提是对应分支尚未被其他工作树占用。若 feature-a 已被占用,第一条命令会直接失败,不会自动创建游离工作树;需要游离状态时必须显式写 --detach。
目录结构示例:
~/projects/
├── myproject/ ← 主工作树(main 分支)
├── wt-feature-login/ ← 开发登录功能
├── wt-feature-api/ ← 开发 API 功能
├── wt-hotfix-bug-123/ ← 紧急修复
└── wt-review-pr-456/ ← 代码审查
7.2 日常管理与工具配合
项目结束时,建议先查看再逐个清理,不要把批量删除脚本当作默认操作:
git worktree list
git worktree remove ../wt-feature-a
git worktree remove ../wt-feature-b
git worktree prune # 清理手动删除目录后留下的记录
在 IDE 中,直接把不同工作树分别打开即可;例如 VS Code:
code project/
code wt-feature-a/
code wt-feature-b/
每个窗口对应一个独立工作目录。若使用 tmux,也可以让每个窗口进入一个工作树,但这属于终端管理方式,并不是 Worktree 的必要配置。
GitHub Desktop 提示: 在支持 Worktree 的版本中,如果你在分支列表里选择了一个已被另一棵工作树占用的分支,Desktop 可能直接把当前仓库视图切到那棵工作树,而不是让当前工作树再次占用该分支。因此界面上看起来像“成功切换了已占用分支”,实际目录并没有重复占用;可用
git worktree list核实。
8. Worktree 的价值与适用场景
8.1 核心价值
Worktree 解决的核心问题:
传统开发:一个目录 ↔ 一个分支
问题:
- 频繁切换分支导致上下文丢失
- 需要反复 stash/unstash
- 无法同时运行多个版本
- 代码审查需要暂停当前工作
Worktree 方案:一个仓库 ↔ 多个独立目录
优势:
- 每个任务独立目录,互不干扰
- 无需 stash,上下文完整保留
- 可以同时运行多个版本
- 共享提交历史,节省磁盘空间
8.2 适用场景总结
| 场景 | 传统方式痛点 | Worktree 解决方案 |
|---|---|---|
| 多 Feature 并行开发 | 频繁切换、stash 混乱 | 每个 Feature 独立目录 |
| 紧急热修复 | 必须中断当前工作 | 新建工作树,不影响开发 |
| 代码审查 | 需要保存当前状态 | 游离工作树,审查完即删 |
| 跨版本测试 | 反复切换 tag | 多个工作树同时运行不同版本 |
| 实验性改动 | 担心污染主分支 | 游离工作树,失败直接丢弃 |
| CI/CD 自动化 | 需要多个 clone | 一个仓库多个工作树,节省空间和时间 |
8.3 与其他方案对比
| 方案 | 优点 | 局限 |
|---|---|---|
stash | 适合短暂保存修改、快速切换 | 不能同时保留两个工作现场,频繁使用容易混乱 |
| 多个 Clone | 配置、权限和运行环境隔离更彻底 | 对象库通常重复,占用更多空间,需要分别管理 |
| Worktree | 共享对象库和提交历史,每个目录有独立工作状态 | 工作区文件、依赖和构建产物仍各自占空间;仓库配置默认共享 |
多个 Clone:
clone-1/.git/objects/ ─┐
clone-2/.git/objects/ ─┼─ 各自一份对象库
clone-3/.git/objects/ ─┘
Worktree:
project/.git/objects/ ← 共享
wt-a/.git(文件)──────┘
wt-b/.git(文件)──────┘
Worktree 共享的是仓库层面的对象和引用,不代表各工作树的文件会自动同步;每个工作树仍要单独安装依赖、生成构建产物,并注意端口或运行时配置冲突。
8.4 不适用的场景
❌ 单个开发者的小型项目: 分支切换频率低,Worktree 收益不大
❌ 需要完全独立配置的环境: 配置是共享的(除非使用 extensions.worktreeConfig)
❌ 团队成员不熟悉 Git: 增加学习成本
✓ 推荐使用 Worktree 的场景:
- 多功能并行开发
- 需要频繁切换上下文
- 需要同时测试多个版本
- CI/CD 流水线需要隔离环境
- 为 AI 编程工具提供独立的工作空间
9. 总结与速查表
9.1 核心概念
分支(Branch) = 指向提交的引用(「人设」)
工作树(Worktree) = 独立的工作目录(「房间」)
关系:
一个仓库 → 多个分支 + 多个工作树
一个工作树 → 当前使用一个分支,或处于游离状态
一个本地分支 → 默认同时只被一个工作树占用
游离状态 → 直接指向提交,不占用本地分支
9.2 命令速查表
| 命令 | 说明 |
|---|---|
git worktree list | 列出所有工作树 |
git worktree add -b <分支> <路径> <起点> | 创建新分支和工作树 |
git worktree add <路径> <已有分支> | 创建工作树并使用已有分支 |
git worktree add --detach <路径> <提交> | 创建游离状态工作树 |
git worktree move <旧路径> <新路径> | 移动/重命名工作树 |
git worktree remove <路径> | 删除工作树(不删除分支) |
git worktree remove --force <路径> | 强制删除(即使有未提交改动) |
git worktree prune | 清理失效的工作树记录 |
git worktree repair <路径> | 修复手动移动后的工作树连接 |
git branch -d <分支> | 删除分支(配合 remove 使用) |
9.3 典型工作流
完整的 Feature 开发流程:
# 1. 创建 Feature 工作树
git worktree add -b feature/login ../wt-login main
# 2. 在工作树中开发
cd ../wt-login
vim src/auth.js
git add .
git commit -m "feat: 添加登录功能"
# 3. 推送到远程
git push -u origin feature/login
# 4. 回到 main 工作树合并
cd ../project
git merge feature/login
git push
# 5. 清理
git worktree remove ../wt-login
git branch -d feature/login
git push origin --delete feature/login
9.4 关键要点
✅ 记住这些:
- 工作树共享对象库和分支引用,但有独立的 HEAD 和暂存区
- 默认情况下,同一本地分支同时只能被一个工作树占用
- 删除工作树不会删除分支,需要手动删除
- 主工作树不能被删除
- Worktree 是本地概念,不会推送到远程
- 未提交的改动不会跟随新工作树
- 合并的是分支,不是工作树
✅ 最佳实践:
- 为工作树使用统一的命名前缀(如
wt-) - 定期清理不再使用的工作树
- 使用
git worktree list检查当前状态 - 审查代码用
--detach,开发用-b - 先 commit 或 stash,再创建新工作树
9.5 一句话总结
用目录隔离工作,用仓库共享历史。
Worktree 让你可以在同一个仓库的多个独立目录中并行工作,每个目录都有自己的 HEAD 和工作状态(通常指向不同分支,也可以 detached),但共享提交对象和大部分仓库数据,既节省空间又提高效率。