Git Worktree 入门:从并行开发到共享仓库原理

Git Worktree 可以让同一个仓库连接多个工作目录,各自保留开发现场并共享提交历史。

阅读 -- 次 参与评论

你正在一个功能分支上重构,工作区里有改到一半的文件,测试还没通过,也不适合提交。此时线上突然出现一个问题,需要马上从 main 拉出修复分支。

直接切换分支之前,必须先处理当前修改。文件一多,暂存、切换和恢复现场就会打断原来的工作节奏。

另一个办法是重新克隆仓库。两个目录可以各自开发,同时也会增加下载时间、磁盘占用和维护成本。

Git Worktree 允许同一个 Git 仓库同时连接多个工作目录,每个目录都可以检出自己的分支。

原目录继续保留重构现场,新目录从 main 开始处理热修复。两边各自保存未提交文件,原来的开发节奏也能继续保持。

示例运行环境为 macOS,Git 版本为 2.33.0。

Git Worktree 是什么

Git Worktree 是一套“一个仓库,多个工作目录”的机制。

执行 git clonegit init 时得到的原始工作目录,官方称为 main worktree,下文简称主工作区。通过 git worktree add 增加的目录称为 linked worktree,下文简称关联工作区。

每个 Worktree 都有自己的工作目录、HEAD 和暂存区,可以保留不同的检出状态和未提交修改。它们共享对象数据库和大部分引用,任何一处创建的提交与分支都能被其他 Worktree 识别。

main worktree:main ──────────────┐
linked worktree A:feature/search ├──> 共享仓库数据:对象、分支、标签、默认配置
linked worktree B:hotfix/login ──┘

每个 Worktree 分别保存自己的工作目录、HEAD 和暂存区。

可以把它理解成酒店给同一位住客配了几张房卡。每张卡打开不同房间,房间里的桌面和行李互不影响;这些房间仍登记在同一家酒店系统里。对应到 Git,共享部分是对象与引用,各目录中的文件保持独立。

Worktree 和重新克隆一份仓库的差异,可以先看这张表:

对比项Git Worktree再次 git clone
提交对象共享一份对象数据库每个克隆通常有自己的对象数据库
分支与标签大部分引用共享各仓库分别维护,需要 fetch 或 push 同步
工作目录与暂存区每个 Worktree 独立每个克隆独立
仓库级配置与 Hooks默认共享分别维护
适合场景同一仓库的并行开发、热修复、测试需要真正独立的仓库配置、权限或生命周期

准备示例仓库

后续示例基于一个名为 shop 的 Git 仓库。当前终端已经位于仓库根目录,当前分支为 main,工作区没有待提交内容。

当前目录:/path/to/shop
当前分支:main

新建的 Worktree 都放在 shop 的同级目录,因此 ../shop-search 表示 /path/to/shop-search

创建第一个关联工作树

现在要开发搜索功能。我们希望创建 feature/search 分支,并把它检出到同级目录 shop-search

git worktree add -b feature/search ../shop-search main
             ↓   ↓  ↓              ↓              ↓
             │   │  │              │              └──── 新分支从 main 当前提交开始
             │   │  │              └─────────────────── 新工作目录,位于 shop 的同级目录
             │   │  └────────────────────────────────── 新分支名称
             │   └───────────────────────────────────── 创建新分支
             └───────────────────────────────────────── 添加 linked worktree

命令完成了三件事:创建 feature/search 分支、创建 shop-search 目录、在新目录中检出这个分支。

通过列表确认登记结果:

git worktree list

             └──── 列出每个 Worktree 的路径、当前提交和分支

预期能看到两个条目,类似下面这样。提交短哈希会因仓库而异:

/path/to/shop         06bcdac [main]
/path/to/shop-search  06bcdac [feature/search]

这个路径已经登记到仓库中。后面移动或删除它时,应优先使用 git worktree 命令,让目录和管理记录一起更新。

在两个目录中并行工作

shop-search 中新增 search.txt 后,通过 -C 指定命令执行目录:

git -C ../shop-search add search.txt
    ↓  ↓              ↓   ↓
    │  │              │   └──── 加入暂存区的文件
    │  │              └──────── 执行 git add
    │  └─────────────────────── 目标 Worktree
    └────────────────────────── 在指定目录中执行 Git 命令

