不用再反复 stash:用 Git Worktree 同时开发多个分支

很多开发者都遇到过这样的场景:你正在一个功能分支上写代码,修改了十几个文件,程序暂时还跑不起来,也不适合提交。就在这时,同事突然告诉你,线上出现了一个紧急问题,需要马上从 main 分支拉出一个 Hotfix。

按照传统做法,你可能会先执行:

bash 复制代码
git stash
git switch main
git pull
git switch -c hotfix/xxx

修复完成以后,再回到原来的分支:

bash 复制代码
git switch feature/xxx
git stash pop

如果只是偶尔操作一次,这种方式倒也能用。但当你同时维护多个 PR、审查别人代码、修复线上问题,或者需要在不同版本之间反复测试时,频繁执行 stashswitch 很容易让工作现场变得混乱。

Git 其实提供了一个非常适合这种场景的功能:Git Worktree

内容结构概览

本文将介绍:

  1. Git Worktree 是什么;
  2. 它与 git switchgit stash 和重新 clone 有什么区别;
  3. 如何创建、查看和删除 Worktree;
  4. 如何在开发新功能的同时处理紧急 Bug;
  5. 多个 PR、代码审查和版本测试中的使用方法;
  6. 常见问题与容易踩到的坑。

一、Git Worktree 是什么?

Git Worktree 可以让一个 Git 仓库同时拥有多个工作目录,每个目录分别检出不同的分支。

普通情况下,一个仓库只有一个代码目录:

text 复制代码
my-project/

这个目录某一时刻只能显示一个分支。你从 main 切到 feature/login 后,目录中的代码也会随之变化。

使用 Worktree 后,可以变成这样:

text 复制代码
code/
├── my-project/                 # main
├── my-project-feature-login/   # feature/login
└── my-project-hotfix/          # hotfix/payment-timeout

三个目录属于同一个 Git 仓库,但分别检出了三个不同分支。

你可以在第一个目录运行稳定版代码,在第二个目录继续开发新功能,同时在第三个目录修复线上问题。它们彼此独立,不需要来回切换分支,也不需要把未完成的代码临时藏进 stash

Git 官方把最初通过 git clonegit init 创建的工作目录称为 main worktree ,后来通过 git worktree add 创建的目录称为 linked worktree 。一个普通仓库可以拥有一个主工作区,以及零个或多个附加工作区。(Git)


二、Worktree 到底共享什么?

Worktree 并不是重新复制出一份完整的 Git 仓库。

多个 Worktree 会共享:

  • Git 提交历史;
  • 对象数据库;
  • 分支和标签引用;
  • 远程仓库信息;
  • 大部分 Git 配置。

但每个 Worktree 都拥有自己的:

  • 工作目录;
  • 当前检出的分支;
  • HEAD
  • 暂存区,也就是 index;
  • 未提交修改;
  • 未跟踪文件。

可以把它理解为:所有工作区共用一个仓库档案室,但每个工作区都有自己的桌面。

text 复制代码
                 共享的 Git 仓库
              commits / objects / refs
                         │
          ┌──────────────┼──────────────┐
          │              │              │
       Worktree 1     Worktree 2     Worktree 3
          main          feature         hotfix
       独立文件        独立文件        独立文件
       独立 HEAD        独立 HEAD       独立 HEAD
       独立 index       独立 index      独立 index

Git 官方文档也说明,不同 Worktree 的 HEAD 等状态彼此独立,而普通分支引用通常由各个 Worktree 共享。(Git)


三、为什么不直接重新 clone 一份?

当然可以重新 clone:

bash 复制代码
git clone git@github.com:example/project.git project-main
git clone git@github.com:example/project.git project-feature
git clone git@github.com:example/project.git project-hotfix

但这样会得到三套独立仓库:

text 复制代码
project-main/.git/
project-feature/.git/
project-hotfix/.git/

对于大型仓库,这意味着:

  • Git 对象可能被重复存储;
  • 每个仓库都要单独执行 fetch
  • Remote、Hook 和部分配置可能不一致;
  • 本地仓库数量越来越多,很难管理;
  • 创建新环境的速度比 Worktree 慢。

