AI 同步开发多需求不打架:git worktree

ClawLihai · · 共 3,863 字 · 约 13 分钟读完

你让 AI 改一个功能,改到一半——它已经动了十几个文件,测试跑了一半——产品经理突然甩来一个急活:线上出问题了,现在就看。

你面前三条路,条条疼:

  • stash 一下切分支?AI 的会话还开着,它记得的是刚才那份代码。你 stash 完,工作区瞬间换了个世界,会话上下文全作废。
  • 再 clone 一份仓库?依赖重装、配置重配,两个 .git 各长各的,一周后你已经分不清哪份是哪份。
  • 让急活排队?它是线上问题。

其实还有第四条路。git 在 2015 年 7 月就给出了答案,只是沉了十一年一直没火:一个仓库,可以同时检出 N 个工作区。命令叫 git worktree。

第一幕·认识:它是什么,为什么现在火

一个工作区,只能站一个分支

先看清你每天在用的 git,被「单工作区」锁得有多死。

一个 git 仓库分三层:最底层是仓库本身(.git 目录),所有提交历史、所有分支都在这,只有一份;中间是暂存区(index);最上面是工作区,就是你正在编辑的那堆文件。

Git 仓库三层结构:.git 存全部历史与分支,checkout 把快照铺进暂存区和工作区,HEAD 标记当前分支

git checkout 的本质,是把某个快照「铺」到工作区里。问题在于:HEAD 只有一个,工作区也只有一个。所以同一时刻,一个仓库只能「站」在一个分支上。想换分支,就得把整个工作区换掉——文件、暂存区、HEAD,全部重置。

这套东西的底层是一套指针系统:提交是链条,分支只是指向某个提交的 41 字节指针文件,HEAD 又指向分支——后面 worktree 的所有行为,都能从这套指针模型推出来。

分支指针模型:提交 c1→c2→c3 成链,main 和 dev 分支只是指向提交的指针,HEAD 指向当前分支

这不是 bug,是 2005 年的默认设计:一个人,一台机器,一次干一件事。十几年没人觉得不对——因为人写代码,本来就快不到哪去。

worktree:不是复制仓库,是多开工作区

一条命令:

git worktree add ../myproject-hotfix -b hotfix-login

git 会在仓库外面新建一个完整的工作目录:有自己的工作区、自己的暂存区、自己的 HEAD,站在你指定的新分支上。

关键在于:仓库还是那一个。.git 没有被复制,提交历史和分支仍然只有一份,存在主仓库里。落到文件系统上看更直观——主工作区的 .git 是个目录(真身),分身目录里的 .git 是个只有一行路径的文件,两边互相登记在册:

worktree 物理文件结构:主工作区的 .git 是包含 objects、refs、worktrees 登记的目录,分身目录的 .git 只是一行路径的文件,指回主仓库

比喻:不是再买一套房,是公司本来有个大档案室(.git),现在给每个项目组各分一间独立办公室(工作区)。档案共享,办公室互不打扰。

一个 .git 挂多个工作区:主工作区与两个分身各自持有独立 HEAD,共享同一份提交历史

还有一条容易踩的保护机制:同一个分支,不能被两个工作区同时占用。你试着在第二个 worktree 里 checkout 主工作区正站着的分支,git 会直接拒绝(老版本报 already checked out,新版本报 already used by worktree at),它防的就是两边同时改同一份代码。

校准一下:它不是「复制了一份代码」

很多人听完流程——开个目录、进去开发、提交、删掉目录——第一反应都是:「这不就是把代码复制一份去改,改完删掉?」流程感觉是对的,但用「复制」建模,四个预测全错:

  1. 你会以为新目录要跟原目录「同步」才能看到对方的提交。实际两边共享同一个 .git,任何一边的提交,另一边零延迟可见,不存在「同步」这个动作。
  2. 你会以为删掉那个目录,成果就跟着没了。实测 rm -rf 掉整个 worktree 目录后,分支上的提交原封不动,随时能取回——目录只是消耗品,提交才是资产,先删目录、后合并,完全合法。
  3. 你会以为两份「复制」可以各自检出同一个分支。实际 git 直接拒绝——就是上面那条保护机制。
  4. 你会以为磁盘翻倍。实际历史和对象库全仓库仅此一份,真正「复制一份」是 git clone 干的事,代价是两套 .git 各自为政,还得维护同步关系。