git -C ../shop-search commit -m "feat: 添加搜索功能"
    ↓  ↓              ↓      ↓  ↓
    │  │              │      │  └──── 提交说明
    │  │              │      └─────── 使用下一段文本作为提交说明
    │  │              └────────────── 创建提交
    │  └───────────────────────────── 目标 Worktree
    └──────────────────────────────── 在指定目录中执行 Git 命令

回到主工作区读取 feature/search 的最新提交:

git log -1 feature/search
    ↓   ↓  ↓
    │   │  └──── 要读取的分支
    │   └─────── 只显示最新一条提交
    └─────────── 查看提交历史

主工作区仍停在 main,目录中没有 search.txtgit log 可以读取 feature/search 上的“添加搜索功能”提交。分支检出状态和文件彼此隔离,分支引用与提交对象由各个 Worktree 共享。

Worktree 只隔离 Git 工作目录。不同 Worktree 仍运行在同一台机器上,并继续使用项目配置的数据库、端口和系统环境。并行启动服务时,这些运行时资源需要单独配置。

为什么同一分支默认不能检出两次

这里很容易产生一个疑问:既然工作目录互相独立,能不能再创建一个目录,也检出 feature/search

我们直接制造这个失败条件:

git worktree add ../shop-search-copy feature/search
             ↓   ↓                   ↓
             │   │                   └──── 准备再次检出的已有分支
             │   └──────────────────────── 第二个工作目录
             └──────────────────────────── 添加 linked worktree

Git 2.33.0 返回的错误为:

fatal: 'feature/search' is already checked out at '/path/to/shop-search'

两个目录同时检出 feature/search 时,会共同更新这一个分支引用。一边提交后,共享的分支指针已经移动,另一边的 HEAD、暂存区和工作文件却还停留在旧基线。后续提交、重置和变基的含义会变得很难判断。

因此,Git 默认限制一个分支只能被一个 Worktree 检出。--force 可以绕过部分检查;日常并行开发应给每个任务单独创建分支,保留这层保护。

如果只是想在同一个提交上执行构建或阅读代码,不需要移动任何分支,可以使用分离 HEAD

git worktree add --detach ../shop-review main
             ↓   ↓        ↓              ↓
             │   │        │              └──── 检出的目标提交
             │   │        └─────────────────── 临时工作目录
             │   └──────────────────────────── 使用 detached HEAD
             └──────────────────────────────── 添加 linked worktree

git worktree remove ../shop-review
             ↓      ↓
             │      └──── 要删除的 linked worktree
             └─────────── 删除工作目录和对应管理记录

分离 HEAD 中也可以提交。这类提交缺少本地分支引用;如果临时修改需要保留,应及时用 git switch -c <新分支> 创建分支,再移除 Worktree。

日常操作:创建、使用与回收

日常使用主要涉及创建、查看和回收 Worktree。

从新分支创建工作树

前面的 feature/search 示例已经覆盖了创建新分支的形式。把分支名、目录和起点替换为实际任务即可,例如 hotfix/login../shop-hotfixmain

检出已经存在的分支

git worktree add ../shop-release release/2.0
             ↓   ↓               ↓
             │   │               └──── 已存在且未被其他 Worktree 检出的分支
             │   └──────────────────── 新工作目录
             └──────────────────────── 添加 linked worktree

如果远程只有 origin/release/2.0,建议把起点和跟踪关系写清楚,避免多个远程存在同名分支时产生歧义:

git worktree add --track -b release/2.0 ../shop-release origin/release/2.0
             ↓   ↓       ↓  ↓           ↓               ↓
             │   │       │  │           │               └──── 远程跟踪分支,也是新分支起点
             │   │       │  │           └──────────────────── 新工作目录
             │   │       │  └──────────────────────────────── 新建的本地分支名称
             │   │       └─────────────────────────────────── 创建本地分支
             │   └─────────────────────────────────────────── 设置 upstream
             └─────────────────────────────────────────────── 添加 linked worktree

