在实际开发中,我们经常会遇到这样的场景:

工作树并行 —— git worktree + 多 Codex 实例,同时修多个 Issue

简介

在实际开发中,我们经常会遇到这样的场景:

  • 有 3 个 Issue 需要修复,每个都需要在当前分支上工作
  • 一个紧急 Bug 需要立即修复,但你正在 feature 分支上开发
  • 需要同时运行多个 AI Agent 实例,各自处理不同的任务

传统的做法是什么?

  1. stash 当前工作 → 切到目标分支 → 修复 → commit → 切回来 → stash pop
  2. 或者干脆等一个任务完成后再开始下一个

这两种方式都有明显的问题:stash 流程繁琐且容易出错,串行处理浪费时间。

git worktree 是更好的解决方案。 它允许你从同一个仓库中创建多个独立的工作目录,每个目录可以处于不同的分支或 commit,互不干扰。

结合 Codex CLI,我们可以在每个 worktree 中运行一个独立的 AI Agent 实例,实现真正的并行开发。

一、git worktree 基础

1.1 什么是 worktree?

Git worktree 让你可以在同一个仓库的不同目录下,同时检出不同的分支。

bash
# 常规 Git:一个仓库 = 一个工作目录
my-repo/
  ├── .git/
  ├── src/
  └── tests/

# worktree:一个仓库 = 多个工作目录
my-repo/
  ├── .git/           # 共享的 Git 数据库
  ├── src/            # 主工作目录(main 分支)
  └── tests/

my-repo-fix-bug/      # worktree 1(fix/bug-123 分支)
  ├── .git            # 链接到主仓库
  ├── src/
  └── tests/

my-repo-new-feature/  # worktree 2(feat/new-api 分支)
  ├── .git            # 链接到主仓库
  ├── src/
  └── tests/

1.2 创建 worktree

bash
# 基于现有分支创建 worktree
cd /opt/data/my-project
git worktree add ../my-project-fix-bug fix/bug-123

# 基于当前分支创建并新建一个分支
git worktree add -b feat/new-feature ../my-project-feature HEAD

# 基于特定 commit 创建(只读模式)
git worktree add --detach ../my-project-review abc1234

# 指定目录和分支
git worktree add /tmp/my-project-hotfix hotfix/urgent-fix

1.3 查看和管理 worktree

bash
# 列出所有 worktree
git worktree list
# 输出示例:
# /opt/data/my-project          abc1234 [main]
# /opt/data/my-project-fix-bug  def5678 [fix/bug-123]
# /opt/data/my-project-feature  ghi9012 [feat/new-feature]

# 查看详细信息
git worktree list --porcelain

# 移除 worktree(先确保已提交)
git worktree remove ../my-project-fix-bug

# 清理已删除的 worktree 记录
git worktree prune

1.4 worktree 的核心优势

text
┌─────────────────────────────────────────────────────────┐
│                   git worktree 优势                      │
├─────────────────────────────────────────────────────────┤
│ ✅ 共享 .git 数据库,磁盘空间高效                         │
│ ✅ 各 worktree 完全独立,切换零开销                       │
│ ✅ 不需要 stash/pop,状态始终保存                         │
│ ✅ 可以同时运行多个构建/测试进程                          │
│ ✅ 每个 worktree 可以运行独立的 AI Agent 实例             │
│ ✅ 紧急任务随时处理,不干扰当前工作                       │
└─────────────────────────────────────────────────────────┘

二、多 Codex 实例并行

2.1 架构设计

text
主仓库 (/opt/data/my-project)
  ├── .git/              ← 共享 Git 数据库
  ├── src/               ← main 分支
  └── Codex CLI 实例 A    ← 处理 Issue #101

../my-project-worktree-1
  ├── .git               ← 链接到主仓库
  ├── src/               ← fix/auth-bug 分支
  └── Codex CLI 实例 B    ← 处理 Issue #102

../my-project-worktree-2
  ├── .git               ← 链接到主仓库
  ├── src/               ← fix/api-timeout 分支
  └── Codex CLI 实例 C    ← 处理 Issue #103

2.2 创建 worktree 并启动 Codex

python
import os
import time

PROJECT_ROOT = "/opt/data/my-project"