还有一个坑同样源于这个误解:复制会连 .gitignore 的文件一起带过去,而 worktree 是全新检出,只带 git 管着的文件——你的 .env、node_modules、CLAUDE.md 统统不会出现(AI 时代这是头号坑,第二幕实战细讲)。

所以更准的心智模型还是那个办公室:复制的只是「桌面」,档案室永远只有一间。

为什么 2026 年突然翻红:AI 太快了

这功能沉了十一年,为什么现在到处都在讨论?

因为 vibe coding 时代,AI 把「写代码」的速度提上来之后,单工作目录从「够用」变成了「瓶颈」。

以前的节奏是一个任务写完、提交、切下一个。现在 AI 会话几乎不要钱,你完全可以让一个 AI 修 bug、一个 AI 写新功能、自己再 review 第三个任务的代码——三件事同时推进。

但只要它们挤在同一个工作目录里,就是灾难:A 会话刚改完的文件,被 B 会话切分支换掉了。stash 救不了——AI 会话的上下文是绑定工作区状态的。它记得自己改了哪些文件、改到哪一步、哪个测试跑过了;你一 stash 一切分支,工作区换了个世界,这些记忆全部作废,切回来还得从头对齐。人被 stash 折腾一下还能忍,AI 会话直接废掉。

物理隔离才是正解:每个任务一个 worktree,各自目录、各自分支、各自提交,互不知道对方存在。所以你会看到 2026 年的景象:

  • Claude Code 把 worktree 做成了一等公民:claude --worktree 任务名 直接在隔离 worktree 里启动会话,子代理可以声明 isolation: worktree,还提供了 .worktreeinclude 文件——用 .gitignore 的语法声明「这些被忽略的配置要自动带进新 worktree」。

  • 社区冒出一批专门管理 worktree 的工具:Worktrunk、Conductor、Vibe Kanban、Claude Squad……本质都是给「一仓多线」(本篇就是一仓三线)加个管理层——看板或会话面板。

  • 连「怎么建 worktree」都有 skill 代劳:流行的 superpowers 技能库里有专门的 using-git-worktrees,核心原则是「优先平台原生工具,手动 git 建只做兜底」。

    它的理由:绕过原生工具,会造出宿主看不见也管不了的「幻影状态」。

    你只需要说一句人话:「基于当前内容建个 worktree,不要未提交的改动」,剩下的它自己路由。

工具热不热闹不重要,重要的是这个共识:AI 并行开发的瓶颈,落在了 git 的单工作目录上。

第二幕·实战:一仓三线,从开工到合流

讲个我自己的真实场景。手头一个 Java 多模块项目,某天早上同时存在三份技术方案:

  • 主工作区:一个改到一半、还没提交的特性,四十多个文件动过了;
  • 任务一:公式联查,技术方案评审完,代码未动;
  • 任务二:历史现金流分析,同样方案就绪、代码未动。

要求:三线并行,提交各自独立,互不影响。整条流水线长这样:

一仓三线全流程:主工作区与两个分身各自开发提交,汇流到 feature-V4.0.0 收齐三线成果

开工:一句人话,或者三条命令

AI 时代的开工方式,是对着项目说句人话——「基于当前内容建一个 worktree,不包含未提交的内容」。会话自动触发了技能库里的 using-git-worktrees,按「原生工具优先、手动兜底」的路由走完全程,你说人话,skill 管路由:

一句自然语言触发 skill 的路由决策:先判断是否已在 worktree,再判断有无原生工具,有则 EnterWorktree 全自动接管,无则手动 git worktree 兜底,最后装依赖跑基线测试开工

不用 AI 的话,等价的是三条命令(效果完全一样):

# 基于当前所在分支(feature-V4.0.0)的顶端,各开一个任务分支 + 独立工作区
git worktree add ../myproject-formula  -b feature-V4.0.0-formula
git worktree add ../myproject-cashflow -b feature-V4.0.0-cashflow

# 验证:应该列出三行——主工作区 + 两个新分身
git worktree list

