10.4 Worktree 管理

模型: claude-opus-4-6 (anthropic/claude-opus-4-6) 生成日期: 2026-02-17


在软件开发中,经常需要同时处理多个任务——你可能正在开发一个新功能,突然需要切换到另一个分支去修复一个紧急 Bug。传统做法是 git stashgit checkout,但这些操作会打断当前工作流。Git Worktree 提供了一种更优雅的方案:在同一个 Git 仓库中创建多个独立的工作目录,每个目录对应不同的分支,彼此互不干扰。

OpenCode 的 Worktree 模块将这一 Git 特性集成到了 AI 编程助手中,使得多个 Agent 实例可以在同一项目的不同分支上并行工作——例如,一个 Agent 在主分支上开发新功能,另一个 Agent 在独立的 Worktree 中修复 Bug。

衍生解释:什么是 Git Worktree?

Git Worktree(工作树)是 Git 2.5 引入的功能。正常情况下,一个 Git 仓库只有一个工作目录(Working Tree),也就是你平时编辑代码的地方。Git Worktree 允许你从同一个仓库中"分裂"出多个工作目录,每个工作目录检出不同的分支。

# 创建一个新的 worktree,检出 feature-x 分支
git worktree add ../my-project-feature-x feature-x

# 现在 ../my-project-feature-x/ 是一个完全独立的工作目录
# 修改这里的文件不会影响主工作目录

所有 Worktree 共享同一个 .git 数据库(对象、引用等),因此创建 Worktree 几乎不消耗额外磁盘空间。但每个 Worktree 有独立的暂存区(index)和工作目录,可以独立地进行 addcommitcheckout 等操作。

核心限制:同一个分支不能同时被两个 Worktree 检出。

10.4.1 数据模型

Worktree 模块定义在 worktree/index.ts 中,采用 TypeScript Namespace 模式。核心数据结构是 Worktree.Info

// worktree/index.ts
export const Info = z
  .object({
    name: z.string(),       // Worktree 名称,如 "brave-cabin"
    branch: z.string(),     // Git 分支名称,如 "opencode/brave-cabin"
    directory: z.string(),  // Worktree 的绝对路径
  })
  .meta({ ref: "Worktree" })

三个操作分别有对应的输入类型:

操作
输入类型
关键字段

创建

CreateInput

name?(可选名称)、startCommand?(可选启动脚本)

删除

RemoveInput

directory(要删除的 Worktree 路径)

重置

ResetInput

directory(要重置的 Worktree 路径)

模块还定义了两个事件,用于通知外部 Worktree 生命周期的变化:

10.4.2 Worktree 创建流程

创建 Worktree 是最复杂的操作。Worktree.create() 的实现可以分为同步阶段异步阶段两个部分——这是一个重要的设计决策。

同步阶段:快速返回

注意第 4 步使用了 --no-checkout 参数——这意味着 Worktree 目录被创建后,里面是空的。实际的文件检出在异步阶段完成。这样设计的好处是:创建操作对调用方来说是"即时"的,用户可以立即获得 Worktree 的元信息,而耗时的文件检出和初始化在后台进行。

名称生成策略

Worktree 的名称生成采用了一种有趣的"形容词-名词"组合方式:

30 × 32 = 960 种组合,足以避免在单个项目中出现名称冲突。candidate() 函数在生成名称后还会检查两个条件:

  1. 目录不存在:确保不会覆盖已有的 Worktree。

  2. 分支不存在:确保 opencode/<name> 分支名称可用。

如果两个条件任一不满足,会重试最多 26 次。如果用户提供了自定义名称,首先尝试使用该名称,失败后才拼接随机后缀:

异步阶段:后台初始化

创建操作返回后,一个 setTimeout 回调在事件循环的下一个 tick 开始执行实际的初始化工作:

初始化分四步:

  1. git reset --hard:将 --no-checkout 创建的空目录填充为完整的工作目录。

  2. Instance.provide():为新的 Worktree 目录初始化 OpenCode 的 Instance 上下文(配置、存储、事件总线等),使其成为一个可以独立运行 Agent 的工作空间。

  3. 发送 Ready 事件:通过 GlobalBus 通知所有监听者 Worktree 已就绪。

  4. 执行启动脚本:先执行项目级的 start 命令(如 npm install),再执行用户提供的额外启动命令。

10.4.3 Worktree 删除

删除操作需要处理三种情况:Worktree 正常存在、Worktree 已在 Git 中取消注册但目录仍存在、以及 Worktree 完全不存在。

--force 参数确保即使 Worktree 中有未提交的更改,也能被删除。分支清理是必要的——因为创建时使用了 -b 参数创建了新分支,删除 Worktree 后这些分支就不再需要了。

路径标准化

路径比较是 Worktree 管理中一个容易出错的细节。canonical() 函数确保路径的一致性:

这个函数处理了三种常见的路径不一致问题:相对路径、符号链接、以及 Windows 上的大小写差异。

10.4.4 Worktree 重置

重置操作将 Worktree 恢复到远程默认分支的最新状态——相当于"把这个工作空间推倒重来"。这是一个复杂的多步骤过程:

默认分支检测

确定"重置到哪个分支"的逻辑是一个有意思的多级回退策略:

这种多级回退确保了在各种 Git 仓库配置下都能正常工作。

深度清理策略

sweep() 函数处理了 git clean 可能失败的边缘情况:

git clean -ffdx 使用了所有"强制"选项(-ff 强制删除,-d 包含目录,-x 包含 gitignore 中的文件),但在某些平台上(尤其是 Windows)仍可能因为文件锁等原因失败。sweep() 解析 Git 的错误输出,提取出"failed to remove"的文件列表,通过 Node.js 的 fs.rm() 手动删除后再重试。

10.4.5 与 Instance 系统的集成

Worktree 与第 3 章介绍的 Instance 系统紧密集成。每个 Worktree 都是一个独立的 Instance 上下文:

注意 directoryworktree 的区别:

  • directory:Instance 的工作目录——对于 Worktree,这是 Worktree 自己的路径。

  • worktree:Git 仓库的根工作树——所有 Instance 共享同一个值(主项目目录)。

这个区别很重要,因为 Git 操作(如 git worktree add)需要在主工作树中执行,而文件操作则在各自的 directory 中进行。

10.4.6 HTTP API

Worktree 操作通过 Server 的实验性路由暴露为 HTTP API:

这些 API 使得 Web UI 和 IDE 扩展也能管理 Worktree,而不仅限于 CLI。

10.4.7 错误处理体系

Worktree 模块定义了六种具名错误类型,每种对应一个特定的失败场景:

这些错误使用 NamedError.create() 工厂函数创建,每个错误都携带结构化的 message 字段。在 Server 层,这些错误会被中间件捕获并转换为 HTTP 400 响应,附带可读的错误信息。

10.4.8 小结

Worktree 模块展示了几个值得学习的设计模式:

  1. 异步初始化create() 立即返回元信息,耗时的初始化在后台完成。通过事件系统(Event.Ready / Event.Failed)通知调用方最终结果。这种"快速响应 + 后台处理 + 事件通知"的三段式设计在交互式应用中非常常见。

  2. 防御式路径处理canonical() 函数统一处理了相对路径、符号链接和平台差异,避免了因路径格式不一致导致的匹配失败。

  3. 渐进式容错sweep() 函数先尝试标准方式,失败后解析错误信息并采取补救措施,最后重试。这种"尝试 → 诊断 → 修复 → 重试"的模式在处理系统级操作时非常实用。

  4. 多级回退:默认分支检测使用了 remote HEAD → main → master 的回退链,确保在各种仓库配置下都能工作。

  5. 友好的名称系统:使用形容词-名词组合生成人类可读的名称,比使用 UUID 或哈希更容易记忆和沟通。

Last updated