# 步骤 1:在主仓库中创建两个 worktree
worktrees = [
    {
        "name": "worktree-auth",
        "branch": "fix/auth-bug-101",
        "prompt": "修复 Issue #101:用户认证失败的问题。检查 src/auth.py 中的 token 验证逻辑。"
    },
    {
        "name": "worktree-api",
        "branch": "fix/api-timeout-102",
        "prompt": "修复 Issue #102:API 请求超时问题。检查 src/api/client.py 中的超时配置。"
    }
]

for wt in worktrees:
    # 创建 worktree 并创建新分支
    cmd = f"cd {PROJECT_ROOT} && git worktree add -b {wt['branch']} ../{wt['name']} HEAD"
    result = terminal(command=cmd)
    print(f"创建 worktree {wt['name']}: exit_code={result.exit_code}")

2.3 在每个 worktree 中启动 Codex 实例

python
# 步骤 2:在每个 worktree 中启动 Codex CLI 实例
codex_sessions = []

for wt in worktrees:
    worktree_path = f"/opt/data/{wt['name']}"

    # 使用 background + pty 启动 Codex
    session = terminal(
        command=f"cd {worktree_path} && codex exec -p \"{wt['prompt']}\"",
        background=True,
        pty=True,  # Codex 需要 PTY
        timeout=600,
        notify_on_complete=True
    )

    codex_sessions.append({
        "name": wt["name"],
        "branch": wt["branch"],
        "session_id": session.session_id,
        "prompt": wt["prompt"]
    })

    print(f"Codex 实例已启动: {wt['name']} (Session: {session.session_id})")

print(f"\n共启动 {len(codex_sessions)} 个 Codex 实例")

2.4 监控所有实例

python
# 步骤 3:监控所有 Codex 实例的执行进度
active = list(codex_sessions)

while active:
    for item in active[:]:
        status = process(action="poll", session_id=item["session_id"])

        if status.status != "running":
            print(f"\n{'='*60}")
            print(f"✅ {item['name']} ({item['branch']}) 已完成")
            print(f"{'='*60}")

            # 获取最终输出
            log = process(action="log", session_id=item["session_id"], limit=50)
            print(log)

            active.remove(item)

    if active:
        print(f"\n仍在运行: {', '.join(i['name'] for i in active)}")
        time.sleep(15)

print("\n🎉 所有任务完成!")

三、同时修复多个 Issue:完整实战

3.1 场景设定

假设我们的项目有以下待修复的 Issue:

Issue 标题 严重程度 需要修改的文件
#201 登录页面 XSS 漏洞 🔴 紧急 src/auth/login.py
#202 用户头像上传失败 🟡 重要 src/upload/avatar.py
#203 搜索分页计算错误 🟢 普通 src/search/pagination.py

3.2 完整自动化流程

python
import json
import time

PROJECT = "/opt/data/my-project"
ISSUES = [
    {
        "id": 201,
        "title": "登录页面 XSS 漏洞",
        "branch": "fix/xss-login-201",
        "worktree": "wt-201",
        "prompt": """修复 Issue #201:登录页面 XSS 漏洞。
问题:用户名输入框没有对特殊字符进行转义。
文件:src/auth/login.py
要求:
1. 对用户名输入进行 HTML 转义
2. 添加输入验证
3. 编写对应的测试用例"""
    },
    {
        "id": 202,
        "title": "用户头像上传失败",
        "branch": "fix/avatar-upload-202",
        "worktree": "wt-202",
        "prompt": """修复 Issue #202:用户头像上传失败。
问题:上传超过 5MB 的头像时返回 500 错误。
文件:src/upload/avatar.py
要求:
1. 添加文件大小校验
2. 返回友好的错误信息
3. 支持图片格式验证"""
    },
    {
        "id": 203,
        "title": "搜索分页计算错误",
        "branch": "fix/search-pagination-203",
        "worktree": "wt-203",
        "prompt": """修复 Issue #203:搜索分页计算错误。
问题:最后一页的数据计算不正确。
文件:src/search/pagination.py
要求:
1. 修复 offset 计算公式
2. 处理空结果集的情况
3. 添加边界条件测试"""
    }
]