当前官方文档也描述了自动猜测唯一远程分支的规则,以及 worktree.guessRemote 配置。团队脚本中仍建议显式写出远程和本地分支,因为行为更容易审阅。

查看全部工作树

git worktree list

             └──── 人工查看路径、提交和分支

git worktree list --porcelain -z
             ↓    ↓          ↓
             │    │          └──── 用 NUL 分隔字段
             │    └─────────────── 输出稳定的机器可解析格式
             └──────────────────── 列出 Worktree

自动化脚本应使用官方承诺稳定的 --porcelain 格式。默认表格面向人工阅读,列宽、空格和括号都不适合作为解析依据。

完成任务后回收

假设 feature/search 已完成,需要合并回 main

git merge feature/search
    ↓     ↓
    │     └──── 合并进入当前分支的任务分支
    └────────── 执行合并

git worktree remove ../shop-search
             ↓      ↓
             │      └──── 已完成任务的 linked worktree
             └─────────── 删除工作目录和关联管理记录

git branch -d feature/search
    ↓      ↓  ↓
    │      │  └──── 已合并、准备删除的分支
    │      └─────── 仅删除已经合并的分支
    └────────────── 管理本地分支

实测执行 git worktree remove 后,工作目录和关联管理记录已经删除,feature/search 分支依然存在。Worktree 与分支的生命周期需要分别管理。

remove 默认要求目标 Worktree 干净。存在未提交内容时,Git 会拒绝删除;remove --force 会丢弃工作目录中的内容。已锁定的 Worktree 需要连续两次 --force 才能删除。

热修复:保留当前现场

回到开头的场景。主目录正在 feature/refactor 上工作,文件尚未提交;线上问题必须从 main 修复。

git worktree add -b hotfix/payment-timeout ../project-hotfix main
             ↓   ↓  ↓                       ↓                ↓
             │   │  │                       │                └──── 热修复分支的起点
             │   │  │                       └───────────────────── 热修复使用的工作目录
             │   │  └───────────────────────────────────────────── 新分支名称
             │   └──────────────────────────────────────────────── 创建新分支
             └──────────────────────────────────────────────────── 添加 linked worktree

git worktree remove ../project-hotfix
             ↓      ↓
             │      └──── 热修复完成后的 linked worktree
             └─────────── 删除工作目录和关联管理记录

git branch -d hotfix/payment-timeout
    ↓      ↓  ↓
    │      │  └──── 已合并的热修复分支
    │      └─────── 仅删除已合并分支
    └────────────── 管理本地分支

project-hotfix 中完成修改、测试、提交、推送和合并。原 Worktree 的 HEAD、暂存区与未提交文件保持原样;热修复结束后,回到原目录即可继续工作。

需要注意,git stash 本身存放在共享的 refs/stash 中。因此在任意工作树执行 git stash list,看到的是同一组 stash 记录。使用多个工作树时,stash 消息最好写清任务和来源分支,避免在错误目录中应用了另一项工作的临时内容。

管理离线、移动和残留目录

Git 会记录每个 Worktree 所在的位置。目录离线、移动或被误删时,需要同步维护这条连接关系。

lock:防止暂时离线的目录被清理

如果 linked worktree 位于移动硬盘或偶尔离线的网络盘,可以在设备离线前使用 lock 保留其管理记录:

git worktree lock --reason "位于移动硬盘,暂时离线" ../shop-release
             ↓    ↓        ↓                    ↓
             │    │        │                    └──── 需要保留的 linked worktree
             │    │        └───────────────────────── 锁定原因,会写入管理记录
             │    └────────────────────────────────── 传入锁定原因
             └─────────────────────────────────────── 锁定 linked worktree

git worktree list --verbose
             ↓    ↓
             │    └──── 显示锁定原因和可清理原因
             └───────── 列出 Worktree

git worktree unlock ../shop-release
             ↓      ↓
             │      └──── 恢复可移动、可删除状态的 linked worktree
             └─────────── 解除锁定

move:让 Git 同时更新目录和记录

目标目录的父目录需要事先存在。move 会同步更新工作目录和管理记录:

git worktree move ../shop-release ../archive/shop-release
             ↓    ↓               ↓
             │    │               └──── 新位置
             │    └──────────────────── 当前 Worktree 路径
             └───────────────────────── 移动 linked worktree