Worktree 复用的是同一套 Git 仓库数据,因此创建速度通常很快,也更加节省空间。

不过要注意,Worktree 只共享 Git 数据,并不会共享所有构建产物。例如下面这些目录通常仍然是各自独立的:

text 复制代码
node_modules/
target/
dist/
vendor/
.venv/

因此两个大型 Rust Worktree 仍可能分别生成自己的 target 目录,占用较多磁盘空间。


四、查看当前已有的 Worktree

进入任意一个工作区,执行:

bash 复制代码
git worktree list

可能得到:

text 复制代码
/Users/mac/code/my-project          a1b2c3d [main]
/Users/mac/code/my-project-login    d4e5f6g [feature/login]
/Users/mac/code/my-project-hotfix   h7i8j9k [hotfix/payment-timeout]

每一行分别表示:

text 复制代码
工作目录路径    当前提交    当前分支

查看更详细的信息:

bash 复制代码
git worktree list --verbose

需要在 Shell 脚本中处理输出时,可以使用:

bash 复制代码
git worktree list --porcelain

五、为已有分支创建 Worktree

假设仓库中已经存在:

text 复制代码
feature/login

当前主仓库位于:

text 复制代码
~/code/my-project

可以执行:

bash 复制代码
cd ~/code/my-project

git worktree add \
  ../my-project-feature-login \
  feature/login

Git 会创建新目录:

text 复制代码
~/code/my-project-feature-login

并在里面检出 feature/login

现在可以直接进入新目录:

bash 复制代码
cd ../my-project-feature-login
git status

输出会显示:

text 复制代码
On branch feature/login

此时主目录仍然保持原来的分支和所有未提交修改,完全不会受到影响。


六、创建新分支时同时创建 Worktree

这是最常用的方式。

假设要从最新的 origin/main 创建一个 Hotfix:

bash 复制代码
git fetch origin

git worktree add \
  -b hotfix/payment-timeout \
  ../my-project-payment-timeout \
  origin/main

这条命令同时完成三件事:

  1. 基于 origin/main 创建 hotfix/payment-timeout
  2. 创建目录 ../my-project-payment-timeout
  3. 在新目录中检出该分支。

它的通用格式是:

bash 复制代码
git worktree add -b <新分支名> <新目录> <起始提交>

起始提交可以是:

text 复制代码
main
origin/main
release/v2
v2.1.0
某个 commit hash

例如:

bash 复制代码
git worktree add \
  -b fix/readme-typo \
  ../my-project-fix-readme \
  origin/main

我通常建议明确写出 origin/main,而不是省略起点。这样可以避免本地 main 很久没有更新,导致新分支建立在旧提交上。


七、实战:开发未完成时紧急修复 Bug

假设你正在 ethereumjs-monorepo 中开发一个功能,当前分支有不少未提交修改:

bash 复制代码
git status

输出类似:

text 复制代码
On branch feature/trie-refactor

Changes not staged for commit:
    modified: packages/trie/src/trie.ts
    modified: packages/trie/test/trie.spec.ts

现在你需要基于上游最新代码修复一个 README typo。

不需要 stash,直接执行:

bash 复制代码
cd ~/code/ethereumjs-monorepo

git fetch upstream

git worktree add \
  -b fix-util-readme-typo \
  ../ethereumjs-fix-util-readme-typo \
  upstream/master

如果项目默认分支是 main,则改成:

bash 复制代码
git worktree add \
  -b fix-util-readme-typo \
  ../ethereumjs-fix-util-readme-typo \
  upstream/main

进入新的工作区:

bash 复制代码
cd ../ethereumjs-fix-util-readme-typo

完成修改:

bash 复制代码
git add packages/util/README.md

git commit -m "docs(util): fix README typo"

git push -u origin fix-util-readme-typo

此时目录结构可能是:

text 复制代码
~/code/
├── ethereumjs-monorepo/
│   └── feature/trie-refactor,保留未提交修改
│
└── ethereumjs-fix-util-readme-typo/
    └── fix-util-readme-typo,准备提交 PR

两个工作区互不干扰。