最常被问的问题:主工作区那四十多个未提交的文件会受影响吗?不会。worktree add 只做两件事:往 .git/worktrees/ 里写登记信息,在仓库外面新建目录。主工作区一个字节都不动——主工作区本身就是第三条线,继续手头特性。

顺带一提:AI 建完汇报「已基于当前分支最新提交建好」时,别全信——Claude Code 的默认基准是 origin/HEAD(仓库默认分支),git 原生命令的默认基准才是当前 HEAD。到新目录跑一句 git log --oneline -1 验一眼再放行,我就实际抓到过汇报和事实不符的一次:

基准翻车现场:新分身实际基于 origin/master 顶端(实线),AI 汇报却声称基于 feature-V4.0.0 顶端(虚线),到新目录跑 git log 即刻穿帮

这只是开工时的一个验证动作,不算关卡。真正的关卡有两道,都在「分身能干活」这件事上。

第一关:分身会「失忆」

新 worktree 是一次干净的检出——被 .gitignore 的东西,全都不在。而我的项目把任务方案文档(dev-doc/、doc/)、AI 的项目规范(.claude/、.agents/,含构建说明、二十来条编码规则、术语表)全部 gitignore 了。也就是说,AI 一进新 worktree,两眼一抹黑:不知道怎么编译、不知道术语、手里连任务方案都没有。

AI 时代用 worktree,迁移成本的大头往往不是 node_modules,而是这些不上库的「AI 上下文」。老教程只会教你复制 .env、重装依赖,没人告诉你 CLAUDE.md 和任务文档才是新工作区的命根子:

# 手动建的话,把 gitignore 掉的 AI 上下文带过去(每个 worktree 一份)
cp -R dev-doc doc .claude .agents .mcp.json ../myproject-formula/

用 Claude Code 的话这一步可以自动化:仓库根目录放一个 .worktreeinclude,写上 dev-doc/、.claude/,它建 worktree 时会自动复制。

这里藏着 worktree 最核心的一条分界线,值得单独讲透——已提交的,所有分身瞬间共享;未提交的,一样都不会跟过去。

  • 工作区改动(改了没 add):不带——只存在于那个目录的文件里;
  • 暂存区(add 了没 commit):不带——每个 worktree 有自己独立的 index 文件;
  • untracked / gitignored 文件:不带;
  • 例外是 stash:它存在共享引用里,任何 worktree 都取得出来。

因为 worktree add 只认 commit:它从对象库把目标提交的快照原样铺进新目录,从头到尾不读别的 worktree 的任何状态。

更深一层看,「共不共享」的判定标准其实不是数据存在哪,而是有没有引用链指向它。git add 的那一刻,文件内容就已经写进共享对象库了——但唯一的指针是本 worktree 私有的 index 条目,别的分身够不着,没有 commit 指向它,迟早被垃圾回收。commit 和 stash 之所以能共享,是因为它们都创建了对象并挂在引用上(stash 本质是挂在共享引用上的一串 commit)。想把半成品带去新 worktree,三条正路:任务分支上先提个 WIP commit 回头整理、stash、或者导 patch。

共享层与私有层的分界线:commit 和 stash 因有引用链而全分身可见,工作区改动、暂存区、untracked 只属于本工作区

第二关:测试跑不起来,凶手是缓存

上下文就位了,真正的深坑在跑单测时现身:同一时段,主工作区测试能跑,新 worktree 报配置缺失。

先撞的是参数。单测的 JVM 参数(app.id、环境标识、密钥)常年躺在主工作区 pom 的本地魔改里,不会跟过去,测试连不上公司环境。会话们各自挣扎出三条野路子:

  • 从 IDEA 运行配置的 XML 里把参数挖出来用;
  • 按项目文档在命令行传 -DargLine,参数不落盘;
  • 以及一次把密钥明文写上命令行——反面教材,密钥从此活在会话日志里。

最后收敛成正经解法:参数进文档、规则进 AI 记忆,下个 worktree 冷启动即得——上下文工程做对了,人才真的可以什么都不懂。(这类被跟踪文件的本地改动,git 还有个 skip-worktree 开关能让它从 git 视线里消失,进阶用法。)