主工作树不能通过该命令移动,包含子模块的关联工作树也不能通过该命令移动。遇到这两种情况,应先阅读当前版本官方限制,不要直接假设与普通目录相同。

repair:修复手动移动后的失联

如果目录已经被文件管理器或普通 mv 移走,仓库仍记录旧路径。手动移动后执行 dry-run,实测输出为:

Removing worktrees/demo-advanced: gitdir file points to non-existent location

这说明仓库把旧管理记录识别成了可清理项。此时不应继续 prune,而应根据新路径修复连接:

git worktree prune --dry-run --verbose --expire now
             ↓     ↓         ↓         ↓        ↓
             │     │         │         │        └──── 将过期时间设为当前时刻
             │     │         │         └───────────── 指定可清理记录的过期条件
             │     │         └─────────────────────── 显示每条候选记录
             │     └──────────────────────────────── 不删除,只预演清理结果
             └─────────────────────────────────────── 清理失联 Worktree 的管理记录

git -C ../shop-release-new worktree repair ../shop-release-new
    ↓  ↓                    ↓        ↓      ↓
    │  │                    │        │      └──── 移动后的实际路径
    │  │                    │        └─────────── 修复登记连接
    │  │                    └──────────────────── Worktree 子命令组
    │  └───────────────────────────────────────── 在移动后的目录中执行
    └──────────────────────────────────────────── 在指定目录中运行 Git

git worktree list --porcelain
             ↓    ↓
             │    └──── 输出稳定的机器可解析格式,便于核对路径
             └───────── 列出 Worktree

repair 执行后退出码为 0,列表中的路径更新为手动移动后的新路径,分支仍能正常读取。

prune:只清理已经失效的管理记录

如果有人直接删除了关联工作树目录,公共仓库中的 .git/worktrees/<id> 管理记录可能仍然存在。prune 用来清理这类陈旧记录:

git worktree prune --dry-run --verbose
             ↓     ↓         ↓
             │     │         └──── 显示每条候选记录
             │     └────────────── 不删除,只预演清理结果
             └──────────────────── 清理失联 Worktree 的管理记录

git worktree prune --verbose
             ↓     ↓
             │     └──── 显示实际移除的记录
             └────────── 清理失联 Worktree 的管理记录

正常回收使用 git worktree remove,它会同时处理工作目录和管理记录。prune 用于工作目录已经消失、只剩陈旧管理记录的情况。

大型仓库只展开需要的目录

多个 Worktree 共享对象数据库,但每个工作目录仍要展开自己的文件。Git 把按规则只展开部分路径的能力称为 sparse-checkout。在大型单仓库中,如果某项任务只关心 docs,可以把它与 Worktree 组合使用。

假设 main 同时追踪 docs/guide.mdsrc/app.txt

git worktree add --no-checkout -b docs/review ../shop-docs main
             ↓   ↓             ↓  ↓           ↓           ↓
             │   │             │  │           │           └──── 检出的起点
             │   │             │  │           └──────────────── 新工作目录
             │   │             │  └──────────────────────────── 新分支名称
             │   │             └─────────────────────────────── 创建新分支
             │   └───────────────────────────────────────────── 创建目录时暂不检出文件
             └───────────────────────────────────────────────── 添加 linked worktree

git -C ../shop-docs sparse-checkout init --cone
    ↓  ↓            ↓              ↓    ↓
    │  │            │              │    └──── 使用按目录匹配的 cone 模式
    │  │            │              └───────── 初始化 sparse-checkout
    │  │            └──────────────────────── 稀疏检出子命令组
    │  └───────────────────────────────────── 目标 Worktree
    └──────────────────────────────────────── 在指定目录中运行 Git

git -C ../shop-docs sparse-checkout set docs
    ↓  ↓            ↓              ↓   ↓
    │  │            │              │   └──── 需要展开的顶层目录
    │  │            │              └──────── 设置稀疏检出规则
    │  │            └─────────────────────── 稀疏检出子命令组
    │  └──────────────────────────────────── 目标 Worktree
    └─────────────────────────────────────── 在指定目录中运行 Git