修复完成后,你可以在浏览器里提交 PR,也可以使用 GitHub CLI:

bash 复制代码
gh pr create

然后回到原来的目录继续开发:

bash 复制代码
cd ../ethereumjs-monorepo

不需要执行:

bash 复制代码
git stash
git switch
git stash pop

也不需要重新启动或恢复原来分支的工作状态。


八、PR 合并后如何清理?

先确认工作区中没有需要保留的修改:

bash 复制代码
cd ../ethereumjs-fix-util-readme-typo
git status

然后回到主仓库或者任意其他 Worktree:

bash 复制代码
cd ../ethereumjs-monorepo

删除附加工作区:

bash 复制代码
git worktree remove ../ethereumjs-fix-util-readme-typo

注意,git worktree remove 会删除对应的工作目录,但不会自动删除分支。

如果本地分支已经合并,可以继续执行:

bash 复制代码
git branch -d fix-util-readme-typo

清理远程已经删除的跟踪引用:

bash 复制代码
git fetch --prune origin

完整清理流程如下:

bash 复制代码
git worktree remove ../ethereumjs-fix-util-readme-typo
git branch -d fix-util-readme-typo
git fetch --prune origin

Git 官方建议使用 git worktree remove 删除附加 Worktree,而不是直接删除目录。(Git)


九、为什么不能直接执行 rm -rf?

下面这种操作并不推荐:

bash 复制代码
rm -rf ../my-project-hotfix

因为目录虽然被删除了,但主仓库内部可能仍然保留该 Worktree 的管理信息。

这时执行:

bash 复制代码
git worktree list

可能仍会看到已经不存在的工作区。

可以使用:

bash 复制代码
git worktree prune

清理失效记录。

正式删除时,最好始终使用:

bash 复制代码
git worktree remove <工作区路径>

如果目录已经被手动删除,再执行:

bash 复制代码
git worktree prune

查看哪些记录将被清理,但暂时不执行:

bash 复制代码
git worktree prune --dry-run --verbose

十、工作区中有未提交代码时怎么办?

假设某个 Worktree 中仍然存在修改:

bash 复制代码
git status

显示:

text 复制代码
Changes not staged for commit

此时执行:

bash 复制代码
git worktree remove ../my-project-hotfix

Git 通常会拒绝删除,因为这会导致未提交内容丢失。

正确做法是先决定如何处理这些代码:

方案一:提交代码

bash 复制代码
git add .
git commit -m "wip: preserve current work"

方案二:暂存修改

bash 复制代码
git stash push -u

方案三:确认不再需要,强制删除

bash 复制代码
git worktree remove --force ../my-project-hotfix

--force 可能造成代码永久丢失,使用前应确认该目录中没有任何需要保留的文件。


十一、一个分支不能同时在两个 Worktree 中检出

假设 main 已经在主工作区中使用:

bash 复制代码
git worktree list

输出:

text 复制代码
/Users/mac/code/my-project  a1b2c3d [main]

此时再执行:

bash 复制代码
git worktree add ../my-project-main-copy main

Git 通常会拒绝,并提示:

text 复制代码
fatal: 'main' is already checked out

这是为了防止两个工作目录同时修改同一个分支指针。

假设你只是想基于 main 做实验,应该创建一个新分支:

bash 复制代码
git worktree add \
  -b experiment/new-parser \
  ../my-project-new-parser \
  main

或者创建一个 detached HEAD 工作区:

bash 复制代码
git worktree add \
  --detach \
  ../my-project-main-test \
  main

默认情况下,Git 会阻止同一个分支同时出现在多个 Worktree 中。(Git)


十二、用 Worktree 审查别人的 PR

Worktree 不仅适合开发,也非常适合代码审查。

假设要审查 GitHub PR #1234,可以先拉取 PR 分支:

bash 复制代码
git fetch origin pull/1234/head:review/pr-1234

然后创建 Worktree:

bash 复制代码
git worktree add \
  ../my-project-review-1234 \
  review/pr-1234

进入该目录:

bash 复制代码
cd ../my-project-review-1234

在不影响当前开发环境的情况下运行:

bash 复制代码
go test ./...

或者:

bash 复制代码
cargo test

审查结束后:

bash 复制代码
cd ../my-project

git worktree remove ../my-project-review-1234
git branch -D review/pr-1234

这样每个 PR 都可以拥有独立的测试环境,不需要频繁切换当前仓库的分支。


十三、同时维护多个 PR

对于经常给开源项目提交贡献的人,Worktree 非常实用。

假设正在处理三个 PR:

text 复制代码
fix/readme-typo
fix/parser-comment
test/new-analyzer

可以分别创建:

bash 复制代码
git fetch upstream

git worktree add \
  -b fix/readme-typo \
  ../project-fix-readme \
  upstream/main

git worktree add \
  -b fix/parser-comment \
  ../project-fix-parser-comment \
  upstream/main

git worktree add \
  -b test/new-analyzer \
  ../project-new-analyzer \
  upstream/main

目录结构变成:

text 复制代码
code/
├── project/
├── project-fix-readme/
├── project-fix-parser-comment/
└── project-new-analyzer/

每个目录可以分别打开一个终端或 IDE 窗口。

等待第一个 PR 的 CI 时,可以直接进入第二个目录继续工作:

bash 复制代码
cd ../project-fix-parser-comment

这种方式比不断 switch 分支更加自然,也不容易把不同 PR 的文件混在一起。

不过,同时提交多个 PR 时仍应注意项目维护者的体验。相关的小修改最好合并处理,不要为了增加提交数量而拆成大量价值很低的 PR。


十四、测试旧版本或历史提交

Worktree 也很适合临时查看某个版本。

例如,需要测试 v1.5.0

bash 复制代码
git worktree add \
  --detach \
  ../my-project-v1.5.0 \
  v1.5.0

进入该目录:

bash 复制代码
cd ../my-project-v1.5.0

完成编译或测试后删除:

bash 复制代码
cd ../my-project

git worktree remove ../my-project-v1.5.0

因为使用了 --detach,这个工作区没有绑定普通分支,适合:

  • 复现旧版本 Bug;
  • 比较两个版本的性能;
  • 编译某个 Tag;
  • 检查历史提交;
  • 执行临时实验。

十五、移动 Worktree

不要优先使用普通的 mv 命令移动工作区。

推荐执行:

bash 复制代码
git worktree move \
  ../my-project-hotfix \
  ../worktrees/my-project-hotfix

如果已经使用 Finder 或 mv 手动移动了目录,导致 Git 的路径记录失效,可以尝试:

bash 复制代码
git worktree repair

也可以指定路径:

bash 复制代码
git worktree repair ../worktrees/my-project-hotfix

当前 Git Worktree 命令提供了 moverepair,分别用于移动工作区和修复工作区关联。(Git)


十六、外置硬盘上的 Worktree

有时 Worktree 放在移动硬盘、网络磁盘或者临时挂载目录中。磁盘没有挂载时,Git 可能认为该工作区已经失效,并在后续清理中删除管理记录。

可以锁定该 Worktree:

bash 复制代码
git worktree lock \
  --reason "Stored on external SSD" \
  /Volumes/SSD/my-project-release

解除锁定:

bash 复制代码
git worktree unlock \
  /Volumes/SSD/my-project-release

日常放在本机固定目录中的 Worktree 通常不需要执行 lock。这个功能主要用于路径可能暂时不可访问的工作区。Git 官方命令集中也包含 lockunlock。(Git)


十七、推荐的目录组织方式

不建议把 Worktree 建在主仓库内部:

text 复制代码
my-project/
└── worktrees/
    └── feature-login/

这样可能导致:

  • IDE 重复索引大量文件;
  • 搜索结果出现多个版本;
  • 文件监听器监控额外目录;
  • 构建工具误扫描另一个工作区;
  • 主仓库出现复杂的未跟踪目录。

更推荐使用并列目录:

text 复制代码
~/code/
├── ethereumjs-monorepo/
├── ethereumjs-fix-readme/
└── ethereumjs-trie-refactor/

或者统一放在专门目录:

text 复制代码
~/code/
├── ethereumjs-monorepo/
└── worktrees/
    └── ethereumjs/
        ├── fix-readme/
        ├── trie-refactor/
        └── review-pr-1234/

对应命令:

bash 复制代码
git worktree add \
  -b fix/readme \
  ~/code/worktrees/ethereumjs/fix-readme \
  upstream/main

目录名称不必与分支名完全相同,但保持一致会更容易管理。


十八、Worktree、stash 和重新 clone 怎么选?

三种方式分别适合不同场景。

场景 推荐方式
临时离开当前分支几分钟 git stash
两个任务需要并行开发 Worktree
同时运行新旧两个版本 Worktree
审查多个 PR Worktree
复现某个历史版本问题 Worktree
需要完全不同的 Remote 和 Git 配置 重新 clone
要破坏性修改 .git 数据 重新 clone
多个 AI Agent 同时处理不同任务 Worktree

简单来说:

stash 是临时把桌面收起来,Worktree 是再准备一张桌子,重新 clone 则是再租一个办公室。


十九、常用命令速查

查看全部 Worktree:

bash 复制代码
git worktree list

为已有分支创建 Worktree:

bash 复制代码
git worktree add <目录> <已有分支>

创建新分支并创建 Worktree:

bash 复制代码
git worktree add -b <新分支> <目录> <起点>

创建 detached HEAD 工作区:

bash 复制代码
git worktree add --detach <目录> <提交或标签>

删除 Worktree:

bash 复制代码
git worktree remove <目录>

强制删除:

bash 复制代码
git worktree remove --force <目录>

移动 Worktree:

bash 复制代码
git worktree move <旧路径> <新路径>

清理失效记录:

bash 复制代码
git worktree prune

修复关联:

bash 复制代码
git worktree repair

锁定外置工作区:

bash 复制代码
git worktree lock <目录>

解除锁定:

bash 复制代码
git worktree unlock <目录>

二十、总结

Git Worktree 解决的问题并不复杂:

它允许同一个 Git 仓库同时在多个目录中检出不同分支。

过去,我们常常需要在一个目录中不断执行:

bash 复制代码
git stash
git switch
git stash pop

使用 Worktree 后,很多时候只需要切换目录:

bash 复制代码
cd ../project-feature
cd ../project-hotfix

每个任务都有自己的目录、分支、暂存区和未提交修改。正在运行的开发服务器、IDE 窗口以及测试环境也可以继续保持,不需要因为切换分支而反复重启。

它尤其适合:

  • 同时维护多个 PR;
  • 开发新功能时紧急修复 Bug;
  • 审查其他人的代码;
  • 对比多个版本;
  • 同时运行多个测试环境;
  • 让多个自动化工具或 AI Agent 并行工作。

掌握 Worktree 后,Git 分支不再只是需要不断切换的状态,而可以变成多个真正并行存在的工作空间。

对于经常参与大型开源项目、同时维护多个分支的开发者来说,这可能是 Git 中最值得养成使用习惯的功能之一。


参考资料

Git 官方 git-worktree 文档,对 Worktree 的创建、查看、删除、移动、清理、锁定和修复等行为给出了完整定义。(Git)

相关推荐
卷无止境19 小时前
KTransformers:让巨型模型跑在你家电脑上的黑科技
人工智能·后端
_waylau19 小时前
Spring Framework HTTP服务客户端详解
java·后端·网络协议·spring·http·spring cloud
是小李呀19 小时前
如何安全撤销未推送的 Git Revert 操作
后端
蓝银草同学19 小时前
Stream 数据统计实战:求和、平均值、分组汇总(AI 辅助学习 Java 8)
java·前端·后端
fengxinzi_zack19 小时前
TagManagePage:CRUD 标签管理 + 编辑模式
后端
IT_陈寒19 小时前
Redis过期key的坑,差点让线上服务崩了
前端·人工智能·后端
Tenifs19 小时前
在 VS Code 中,你可以通过修改全局或项目目录下的 settings.json 文件来彻底关闭 GitHub Copilot
github·copilot
月落归舟20 小时前
谈谈SpringMVC底层原理
后端·springmvc