再深挖,真凶是缓存:配置中心域名在这台机器上 DNS 根本解析不了——主工作区一直靠构建产物里的历史缓存在续命,新 worktree 没有缓存,真相才暴露。你以为的「能跑」和真实的「为什么能跑」,中间隔着一个缓存。

缓存续命真相:配置中心域名本机 DNS 解析失败,主工作区靠 target 里的历史缓存一直续命,新 worktree 无缓存真相当场暴露

合流:三条线收一条

三线各自开发、测试全绿之后,收尾是水到渠成的:每个分身各自提交(提交全部落在共享仓库里,主工作区零延迟可见),然后合流——推任务分支走 MR 是正式路线;快路线是在主工作区把两个任务分支依次 merge 进主线(三线文件零交集,合并干净),推远端:

git switch feature-V4.0.0         # 主工作区回到主线
git merge feature-V4.0.0-formula
git merge feature-V4.0.0-cashflow

这套命令在主工作区敲就行——merge 消费的是提交,不是目录:分身目录还在不在、在哪,都不影响合并,提交在,一切就在。这也反过来解释了为什么分身里的活必须先 commit:未提交的改动,merge 根本不认。

不敲命令也行,这是同一条路的 GUI 版:IDEA 用户打开 Git 日志视图,找到任务分支最新的提交,右键 → 底部的「分支 xxx」→「将 xxx 合并到 feature-V4.0.0 中」,点两下鼠标。我这次的三线合流,最后就是在界面上点完的:

IDEA 的本地合并入口:Git 日志视图右键任务分支的提交,在分支子菜单里选择合并到目标分支

用完的 worktree 是消耗品:git worktree remove 拆目录、git branch -d 删已合并分支;用 Claude Code 的话,退出会话时它检测到无未提交改动、无新提交,会提示自动清理。

多线并行还有几处会互相踩脚,提前知道就能避开。本地仓库 ~/.m2 是所有 worktree 共享的,一边 install 的 jar 可能被另一边的构建悄悄覆盖,所以构建别 install、带上 -am;测试库也是共享的,你会看到对方会话造的数据,造数时用动态前缀编码加事务回滚就互不干扰;还有,两边的 JVM 如果用相同的路由 tag,会抢端口,错峰起服务就行。

第三幕·沉淀:速查与清醒

坑速查表

坑一句话解法
worktree 目录嵌在仓库里还提交了放仓库外,或先 gitignore
rm -rf 删了目录,git 还认git worktree remove;已 rm 的跑 prune
构建产物每个分身一份接受 ×N,或错峰构建
同分支互斥报 already used保护不是 bug,给分身独立分支
被跟踪文件的本地魔改skip-worktree + patch 重放;正解是搬进 untracked 文件
新分身连不上测试环境参数走命令行,环境依赖查缓存
想双开又怕冲突先确认文件级零交集;merge 的冲突它不包

清醒点:并行不是免费的

worktree 解决的是「同时开工」,解决不了「同时完工」。

三线并行意味着三倍 review 负担、三份合并风险、三倍上下文切换。AI 写得再快,瓶颈最后还是坐在屏幕前拍板的你。任务之间有依赖、改动面重叠大、或者一个人根本 review 不过来的时候,老实排队串行,比开三个分身更专业。

工具也一样:Claude Code 原生的 --worktree 已经覆盖最常见场景,先用原生的;worktree 多到需要看板管理了,再去看 Worktrunk、Conductor 这些工具不迟。

一句话收束:一个 .git 可以挂 N 个工作区,各自 HEAD、各自分支、各自提交;它买来的是过程隔离,买不来免冲突。 一个等了十一年的老功能,终于等到能把它用出花样的时代。

延伸阅读:上一篇《探针 1|为什么你每次问大模型,答案都不一样》 https://mp.weixin.qq.com/s/72whbs2w_Afrt-TUJrwOWg

这个话题还有

▶ 交互版 · 横竖屏可选 · 网页直接看 在线观看
📺 视频版 · 约 8 分 36 秒 已制作,即将上线 B站 · 视频号 · 抖音 · 小红书
双击或滚轮缩放 · 拖动平移 · Esc 关闭