git -C ../shop-docs checkout
    ↓  ↓            ↓
    │  │            └──── 按当前 sparse-checkout 规则展开文件
    │  └───────────────── 目标 Worktree
    └──────────────────── 在指定目录中运行 Git

回归时先用 git cat-file 确认当前提交同时包含两个文件,再检查工作区,结果为 docs 存在、src 不存在。由此可以确认筛选发生在文件展开阶段。

sparse-checkout 减少工作目录展开的文件,对象数据库仍由各个 Worktree 完整共享。具体规则和命令在不同 Git 版本中有变化,团队落地前应按所用 Git 版本补充验证。

不同 Worktree 使用不同配置

默认情况下,仓库的 .git/config 由所有工作树共享。也就是说,在关联工作树中执行普通的 git config user.email ...,改到的仍是公共仓库配置。

如果同一个仓库的不同工作树确实需要不同配置,可以开启 worktreeConfig 扩展:

git config extensions.worktreeConfig true
    ↓      ↓                            ↓
    │      │                            └──── 开启该扩展
    │      └──────────────────────────────── 工作树独立配置扩展名
    └─────────────────────────────────────── 修改仓库配置

git config --worktree user.email "main@example.com"
    ↓      ↓           ↓          ↓
    │      │           │          └──── 主工作区使用的提交邮箱
    │      │           └─────────────── 配置键
    │      └────────────────────────── 写入当前 Worktree 的配置文件
    └───────────────────────────────── 修改仓库配置

git -C ../shop-docs config --worktree user.email "docs@example.com"
    ↓  ↓            ↓      ↓           ↓          ↓
    │  │            │      │           │          └──── docs Worktree 使用的提交邮箱
    │  │            │      │           └─────────────── 配置键
    │  │            │      └─────────────────────────── 写入目标 Worktree 的配置文件
    │  │            └────────────────────────────────── 修改仓库配置
    │  └─────────────────────────────────────────────── 目标 Worktree
    └────────────────────────────────────────────────── 在指定目录中运行 Git

实测主工作区返回 main@example.com,关联工作区返回 docs@example.com。后者的配置文件位于公共仓库管理区的 .git/worktrees/<id>/config.worktree

工作树独立配置适合差异确实与工作目录绑定的场景,例如不同稀疏检出设置。不要为了普通分支差异随意开启它,因为仓库格式扩展会带来旧版本兼容成本。

.git 如何连接多个 Worktree

先在关联工作树中检查 .git

cat ../shop-docs/.git
↓   ↓
│   └──── linked worktree 顶层的 Git 连接文件
└──────── 读取文件内容

git -C ../shop-docs rev-parse --git-dir
    ↓  ↓            ↓         ↓
    │  │            │         └──── 输出当前 Worktree 的私有管理目录
    │  │            └────────────── 解析 Git 内部路径
    │  └─────────────────────────── 目标 Worktree
    └──────────────────────────────── 在指定目录中运行 Git

git -C ../shop-docs rev-parse --git-common-dir
    ↓  ↓            ↓         ↓
    │  │            │         └──── 输出全部 Worktree 共用的仓库目录
    │  │            └────────────── 解析 Git 内部路径
    │  └─────────────────────────── 目标 Worktree
    └──────────────────────────────── 在指定目录中运行 Git

实测目录结构与下面一致,真实绝对路径会随仓库位置变化:

gitdir: /path/to/shop/.git/worktrees/shop-docs
/path/to/shop/.git/worktrees/shop-docs
/path/to/shop/.git

关联工作树顶层的 .git 文件指向公共仓库中的私有管理目录。该私有目录里有当前工作树自己的 HEADindexgitdircommondir

shop/.git/
├── objects/
├── refs/
├── config
└── worktrees/
    └── shop-docs/
        ├── HEAD
        ├── index
        ├── gitdir
        ├── commondir
        └── config.worktree

objects 保存全部 Worktree 共享的提交、树和文件对象,refs 保存大部分共享分支与标签,config 是默认共享的仓库配置。HEADindex 属于 shop-docsgitdir 记录关联目录的 .git 文件位置,commondir 指回公共仓库目录,启用扩展后才会生成 config.worktree