# ========== 阶段 1:创建所有 worktree ==========
print("📦 阶段 1:创建 worktree...")
for issue in ISSUES:
    cmd = f"cd {PROJECT} && git worktree add -b {issue['branch']} /opt/data/{issue['worktree']} HEAD"
    result = terminal(command=cmd)
    if result.exit_code == 0:
        print(f"  ✅ {issue['worktree']} -> {issue['branch']}")
    else:
        print(f"  ❌ {issue['worktree']} 创建失败: {result.output}")

# ========== 阶段 2:启动所有 Codex 实例 ==========
print("\n🤖 阶段 2:启动 Codex 实例...")
sessions = {}
for issue in ISSUES:
    worktree_path = f"/opt/data/{issue['worktree']}"

    session = terminal(
        command=f"cd {worktree_path} && codex exec -p \"{issue['prompt']}\"",
        background=True,
        pty=True,
        timeout=900,
        notify_on_complete=True
    )

    sessions[issue["id"]] = {
        "session_id": session.session_id,
        "worktree": issue["worktree"],
        "branch": issue["branch"]
    }
    print(f"  🚀 Issue #{issue['id']} -> Session {session.session_id}")

# ========== 阶段 3:等待并监控 ==========
print("\n⏳ 阶段 3:监控执行...")
completed = []

while len(completed) < len(ISSUES):
    for issue_id, info in sessions.items():
        if issue_id in [c["id"] for c in completed]:
            continue

        status = process(action="poll", session_id=info["session_id"])

        if status.status != "running":
            # 获取变更摘要
            worktree_path = f"/opt/data/{info['worktree']}"
            diff = terminal(
                command=f"cd {worktree_path} && git diff --stat",
                timeout=30
            )

            completed.append({
                "id": issue_id,
                "status": status.status,
                "diff": diff.output
            })

            print(f"\n  ✅ Issue #{issue_id} 完成")
            print(f"     变更统计:\n{diff.output}")

# ========== 阶段 4:创建提交 ==========
print("\n💾 阶段 4:创建提交...")
for item in completed:
    worktree_path = f"/opt/data/{sessions[item['id']]['worktree']}"
    branch = sessions[item["id"]]["branch"]

    # 在对应的 worktree 中添加和提交
    cmds = [
        f"cd {worktree_path} && git add -A",
        f'cd {worktree_path} && git commit -m "fix: 修复 Issue #{item[\'id\']}"',
    ]

    for cmd in cmds:
        r = terminal(command=cmd, timeout=30)
        print(f"  {cmd.split('/')[-1][:30]}... exit_code={r.exit_code}")

print("\n🎉 所有 Issue 修复完成!")

四、高级技巧

4.1 worktree + 裸仓库模式

对于只读审查场景,可以使用裸仓库作为中央存储:

bash
# 创建裸仓库
git clone --bare /opt/data/my-project /opt/data/my-project.git

# 从裸仓库创建多个 worktree
git clone /opt/data/my-project.git /opt/data/work-1
git clone /opt/data/my-project.git /opt/data/work-2

4.2 不同分支的 Codex 配置

每个 worktree 可以有不同的 Codex 配置:

python
# 为不同任务设置不同的 CLAUDE.md
configs = {
    "wt-201": """# Issue #201 修复指南
- 这是一个安全修复,请格外谨慎
- 所有输入必须进行转义
- 修改后运行安全测试""",
    "wt-202": """# Issue #202 修复指南
- 这是用户体验问题
- 保持向后兼容
- 添加适当的错误处理""",
    "wt-203": """# Issue #203 修复指南
- 这是逻辑 bug
- 重点检查边界条件
- 确保分页 API 正确工作"""
}

for wt, config in configs.items():
    config_path = f"/opt/data/{wt}/CLAUDE.md"
    write_file(path=config_path, content=config)

4.3 并行测试

在每个 worktree 中独立运行测试,互不干扰:

python
# 在所有 worktree 中并行运行测试
test_sessions = {}
for issue in ISSUES:
    wt_path = f"/opt/data/{issue['worktree']}"

    session = terminal(
        command=f"cd {wt_path} && pytest tests/ -v --tb=short",
        background=True,
        timeout=300,
        notify_on_complete=True
    )

    test_sessions[issue["id"]] = session.session_id
    print(f"测试已启动: Issue #{issue['id']}")

