让 Codex 并行开发
Git Worktree + Squash + Codex 并行开发实战,用 Git Worktree 隔离多个并行任务,每个任务独立用 Codex 开发,完成后 Squash 成干净提交再合并。一套让 AI 编程真正跑得起来的工作流。
你有没有遇到过这种情况:Codex 正在改 A 功能,突然来了个紧急 bug 要修,但切分支会打断它,stash 又怕丢东西?
这篇教程解决的就是这个问题:让多个 Codex 任务同时跑,互不干扰。
一句话概括
Git Worktree 隔离现场,Codex 并行干活,Squash 整理提交。
三个工具各管一件事:
<svg viewBox="0 0 720 200" xmlns="http://www.w3.org/2000/svg" font-family="system-ui, sans-serif">
<!-- Worktree -->
<rect x="20" y="50" width="200" height="120" rx="14" fill="#EFF6FF" stroke="#3B82F6" stroke-width="2"/>
<text x="120" y="90" text-anchor="middle" font-weight="bold" font-size="16" fill="#1E40AF">Git Worktree</text>
<text x="120" y="118" text-anchor="middle" font-size="13" fill="#374151">隔离多个工作目录</text>
<text x="120" y="140" text-anchor="middle" font-size="12" fill="#6B7280">同一仓库,不同分支</text>
<text x="120" y="158" text-anchor="middle" font-size="12" fill="#6B7280">各自独立的文件现场</text>
<!-- Arrow -->
<path d="M235 110 L265 110" stroke="#9CA3AF" stroke-width="2" marker-end="url(#arr)"/>
<!-- Codex -->
<rect x="280" y="50" width="180" height="120" rx="14" fill="#ECFDF5" stroke="#10B981" stroke-width="2"/>
<text x="370" y="90" text-anchor="middle" font-weight="bold" font-size="16" fill="#065F46">Codex 并行</text>
<text x="370" y="118" text-anchor="middle" font-size="13" fill="#374151">多个任务同时开工</text>
<text x="370" y="140" text-anchor="middle" font-size="12" fill="#6B7280">终端 A → 任务 1</text>
<text x="370" y="158" text-anchor="middle" font-size="12" fill="#6B7280">终端 B → 任务 2</text>
<!-- Arrow -->
<path d="M475 110 L505 110" stroke="#9CA3AF" stroke-width="2" marker-end="url(#arr)"/>
<!-- Squash -->
<rect x="520" y="50" width="180" height="120" rx="14" fill="#FFF7ED" stroke="#F59E0B" stroke-width="2"/>
<text x="610" y="90" text-anchor="middle" font-weight="bold" font-size="16" fill="#92400E">Squash 合并</text>
<text x="610" y="118" text-anchor="middle" font-size="13" fill="#374151">压成干净的一条提交</text>
<text x="610" y="140" text-anchor="middle" font-size="12" fill="#6B7280">历史清爽可读</text>
<text x="610" y="158" text-anchor="middle" font-size="12" fill="#6B7280">PR 一目了然</text>
<defs>
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#9CA3AF"/>
</marker>
</defs>
</svg>
核心原理
Git Worktree:不是复制,是开窗
很多人第一次听到 worktree 会觉得是"复制一份项目"。其实不是。
Worktree 的本质是:让同一个 Git 仓库同时拥有多个独立的工作目录。
<svg viewBox="0 0 600 340" xmlns="http://www.w3.org/2000/svg" font-family="system-ui, sans-serif">
<!-- Shared .git -->
<rect x="180" y="10" width="240" height="60" rx="10" fill="#FEE2E2" stroke="#EF4444" stroke-width="1.5"/>
<text x="300" y="48" text-anchor="middle" font-size="14" font-weight="bold" fill="#991B1B">共享同一个 .git 仓库</text>
<!-- Lines from .git to worktrees -->
<line x1="250" y1="70" x2="140" y2="130" stroke="#D1D5DB" stroke-width="1.5" stroke-dasharray="5,3"/>
<line x1="300" y1="70" x2="300" y2="130" stroke="#D1D5DB" stroke-width="1.5" stroke-dasharray="5,3"/>
<line x1="350" y1="70" x2="460" y2="130" stroke="#D1D5DB" stroke-width="1.5" stroke-dasharray="5,3"/>
<!-- Main worktree -->
<rect x="40" y="130" width="200" height="180" rx="10" fill="#EFF6FF" stroke="#3B82F6" stroke-width="2"/>
<text x="140" y="160" text-anchor="middle" font-size="13" font-weight="bold" fill="#1E40AF">主 worktree</text>
<text x="140" y="182" text-anchor="middle" font-size="12" fill="#6B7280">aurora-web/</text>
<text x="140" y="204" text-anchor="middle" font-size="12" fill="#6B7280">分支: main</text>
<rect x="60" y="220" width="160" height="70" rx="6" fill="#DBEAFE" stroke="#93C5FD" stroke-width="1"/>
<text x="140" y="242" text-anchor="middle" font-size="11" fill="#1E3A8A">src/</text>
<text x="140" y="258" text-anchor="middle" font-size="11" fill="#1E3A8A">package.json</text>
<text x="140" y="274" text-anchor="middle" font-size="11" fill="#1E3A8A">node_modules/</text>
<!-- Worktree A -->
<rect x="210" y="130" width="200" height="180" rx="10" fill="#ECFDF5" stroke="#10B981" stroke-width="2"/>
<text x="310" y="160" text-anchor="middle" font-size="13" font-weight="bold" fill="#065F46">worktree A</text>
<text x="310" y="182" text-anchor="middle" font-size="12" fill="#6B7280">aurora-fix-login/</text>
<text x="310" y="204" text-anchor="middle" font-size="12" fill="#6B7280">分支: fix/login-bug</text>
<rect x="230" y="220" width="160" height="70" rx="6" fill="#D1FAE5" stroke="#6EE7B7" stroke-width="1"/>
<text x="310" y="242" text-anchor="middle" font-size="11" fill="#064E3B">src/auth.ts</text>
<text x="310" y="258" text-anchor="middle" font-size="11" fill="#064E3B">改动: +12 -3</text>
<text x="310" y="274" text-anchor="middle" font-size="11" fill="#064E3B">不影响主目录</text>
<!-- Worktree B -->
<rect x="380" y="130" width="200" height="180" rx="10" fill="#FFF7ED" stroke="#F59E0B" stroke-width="2"/>
<text x="480" y="160" text-anchor="middle" font-size="13" font-weight="bold" fill="#92400E">worktree B</text>
<text x="480" y="182" text-anchor="middle" font-size="12" fill="#6B7280">aurora-dark-mode/</text>
<text x="480" y="204" text-anchor="middle" font-size="12" fill="#6B7280">分支: feat/dark-mode</text>
<rect x="400" y="220" width="160" height="70" rx="6" fill="#FEF3C7" stroke="#FCD34D" stroke-width="1"/>
<text x="480" y="242" text-anchor="middle" font-size="11" fill="#78350F">src/theme.ts</text>
<text x="480" y="258" text-anchor="middle" font-size="11" fill="#78350F">改动: +45 -8</text>
<text x="480" y="274" text-anchor="middle" font-size="11" fill="#78350F">不影响主目录</text>
</svg>
它隔离什么:
- 代码文件:各目录独立,改了 A 不会出现在 B
- 当前分支:每个 worktree 挂不同分支
- 暂存区和未提交改动:完全隔离
- node_modules、构建产物、本地 .env
它不自动隔离什么:
- 端口:两个后端都用 3000 端口会撞车
- Redis / 数据库:指向同一个实例就共享状态
- 浏览器登录态
一句话:worktree 隔离的是文件和 Git 状态,不隔离系统资源。跑两套服务记得换端口。
Squash:整理提交的橡皮擦
Codex 会帮你做很多小提交——修一行、跑测试、改个配置,每次一个 commit。这在开发过程中完全正常,但合并进主分支时,你不会想要这堆碎提交。
Squash 把一整条分支的提交压成一条,保留最终代码,去掉中间过程。
<svg viewBox="0 0 600 260" xmlns="http://www.w3.org/2000/svg" font-family="system-ui, sans-serif">
<!-- Before squash -->
<text x="140" y="25" text-anchor="middle" font-size="14" font-weight="bold" fill="#374151">Squash 前</text>
<circle cx="60" cy="70" r="8" fill="#3B82F6"/>
<text x="60" y="60" text-anchor="middle" font-size="10" fill="#6B7280">main</text>
<line x1="68" y1="70" x2="112" y2="70" stroke="#D1D5DB" stroke-width="2"/>
<circle cx="120" cy="70" r="8" fill="#10B981"/>
<line x1="128" y1="70" x2="172" y2="70" stroke="#D1D5DB" stroke-width="2"/>
<circle cx="180" cy="70" r="8" fill="#10B981"/>
<line x1="188" y1="70" x2="232" y2="70" stroke="#D1D5DB" stroke-width="2"/>
<circle cx="240" cy="70" r="8" fill="#10B981"/>
<line x1="248" y1="70" x2="252" y2="70" stroke="#D1D5DB" stroke-width="2"/>
<text x="120" y="100" text-anchor="middle" font-size="10" fill="#6B7280">fix typo</text>
<text x="180" y="100" text-anchor="middle" font-size="10" fill="#6B7280">add API</text>
<text x="240" y="100" text-anchor="middle" font-size="10" fill="#6B7280">fix test</text>
<!-- Arrow -->
<text x="300" y="75" text-anchor="middle" font-size="24" fill="#9CA3AF">→</text>
<!-- After squash -->
<text x="440" y="25" text-anchor="middle" font-size="14" font-weight="bold" fill="#374151">Squash 后</text>
<circle cx="380" cy="70" r="8" fill="#3B82F6"/>
<text x="380" y="60" text-anchor="middle" font-size="10" fill="#6B7280">main</text>
<line x1="388" y1="70" x2="432" y2="70" stroke="#D1D5DB" stroke-width="2"/>
<circle cx="440" cy="70" r="10" fill="#F59E0B"/>
<text x="440" y="100" text-anchor="middle" font-size="10" fill="#92400E" font-weight="bold">feat: add user API</text>
<text x="440" y="114" text-anchor="middle" font-size="10" fill="#6B7280">一条干净的提交</text>
<!-- Detail section -->
<rect x="40" y="140" width="520" height="100" rx="8" fill="#F9FAFB" stroke="#E5E7EB" stroke-width="1"/>
<text x="60" y="168" font-size="12" fill="#374151" font-weight="bold">为什么要在合并前 Squash?</text>
<text x="60" y="190" font-size="12" fill="#6B7280">• 主分支历史只保留有意义的功能提交,不夹杂调试过程</text>
<text x="60" y="208" font-size="12" fill="#6B7280">• git revert 时可以整条功能一次性回滚</text>
<text x="60" y="226" font-size="12" fill="#6B7280">• PR 审查时看到的是一条完整变更,而不是 20 条碎片</text>
</svg>
把这张图放到实际的分支合并场景里,就是这样的:
<svg viewBox="0 0 720 360" xmlns="http://www.w3.org/2000/svg" font-family="system-ui, sans-serif">
<!-- Title -->
<text x="360" y="25" text-anchor="middle" font-size="15" font-weight="bold" fill="#374151">两条分支并行开发,各自 squash 合并回 main</text>
<!-- main line -->
<line x1="60" y1="80" x2="660" y2="80" stroke="#3B82F6" stroke-width="3"/>
<circle cx="60" cy="80" r="8" fill="#3B82F6"/>
<text x="60" y="70" text-anchor="middle" font-size="11" fill="#6B7280">开始</text>
<circle cx="660" cy="80" r="10" fill="#3B82F6"/>
<text x="660" y="70" text-anchor="middle" font-size="11" fill="#1E40AF" font-weight="bold">main (最新)</text>
<!-- Branch A: fix/password-validate -->
<line x1="160" y1="80" x2="190" y2="140" stroke="#10B981" stroke-width="2"/>
<line x1="190" y1="140" x2="250" y2="140" stroke="#10B981" stroke-width="2"/>
<circle cx="190" cy="140" r="6" fill="#10B981"/>
<circle cx="250" cy="140" r="6" fill="#10B981"/>
<line x1="250" y1="140" x2="310" y2="140" stroke="#10B981" stroke-width="2"/>
<circle cx="310" cy="140" r="6" fill="#10B981"/>
<text x="250" y="170" text-anchor="middle" font-size="11" fill="#065F46">fix/password-validate</text>
<text x="250" y="186" text-anchor="middle" font-size="10" fill="#6B7280">fix typo → add API → fix test</text>
<!-- Squash merge arrow A -->
<path d="M310 140 Q360 140 360 80" stroke="#F59E0B" stroke-width="2.5" fill="none" stroke-dasharray="6,3"/>
<polygon points="360,85 355,95 365,95" fill="#F59E0B"/>
<text x="350" y="125" font-size="10" fill="#92400E" font-weight="bold">squash merge</text>
<!-- Squash commit on main -->
<circle cx="380" cy="80" r="10" fill="#F59E0B"/>
<text x="380" y="105" text-anchor="middle" font-size="10" fill="#92400E">fix: 密码校验修复</text>
<!-- Branch B: feat/dark-mode -->
<line x1="380" y1="80" x2="420" y2="200" stroke="#8B5CF6" stroke-width="2"/>
<line x1="420" y1="200" x2="480" y2="200" stroke="#8B5CF6" stroke-width="2"/>
<circle cx="420" cy="200" r="6" fill="#8B5CF6"/>
<circle cx="480" cy="200" r="6" fill="#8B5CF6"/>
<line x1="480" y1="200" x2="540" y2="200" stroke="#8B5CF6" stroke-width="2"/>
<circle cx="540" cy="200" r="6" fill="#8B5CF6"/>
<text x="480" y="230" text-anchor="middle" font-size="11" fill="#6D28D9">feat/dark-mode</text>
<text x="480" y="246" text-anchor="middle" font-size="10" fill="#6B7280">theme CSS → toggle btn → localStorage</text>
<!-- Squash merge arrow B -->
<path d="M540 200 Q590 200 590 80" stroke="#F59E0B" stroke-width="2.5" fill="none" stroke-dasharray="6,3"/>
<polygon points="590,85 585,95 595,95" fill="#F59E0B"/>
<text x="575" y="160" font-size="10" fill="#92400E" font-weight="bold">squash merge</text>
<!-- Squash commit on main B -->
<circle cx="610" cy="80" r="10" fill="#F59E0B"/>
<text x="610" y="105" text-anchor="middle" font-size="10" fill="#92400E">feat: 深色模式</text>
<!-- Legend -->
<rect x="140" y="275" width="440" height="70" rx="8" fill="#F9FAFB" stroke="#E5E7EB" stroke-width="1"/>
<circle cx="170" cy="300" r="6" fill="#3B82F6"/>
<text x="185" y="304" font-size="11" fill="#374151">main 分支</text>
<circle cx="310" cy="300" r="6" fill="#10B981"/>
<text x="325" y="304" font-size="11" fill="#374151">功能分支 A</text>
<circle cx="440" cy="300" r="6" fill="#8B5CF6"/>
<text x="455" y="304" font-size="11" fill="#374151">功能分支 B</text>
<circle cx="550" cy="300" r="8" fill="#F59E0B"/>
<text x="565" y="304" font-size="11" fill="#92400E" font-weight="bold">squash 合并</text>
<text x="360" y="332" text-anchor="middle" font-size="11" fill="#6B7280">两条分支各自独立开发,squash 后 main 只看到两条干净的提交</text>
</svg>
Codex:真正的劳动力
Codex 不是"你让它写一个函数"就完了。在这套工作流里,你给它一个完整的任务描述,它在一个隔离的 worktree 里独立完成整个开发循环:改代码、跑测试、修错误、提 PR。
你需要做的,只是定义任务和审查结果。
完整实战流程
假设你有一个前端项目 aurora-web,现在要并行推进两件事:
- 任务 A:修复登录页的密码校验 bug
- 任务 B:给设置页加上深色模式
第一步:创建两个 Worktree
cd D:\Project\github\aurora-web
git fetch origin
# 任务 A:修 bug
git worktree add -b fix/password-validate ..\aurora-fix-password origin/main
# 任务 B:新功能
git worktree add -b feat/dark-mode ..\aurora-dark-mode origin/main
执行完后,你的磁盘上会有三个目录:
D:\Project\github\
aurora-web/ ← 主目录(main 分支)
aurora-fix-password/ ← worktree A(fix/password-validate 分支)
aurora-dark-mode/ ← worktree B(feat/dark-mode 分支)
三个目录共享同一个 Git 历史,但文件、分支、改动完全隔离。
第二步:给每个任务独立的数据库和端口
这一步容易忘,但非常重要。两个 worktree 如果同时跑服务,端口和数据库必须分开。
# 任务 A:端口 3000
cd D:\Project\github\aurora-fix-password
$env:SQLITE_PATH="D:\Project\github\aurora-fix-password\local.db"
npm run dev # 默认跑在 3000
# 任务 B:端口 3010
cd D:\Project\github\aurora-dark-mode
$env:SQLITE_PATH="D:\Project\github\aurora-dark-mode\local.db"
$env:PORT="3010"
npm run dev # 跑在 3010
前端同理:任务 A 用
5173,任务 B 换5174,分别指向各自的后端端口。
第三步:在两个终端里分别启动 Codex
打开两个终端窗口,分别进入各自的 worktree,让 Codex 干活。
快捷操作: 在 Codex 界面中按
Ctrl + Shift + N可以直接打开新窗口,无需手动再开一个终端。每个窗口各自进入不同的 worktree 目录,就能同时跑多个独立的对话任务。
终端 A — 修 bug:
cd D:\Project\github\aurora-fix-password
codex
在 Codex 里描述任务:
登录页的密码校验有 bug:当密码包含特殊字符
&和=时,校验会跳过长度检查。请在src/auth/validate.ts中修复,并补充对应的单元测试。
终端 B — 新功能:
cd D:\Project\github\aurora-dark-mode
codex
在 Codex 里描述任务:
给设置页添加深色模式切换。要求:1. 在
src/components/Settings.tsx添加切换按钮;2. 用 CSS 变量实现主题色;3. 记住用户选择到 localStorage。
两个 Codex 同时开工,互不干扰。
<svg viewBox="0 0 700 300" xmlns="http://www.w3.org/2000/svg" font-family="system-ui, sans-serif">
<!-- Terminal A -->
<rect x="20" y="20" width="320" height="260" rx="10" fill="#1F2937" stroke="#374151" stroke-width="1.5"/>
<rect x="20" y="20" width="320" height="30" rx="10" fill="#374151"/>
<circle cx="40" cy="35" r="5" fill="#EF4444"/>
<circle cx="55" cy="35" r="5" fill="#FBBF24"/>
<circle cx="70" cy="35" r="5" fill="#10B981"/>
<text x="180" y="40" text-anchor="middle" font-size="11" fill="#9CA3AF">终端 A — aurora-fix-password</text>
<text x="35" y="72" font-size="11" fill="#6EE7B7">lt;/text>
<text x="48" y="72" font-size="11" fill="#D1D5DB">codex</text>
<text x="35" y="95" font-size="11" fill="#FBBF24">▸ 修复密码校验 bug...</text>
<text x="35" y="115" font-size="11" fill="#6EE7B7">✓ 找到 src/auth/validate.ts 第 42 行</text>
<text x="35" y="135" font-size="11" fill="#6EE7B7">✓ 已修复特殊字符转义逻辑</text>
<text x="35" y="155" font-size="11" fill="#6EE7B7">✓ 补充 3 个单元测试用例</text>
<text x="35" y="175" font-size="11" fill="#FBBF24">▸ 跑测试中...</text>
<text x="35" y="200" font-size="11" fill="#9CA3AF">───────────────────</text>
<text x="35" y="220" font-size="11" fill="#D1D5DB">分支: fix/password-validate</text>
<text x="35" y="240" font-size="11" fill="#D1D5DB">改动: 2 files, +18 -5</text>
<text x="35" y="260" font-size="11" fill="#6B7280">状态: Codex 执行中...</text>
<!-- Terminal B -->
<rect x="360" y="20" width="320" height="260" rx="10" fill="#1F2937" stroke="#374151" stroke-width="1.5"/>
<rect x="360" y="20" width="320" height="30" rx="10" fill="#374151"/>
<circle cx="380" cy="35" r="5" fill="#EF4444"/>
<circle cx="395" cy="35" r="5" fill="#FBBF24"/>
<circle cx="410" cy="35" r="5" fill="#10B981"/>
<text x="520" y="40" text-anchor="middle" font-size="11" fill="#9CA3AF">终端 B — aurora-dark-mode</text>
<text x="375" y="72" font-size="11" fill="#6EE7B7">lt;/text>
<text x="388" y="72" font-size="11" fill="#D1D5DB">codex</text>
<text x="375" y="95" font-size="11" fill="#FBBF24">▸ 添加深色模式...</text>
<text x="375" y="115" font-size="11" fill="#6EE7B7">✓ 创建 src/styles/theme.css</text>
<text x="375" y="135" font-size="11" fill="#6EE7B7">✓ 修改 Settings.tsx 添加切换按钮</text>
<text x="375" y="155" font-size="11" fill="#6EE7B7">✓ localStorage 持久化已接入</text>
<text x="375" y="175" font-size="11" fill="#FBBF24">▸ 组件测试中...</text>
<text x="375" y="200" font-size="11" fill="#9CA3AF">───────────────────</text>
<text x="375" y="220" font-size="11" fill="#D1D5DB">分支: feat/dark-mode</text>
<text x="375" y="240" font-size="11" fill="#D1D5DB">改动: 4 files, +86 -12</text>
<text x="375" y="260" font-size="11" fill="#6B7280">状态: Codex 执行中...</text>
</svg>
第四步:审查、调整、继续
Codex 跑完后,切换到各自的 worktree 检查结果:
# 检查任务 A 的改动
cd D:\Project\github\aurora-fix-password
git log --oneline -5
git diff --stat origin/main
# 检查任务 B 的改动
cd D:\Project\github\aurora-dark-mode
git log --oneline -5
git diff --stat origin/main
如果需要调整,回到对应终端继续让 Codex 迭代。两个任务可以独立推进,不需要等另一个完成。
第五步:Squash 合并
两个任务都完成后,分别 squash 成干净提交,合并回 main:
# 回到主目录
cd D:\Project\github\aurora-web
git checkout main
git pull origin main
# 合并任务 A(squash 成一条提交)
git merge --squash fix/password-validate
git commit -m "fix: 密码校验跳过特殊字符长度检查的问题"
# 合并任务 B(squash 成一条提交)
git merge --squash feat/dark-mode
git commit -m "feat: 设置页添加深色模式切换"
# 推送
git push origin main
如果你更习惯用 PR,也可以把每个 worktree 的分支推送上去,在 GitHub 上创建 PR 并勾选 Squash and merge:
# 推送任务 A 分支
cd D:\Project\github\aurora-fix-password
git push origin fix/password-validate
# 推送任务 B 分支
cd D:\Project\github\aurora-dark-mode
git push origin feat/dark-mode
第六步:清理 Worktree
任务合并后,及时清理 worktree:
cd D:\Project\github\aurora-web
# 移除 worktree
git worktree remove ..\aurora-fix-password
git worktree remove ..\aurora-dark-mode
# 删除已合并的分支
git branch -d fix/password-validate
git branch -d feat/dark-mode
不要手动删 worktree 目录,用
git worktree remove清理。手动删会留下残留引用,git worktree list会报异常。
完整流程总览
<svg viewBox="0 0 720 480" xmlns="http://www.w3.org/2000/svg" font-family="system-ui, sans-serif">
<!-- Step 1 -->
<circle cx="360" cy="30" r="18" fill="#3B82F6"/>
<text x="360" y="35" text-anchor="middle" font-size="13" fill="white" font-weight="bold">1</text>
<text x="360" y="65" text-anchor="middle" font-size="13" font-weight="bold" fill="#1E40AF">创建 Worktree</text>
<text x="360" y="83" text-anchor="middle" font-size="11" fill="#6B7280">git worktree add -b 分支名</text>
<line x1="360" y1="100" x2="360" y2="120" stroke="#D1D5DB" stroke-width="2" marker-end="url(#arr2)"/>
<!-- Step 2 -->
<circle cx="360" cy="138" r="18" fill="#3B82F6"/>
<text x="360" y="143" text-anchor="middle" font-size="13" fill="white" font-weight="bold">2</text>
<text x="360" y="173" text-anchor="middle" font-size="13" font-weight="bold" fill="#1E40AF">隔离资源</text>
<text x="360" y="191" text-anchor="middle" font-size="11" fill="#6B7280">换端口、换数据库路径</text>
<line x1="360" y1="208" x2="360" y2="228" stroke="#D1D5DB" stroke-width="2" marker-end="url(#arr2)"/>
<!-- Step 3 - parallel split -->
<circle cx="360" cy="246" r="18" fill="#10B981"/>
<text x="360" y="251" text-anchor="middle" font-size="13" fill="white" font-weight="bold">3</text>
<text x="360" y="281" text-anchor="middle" font-size="13" font-weight="bold" fill="#065F46">Codex 并行开发</text>
<!-- Left branch -->
<line x1="342" y1="288" x2="180" y2="310" stroke="#10B981" stroke-width="2"/>
<rect x="80" y="310" width="200" height="50" rx="8" fill="#ECFDF5" stroke="#10B981" stroke-width="1.5"/>
<text x="180" y="332" text-anchor="middle" font-size="12" font-weight="bold" fill="#065F46">终端 A</text>
<text x="180" y="350" text-anchor="middle" font-size="11" fill="#6B7280">修密码 bug</text>
<!-- Right branch -->
<line x1="378" y1="288" x2="540" y2="310" stroke="#10B981" stroke-width="2"/>
<rect x="440" y="310" width="200" height="50" rx="8" fill="#ECFDF5" stroke="#10B981" stroke-width="1.5"/>
<text x="540" y="332" text-anchor="middle" font-size="12" font-weight="bold" fill="#065F46">终端 B</text>
<text x="540" y="350" text-anchor="middle" font-size="11" fill="#6B7280">加深色模式</text>
<!-- Converge -->
<line x1="180" y1="360" x2="342" y2="380" stroke="#10B981" stroke-width="2"/>
<line x1="540" y1="360" x2="378" y2="380" stroke="#10B981" stroke-width="2"/>
<!-- Step 4 -->
<circle cx="360" cy="398" r="18" fill="#F59E0B"/>
<text x="360" y="403" text-anchor="middle" font-size="13" fill="white" font-weight="bold">4</text>
<text x="360" y="433" text-anchor="middle" font-size="13" font-weight="bold" fill="#92400E">Squash 合并</text>
<text x="360" y="451" text-anchor="middle" font-size="11" fill="#6B7280">git merge --squash → 干净提交 → git push</text>
<defs>
<marker id="arr2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#D1D5DB"/>
</marker>
</defs>
</svg>
其实,一句话就够了
上面六步是为了让你理解每一步在做什么。实际使用时,你不需要手动操作任何一步。
打开 Codex,用一句话告诉它:
帮我基于 main 分支,创建一个 worktree,开发深色模式功能。
Codex 会自动完成:创建 worktree、建分支、写代码、跑测试、提交代码。你只需要坐在旁边喝杯咖啡。
开发完成后,每个窗口各自提一个 PR 合并回 main,就完成了并行开发。
<svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" font-family="system-ui, sans-serif">
<!-- You -->
<rect x="220" y="10" width="280" height="70" rx="12" fill="#FEF3C7" stroke="#F59E0B" stroke-width="2"/>
<text x="360" y="40" text-anchor="middle" font-size="15" font-weight="bold" fill="#92400E">你说一句话</text>
<text x="360" y="62" text-anchor="middle" font-size="13" fill="#78350F">"基于 main 创建 worktree,开发 xx 功能"</text>
<!-- Arrow down -->
<line x1="360" y1="80" x2="360" y2="110" stroke="#D1D5DB" stroke-width="2" marker-end="url(#arr3)"/>
<!-- Codex does everything -->
<rect x="160" y="115" width="400" height="55" rx="10" fill="#ECFDF5" stroke="#10B981" stroke-width="2"/>
<text x="360" y="140" text-anchor="middle" font-size="14" font-weight="bold" fill="#065F46">Codex 全自动完成</text>
<text x="360" y="158" text-anchor="middle" font-size="12" fill="#6B7280">创建 worktree → 建分支 → 写代码 → 跑测试 → 提交</text>
<!-- Arrow down -->
<line x1="360" y1="170" x2="360" y2="200" stroke="#D1D5DB" stroke-width="2" marker-end="url(#arr3)"/>
<!-- PR -->
<rect x="220" y="205" width="280" height="55" rx="10" fill="#EFF6FF" stroke="#3B82F6" stroke-width="2"/>
<text x="360" y="230" text-anchor="middle" font-size="14" font-weight="bold" fill="#1E40AF">每个窗口提 PR 合并回 main</text>
<text x="360" y="248" text-anchor="middle" font-size="12" fill="#6B7280">并行开发完成</text>
<defs>
<marker id="arr3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#D1D5DB"/>
</marker>
</defs>
</svg>
你需要做的事情只有一件:告诉 Codex 做什么。 剩下的交给它。
常见问题
两个 worktree 能同时跑同一个服务吗?
可以,但必须换端口。不换端口会报 EADDRINUSE,因为两个进程抢同一个端口。
Worktree 占多少磁盘?
不多。它只多一份工作文件,不复制 .git 仓库。通常比完整 git clone 少占一半以上空间。
可以在同一个分支上建两个 worktree 吗?
不可以。同一个分支只能被一个 worktree 检出。这是 Git 的硬限制,防止两个目录同时修改同一个分支造成冲突。
Codex 能感知到 worktree 里的其他分支吗?
能。Codex 访问的是同一个 .git,所以 git log、git diff 都能看到全部分支。但 Codex 的文件操作只影响当前 worktree 的工作目录。
推荐几个 worktree 同时跑?
日常开发 2-3 个就够。超过 5 个时终端窗口管理会变麻烦,不如用 CI 或任务队列来调度。
适用场景
| 场景 | 适合用 Worktree 并行吗 |
|---|---|
| 修一个紧急 bug,同时在开发新功能 | ✅ 最典型场景 |
| 两个不相关的功能并行开发 | ✅ 完全隔离 |
| 一个功能需要前后端同时改 | ⚠️ 可以,但要协调端口 |
| 同一个文件的两种方案对比 | ✅ 开两个 worktree 各试一套 |
| 代码审查 / CI 调试 | ✅ 不影响当前工作 |
总结
这套工作流的精髓不在某个单一工具,而是三者的组合:
- Worktree 负责隔离:多任务并行,文件和分支互不污染
- Codex 负责执行:每个任务在独立环境里自主完成开发循环
- Squash 负责整理:把 Codex 产生的碎提交压缩成干净的合并记录
用熟以后,你的节奏会变成:定义任务 → 启动 Codex → 去做别的事 → 回来审查 → 合并推送。
代码在并行推进,你的注意力不用被钉死在一个终端上。