Git | WorkTree(工作树)

0 阅读24分钟

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 命令”找到自己的 HEADindex 等专属管理数据;
  • ③ 中的 gitdir 让共同 Git 目录反向找到实际工作树,供 listmoveprunerepair 等管理操作使用;
  • <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 listgit 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

关键规则:

  1. ✅ 一个分支可以不被任何工作树占用
  2. ❌ 一个分支不能同时被多个工作树占用
  3. ✅ 游离状态(detached HEAD)不占用分支,可以有多个工作树都处于游离状态
  4. ✅ 工作树与分支不是永久绑定:任一工作树都可以切换到任何未被其他工作树占用的分支

如果两棵工作树当前分别占用 mainfeature-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-reviewHEAD。因为显式使用了 --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

关键点:

  1. 不需要 cd 到工作树 B
  2. 即使 feature-a 被工作树 B 占用,也不影响读取它的提交
  3. 合并发生在当前所在的分支

错误示范:

# ❌ 错误:在 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 关键要点

记住这些:

  1. 工作树共享对象库和分支引用,但有独立的 HEAD 和暂存区
  2. 默认情况下,同一本地分支同时只能被一个工作树占用
  3. 删除工作树不会删除分支,需要手动删除
  4. 主工作树不能被删除
  5. Worktree 是本地概念,不会推送到远程
  6. 未提交的改动不会跟随新工作树
  7. 合并的是分支,不是工作树

最佳实践:

  1. 为工作树使用统一的命名前缀(如 wt-
  2. 定期清理不再使用的工作树
  3. 使用 git worktree list 检查当前状态
  4. 审查代码用 --detach,开发用 -b
  5. 先 commit 或 stash,再创建新工作树

9.5 一句话总结

用目录隔离工作,用仓库共享历史。

Worktree 让你可以在同一个仓库的多个独立目录中并行工作,每个目录都有自己的 HEAD 和工作状态(通常指向不同分支,也可以 detached),但共享提交对象和大部分仓库数据,既节省空间又提高效率。


参考资源