# 收集所有测试结果
results = {}
for issue_id, sid in test_sessions.items():
    result = process(action="wait", session_id=sid, timeout=300)
    results[issue_id] = {
        "exit_code": result.exit_code,
        "passed": result.exit_code == 0
    }

for iid, r in results.items():
    status = "✅ 通过" if r["passed"] else "❌ 失败"
    print(f"Issue #{iid} 测试: {status}")

4.4 资源隔离

每个 Codex 实例是独立的进程,可以设置不同的资源限制:

python
# 使用 systemd-run 或类似工具限制资源
session = terminal(
    command="cd /opt/data/wt-201 && systemd-run --scope -p MemoryMax=2G codex exec -p '...'",
    background=True,
    pty=True
)

4.5 批量清理 worktree

python
# 完成任务后清理所有 worktree
for issue in ISSUES:
    wt_path = f"/opt/data/{issue['worktree']}"

    # 确保没有未提交的变更
    status = terminal(command=f"cd {wt_path} && git status --porcelain")
    if status.output.strip():
        print(f"⚠️ {issue['worktree']} 有未提交的变更")
    else:
        # 从主仓库移除 worktree
        remove = terminal(
            command=f"cd {PROJECT} && git worktree remove {wt_path}"
        )
        print(f"🗑️ 已移除 {issue['worktree']}")

五、注意事项与最佳实践

5.1 分支冲突管理

bash
# 不要对同一个分支创建多个 worktree
# ❌ 错误:两个 worktree 指向同一分支
git worktree add ../wt-1 fix/same-branch
git worktree add ../wt-2 fix/same-branch  # 会报错或行为异常

# ✅ 正确:每个 worktree 使用独立分支
git worktree add -b fix/issue-1 ../wt-1 HEAD
git worktree add -b fix/issue-2 ../wt-2 HEAD

5.2 Git 操作同步

bash
# 在主仓库 fetch 后,所有 worktree 自动可见
cd /opt/data/my-project
git fetch origin

# 在任一 worktree 中 push 后,主仓库也可见
cd /opt/data/wt-201
git push origin fix/xss-login-201

5.3 共享文件修改

⚠️ 注意: 多个 worktree 共享同一个 .git 数据库。如果你在一个 worktree 中修改了 .git/ 目录下的配置,会影响所有 worktree。

bash
# ❌ 避免直接修改 .git 目录
# ✅ 使用 git config 命令
cd /opt/data/wt-201
git config user.name "Agent-201"
git config user.email "agent-201@example.com"

5.4 性能考量

text
┌─────────────────────────────────────────────────┐
│           worktree 并行性能考量                   │
├─────────────────────────────────────────────────┤
│ 磁盘 I/O:多个实例同时读写可能成为瓶颈             │
│ CPU:多个 Codex 实例的 CPU 使用率会累加            │
│ 内存:每个实例有独立的内存占用                     │
│ 网络:同时请求外部 API 可能触发限流                │
│                                                 │
│ 建议:                                            │
│ - 3-5 个实例通常不会有明显性能问题                 │
│ - 超过 5 个实例时关注磁盘 I/O                      │
│ - 监控内存使用,避免 OOM                           │
└─────────────────────────────────────────────────┘

总结

本文深入讲解了 git worktree 结合 Codex CLI 的并行开发模式:

  1. git worktree 基础:创建、查看、管理多个独立工作目录
  2. 多 Codex 实例:每个 worktree 运行独立的 AI Agent,互不干扰
  3. 完整实战:从创建 worktree 到启动 Codex 到监控到提交的完整流程
  4. 高级技巧:不同配置、并行测试、批量清理
  5. 最佳实践:分支管理、同步操作、性能考量

核心要点:git worktree 让你无需 stash/checkout 就能同时工作在多个分支上,结合 Codex CLI 的并行实例,你可以让 AI 同时修复多个 Issue,大幅提升开发效率。

下篇预告

下一篇:《PR Review 实战 —— 临时克隆 + 安全审查、gh pr checkout》

我们将学习如何使用 GitHub CLI 检出 PR 分支,在安全的隔离环境中让 Codex CLI 进行代码审查,生成详细的审查报告。包括临时克隆策略、安全隔离、自动审查报告生成等内容。敬请期待!