一个会话在改 12 个文件,另一个会话也在改同一批文件——这不叫并行,叫互相拆台。真正能并行的,是"共享仓库历史、但不共享工作区"的任务。git worktree 恰好提供这个单位:同一份 .git,多个独立工作目录,各自的 HEAD。
它解决的是隔离问题,不解决冲突问题。分不清这两件事,开三个窗口只会换来三次手忙脚乱。
1. 串行为什么慢:先分清四种瓶颈
| 瓶颈 | 表现 | 并行能不能解 |
|---|---|---|
| 上下文污染 | 会话 A 的改动被会话 B 看到,判断跟着跑偏 | 能(独立工作区 + 独立会话) |
| 纯等待 | 一次只跑一件事,人和机器都在等 | 能 |
| 合并冲突 | 两个会话改同一文件,最后合并打架 | 不能,要靠任务切分 |
| 环境竞争 | 抢同一个端口、同一个库、同一个构建目录 | 不能,要靠资源隔离 |
| 依赖安装 | 每个工作区都要装一遍 | 不能,只能优化 |
所以并行的收益来自前两项,成本来自后三项。收益是线性的,成本是超线性的——这就是为什么"能并行三个"和"并行三个更快"是两件事。
2. git worktree 到底共享什么
| 项 | 是否共享 | 说明 |
|---|---|---|
| 对象库(提交、树、blob) | 共享 | 不会因为多开工作区而翻倍占盘 |
| 分支引用 | 共享 | 一个分支同时只能在一个工作区检出 |
| HEAD | 独立 | 每个工作区停在自己的分支 |
| 索引/暂存区 | 独立 | 各自的 git add 互不影响 |
| 配置与 hooks | 共享 | 来自同一份 .git/config |
未跟踪文件(node_modules、.env) |
独立 | 新工作区里是空的 |
git worktree add ../proj-feat-auth -b feat/auth
git worktree add ../proj-feat-api -b feat/api
git worktree list
/repo abc1234 [main]
/repo/../proj-feat-auth def5678 [feat/auth]
/repo/../proj-feat-api 0123abc [feat/api]
一条 git 的硬规则:同一个分支不能同时检出到两个工作区。想开第二个窗口干别的活,必须新开分支(-b),否则 git 直接拒绝。
3. 一个 worktree 一个会话
cd ../proj-feat-auth && claude
# 另一个终端
cd ../proj-feat-api && claude
每个会话拥有自己的上下文窗口,互不可见——这正是隔离的价值。但要记住三件"没有跟过来"的东西:
- 未提交的个人配置:
.claude/settings.local.json属于工作区里的文件,新 worktree 里没有; - 未跟踪的依赖:
node_modules、虚拟环境、构建缓存都是空的; - IDE 的索引:新目录需要单独打开,否则跳转会指回旧路径。
mkdir -p ../proj-feat-auth/.claude
cp .claude/settings.local.json ../proj-feat-auth/.claude/ 2>/dev/null || true
更好的做法是把团队规则放进已提交的 .claude/settings.json,个人差异才用 local 文件——这样新 worktree 天然就带着正确的权限配置。
4. 分支命名与合并策略
| 策略 | 做法 | 适合 | 代价 |
|---|---|---|---|
| 一任务一分支 | worktree add -b feat/<主题> |
独立功能、互不依赖 | 分支多,要勤清理 |
| 共享基线 + 各自 rebase | 都从 main 切,完成前 rebase |
想要线性历史 | rebase 会反复解冲突 |
| 集成分支 | 各自合到 integration,再整体进 main |
多人多任务同批上线 | 多一层合并 |
| 短生命周期 | 完成即合并、即删 worktree | 减少长期漂移 | 要求任务切得够小 |
命名建议 类型/域-动作:feat/api-pagination、fix/web-login-loop。名字里带上模块名,后面判断"这两个分支会不会撞"时省很多事。
合并顺序也有讲究:先合小改动、先合共享文件多的那个。共享文件先落地,后合并的分支只需要解一次冲突,而不是反复解。
5. 冲突预防:按文件与模块切
| 切分方式 | 例子 | 冲突风险 |
|---|---|---|
| 按目录/模块 | apps/web vs services/api |
低 |
| 按层 | 前端 / 后端 / 数据迁移 | 中(共享类型定义) |
| 按文件 | 一人一文件 | 低,但难规划 |
| 按功能纵切 | 一个功能从前端到库表 | 高(同文件多处改) |
纵切听起来最"完整",实际冲突最多:类型定义、路由表、i18n 文案、锁文件全是共享热点。
一条实践经验:共享文件指定唯一负责人。类型定义、路由注册、依赖锁文件,只允许一个 worktree 改,其他 worktree 需要时用 patch 交接,或等对方先合并。
git diff --name-only main...feat/auth | sort > /tmp/a.txt
git diff --name-only main...feat/api | sort > /tmp/b.txt
comm -12 /tmp/a.txt /tmp/b.txt # 交集就是冲突候选
这条命令值得在开工前跑一次。交集为空,可以放心并行;交集里有三四个文件,就先调整切分方式。
6. 资源竞争:端口、数据库、构建目录
| 资源 | 冲突表现 | 隔离做法 |
|---|---|---|
| 端口 | EADDRINUSE,或两个实例抢同一个 |
每个 worktree 一个 PORT,写进各自的 .env.local |
| 数据库 | 数据互相污染、迁移互踩 | 每 worktree 独立库名或独立 schema |
| 构建产物 | 产物互覆盖、缓存键冲突 | 独立 dist/、独立缓存目录 |
| 依赖 | node_modules 重装、lock 冲突 |
各自安装;lock 只在一处改 |
| 测试数据 | 固定 fixture 被改坏 | 每次运行生成随机前缀 |
| 日志文件 | 两个进程写同一个文件 | 日志路径带分支名 |
# 每个 worktree 的 .env.local(不进 git)
PORT=3101
DATABASE_URL=postgres://localhost:5432/app_feat_auth
CACHE_DIR=.cache/feat-auth
LOG_FILE=logs/feat-auth.log
数据库这条最容易出事:迁移只跑了一半的库,两个分支都读它,报错信息会指向完全无关的代码。开工前把 DATABASE_URL 写对,比事后 debug 便宜一百倍。
7. 清理与回收
| 命令 | 作用 | 注意 |
|---|---|---|
git worktree list |
列出所有工作区 | 定期看一眼,别积累 |
git worktree remove <path> |
删除工作区 | 有未提交改动会被拒绝,需先处理 |
git worktree prune |
清理失效元数据 | 目录被 rm -rf 后必须跑 |
git branch -d <name> |
删除已合并分支 | 用 -d 而不是 -D,让 git 帮你判断 |
git worktree remove ../proj-feat-auth
git worktree prune
git branch -d feat/auth
git worktree list
prune 是唯一容易忘的一步:直接 rm -rf 掉目录,git worktree list 里还会留着幽灵条目,之后想用同名路径重建就会报"已存在"。养成"先 remove、再 prune"的习惯。
8. 什么时候不该并行
| 场景 | 为什么 | 替代做法 |
|---|---|---|
| 强耦合改动(接口重构) | 每一步都依赖上一步的结果 | 串行,或先定接口再分头实现 |
| 需要频繁互看 | 两边的上下文本来就是互补的 | 合并成一个会话 |
| 同一文件的连续修改 | 必然冲突,且冲突在语义层 | 串行 |
| 方向未定的探索 | 还不知道要改哪些文件 | 先探明边界,再并行 |
| 单个小任务 | 开工作区 + 装依赖的开销超过收益 | 直接串行 |
| 需要真实用户数据验证 | 数据只有一份 | 串行,或用只读副本 |
一个常被忽略的判断:并行度应该受"你能同时审查多少产出"限制,而不是受机器限制。三个会话同时吐代码,人只有一双手 review,最后还是要排队。
9. 排错:六个高频坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 新工作区里命令全报 module not found | 依赖没装,node_modules 不共享 |
在新目录跑一次安装 |
| IDE 跳转到旧文件 | 索引指向主工作区 | 用 IDE 单独打开 worktree 目录,别打开父目录 |
| 测试互相污染 | 共用同一个库 | 独立库名/schema,或测试用事务回滚 |
fatal: 'feat/x' is already checked out |
同一分支被两个工作区占用 | 用 -b 新建分支 |
worktree list 里有不存在的路径 |
直接删了目录没 prune | git worktree prune |
| 提交到了错的分支 | 终端提示符没显示分支 | 提示符里带上分支名,提交前 git status |
第 2 条最费时间:IDE 打开的是父目录,go to definition 全跳到主工作区的同名文件上,改了半天发现改的不是运行的那份。一个 worktree 一个 IDE 窗口,是唯一不容易错的用法。
10. 边界:这些需求 worktree 解决不了
| 需求 | 更合适的做法 | 理由 |
|---|---|---|
| 同一文件的多处并行修改 | 不并行,或先拆分文件 | 语义冲突无法机械合并 |
| 系统级隔离(不同运行时版本) | 容器 / 虚拟机 | worktree 只隔离文件与分支 |
| 大规模并行构建/测试 | CI 的分布式 runner | 本机资源是瓶颈 |
| 需要共享同一份运行中的服务 | 起一份服务,多个客户端连 | 端口与状态只有一份 |
| 团队多人协作同一分支 | 远程分支 + 常规协作流程 | worktree 是本机概念 |
一句话:worktree 隔离的是"文件和分支",不是"进程和数据"。
11. 自测
- worktree 之间哪些东西共享、哪些独立?至少各说三项。
- 为什么同一个分支不能同时检出到两个 worktree,怎么绕开?
- 开工前用什么命令判断两个分支会撞车?
- 新 worktree 里最容易漏掉的三样东西是什么?
rm -rf删掉工作区目录后,git worktree list还显示它,怎么处理?- 哪些任务类型不该并行,判断依据是什么?