这是一条双向连接:关联目录的 .git 指向管理区,管理区的 gitdir 又指回关联目录。move 会同时维护两边,repair 则用于外部移动让两边失去一致后的恢复。

共享与隔离可以更准确地归纳为:

状态是否共享直接结果
对象数据库共享一处提交后,其他工作树立即能解析该提交
大部分分支和标签引用共享一处创建、移动或删除分支,其他工作树立即感知
HEAD每个工作树独立不同目录可以停在不同分支或提交
index 暂存区每个工作树独立两边可以分别 git add,修改只进入当前暂存区
工作目录文件每个工作树独立未提交修改与检出文件互不覆盖
仓库配置和 Hooks默认共享在任一工作树修改仓库级设置,可能影响全部工作树
config.worktree可选独立开启扩展后可保存与工作树绑定的配置

官方给出的引用规则比表格更严格:一般来说,直接位于 $GIT_DIR 下的伪引用是每个工作树独立的,refs/ 下的引用共享;但 refs/bisectrefs/worktreerefs/rewritten 等存在例外。因此,脚本不要自己拼接 .git 内部路径,应使用 git rev-parse --git-path <名称> 让 Git 按当前工作树解析。

常见误区与边界

磁盘占用仍会增加

对象数据库由各个 Worktree 共享,每个工作目录仍有一份实际检出的文件,构建产物和依赖目录也可能各占一份空间。共享对象主要减少 Git 历史的重复存储;大型项目还可以配合 sparse-checkout,或让构建缓存指向经过验证的共享位置。

删除目录前先解除 Worktree 关联

直接删除只处理了文件系统目录,没有同步处理公共仓库中的管理记录。日常清理使用 git worktree remove;误删后再用 git worktree prune --dry-run 检查遗留项。

分支、标签和仓库配置仍然共享

分支、标签、stash、仓库配置和 Hooks 等仍然共享。一个 Worktree 中执行分支删除、重置共享分支或修改仓库配置,可能立刻影响其他 Worktree。独立范围主要包括工作目录、HEAD 和暂存区。

运行环境需要单独隔离

不同 Worktree 继续使用项目默认的端口、数据库、容器名、环境变量和依赖缓存。两个服务若都监听 3000 端口,仍会发生冲突。需要并行运行时,应在项目层显式区分运行时资源。

边界:包含子模块的项目要额外谨慎

当前官方文档仍把多工作树对子模块的支持列为不完整,并明确不建议对 superproject 做多份检出。git worktree move 也不能移动包含子模块的关联工作树。此类仓库不要直接套用普通项目流程,应先建立最小样本验证初始化、更新、移动和清理行为。

验收清单

完成第一次 Worktree 实践后,可以逐项确认:

  • git worktree list 同时显示主工作树和关联工作树。
  • 两个目录的 git branch --show-current 返回不同分支。
  • 在关联工作树提交后,主工作树能通过分支名读取提交,但主目录文件没有自动变化。
  • 重复检出同一分支时,Git 给出“already checked out”保护性错误。
  • 有未提交或未跟踪文件时,普通 git worktree remove 会拒绝删除。
  • 工作树移除后,对应分支仍存在,分支删除需要单独执行。
  • 自动化脚本使用 git worktree list --porcelain -z,不解析默认表格。
  • 对手动移动或误删场景,先执行 prune --dry-run,再选择 repair 或实际 prune

总结

Git Worktree 让同一个仓库同时连接多个工作目录。每个 Worktree 拥有独立的 HEAD、暂存区和工作文件,同时共享对象数据库与大部分引用。当前任务可以留在原目录,临时任务则在另一个目录中开始。

基本使用只需要记住 addlistremove。当目录位于离线设备、被外部移动、需要程序化管理或只想检出部分文件时,再引入 lockrepairprune--porcelain 与 sparse-checkout。

Worktree 隔离 Git 工作现场;分支、对象与仓库级配置仍有共享部分;端口、数据库和容器等运行资源由项目自行管理。掌握这三层边界后,就可以把它用于热修复、并行开发、代码审查和多版本测试。

参考资料

评论

使用 GitHub 登录参与讨论。