10.1 Git 快照系统

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


AI Agent 修改文件时可能会犯错——它可能错误地删除代码、覆盖重要内容、或者产生语法错误。如果没有回滚机制,用户将不得不手动恢复这些更改,甚至可能无法恢复。OpenCode 的 Snapshot(快照)系统 正是为解决这个问题而设计的——它在 Agent 每一步操作之前自动创建文件系统快照,使得任何更改都可以被精确回滚。

10.1.1 设计目标

Snapshot 系统的设计目标是:

  1. 细粒度:每个工具调用步骤(step)前都创建快照,而非仅在会话级别。

  2. 零干扰:快照操作对用户和 Agent 完全透明,不影响项目的 Git 历史。

  3. 低开销:利用 Git 的内容寻址存储(content-addressable storage)和增量机制,最小化磁盘使用。

  4. 自动清理:过期的快照会被自动清理,不会无限增长。

10.1.2 独立 Git 仓库策略

Snapshot 系统的核心设计决策是使用一个独立的、隐藏的 Git 仓库来存储快照,而不是使用项目自身的 Git 仓库。这个仓库位于 OpenCode 的全局数据目录中:

// snapshot/index.ts
function gitdir() {
  const project = Instance.project
  return path.join(Global.Path.data, "snapshot", project.id)
  // 典型路径: ~/.local/share/opencode/snapshot/<project-id>
}

为什么不直接使用项目的 Git 仓库?原因有三:

  1. 不污染项目历史:快照是 OpenCode 的内部实现细节,不应该出现在项目的 Git 日志中。

  2. 不干扰工作区:项目可能有未提交的更改、暂存区状态等,快照操作不应该影响这些。

  3. 支持非 Git 项目:虽然当前实现依赖 Git,但将快照仓库与项目仓库分离,为未来支持其他版本控制系统留下了空间。

Git 的 --git-dir--work-tree 参数使得这种"独立仓库管理外部工作区"的方案成为可能——Git 仓库的元数据(.git)存储在 OpenCode 的数据目录中,但跟踪的文件是项目的工作区。

10.1.3 Snapshot.track():创建快照

track() 是创建快照的核心函数:

衍生解释:Git 的底层对象模型

Git 在底层使用三种核心对象:

  • Blob:存储文件内容

  • Tree:存储目录结构(记录每个文件/子目录的名称、权限和对应的 blob/tree hash)

  • Commit:指向一个 tree 对象,加上作者、时间戳和父 commit 等元数据

git write-tree 是一个底层命令(plumbing command),它将当前暂存区(index)的内容写入一个 tree 对象,返回该 tree 的 SHA-1 哈希值。与 git commit 不同,write-tree 不创建 commit 对象,也不修改 HEAD。这使得快照操作极其轻量——它只是将当前文件状态"冻结"为一个 tree 对象。

track() 的返回值是一个 tree 对象的哈希值(如 "a3f7b2c...")。这个哈希值会被存储在 Session 的消息数据中,作为后续回滚的锚点。

session/processor.ts 中,track() 在每个步骤(step)开始时被调用:

10.1.4 Snapshot.restore():恢复快照

当用户需要回滚到某个快照时,restore() 函数将工作区恢复到指定 tree 的状态:

恢复过程分为两个 Git 底层操作:

  1. git read-tree:将指定的 tree 对象读入暂存区(index),替换当前暂存区的内容。

  2. git checkout-index -a -f:将暂存区中的所有文件(-a)强制写出(-f)到工作区,覆盖现有文件。

细粒度回滚:revert()

除了完整恢复之外,revert() 函数支持更精细的文件级回滚:

revert() 的逻辑更加精细:

  • 它接受一组 Patch(每个 Patch 包含一个 tree hash 和变更的文件列表)。

  • 对每个变更的文件,尝试从对应的快照中恢复。

  • 如果文件在快照中不存在(说明是 Agent 新创建的),则删除该文件。

  • 使用 Set 确保每个文件只处理一次,避免重复操作。

10.1.5 Snapshot.diff():变更对比

diff() 函数用于比较当前工作区与某个快照之间的差异:

patch() 函数则只返回变更的文件列表(不含具体内容差异),更加轻量:

diffFull() 提供最完整的差异信息,包含每个文件的变更前后内容、添加/删除行数和状态:

10.1.6 自动清理机制

快照数据会随时间积累。OpenCode 通过 Scheduler(定时任务调度器,详见第 11 章)注册了一个每小时执行一次的清理任务:

gc --prune=7.days 是 Git 的垃圾回收命令——它会压缩 Git 对象(打包为 packfile)并删除 7 天前的不可达对象。由于快照只使用 write-tree(不创建 commit),tree 对象在没有 commit 引用时是"不可达"的,自然会在清理时被移除。

这种设计意味着:最近 7 天的快照始终可用,而更早的快照会被自动清理以释放磁盘空间。对于日常开发来说,7 天的窗口足够覆盖绝大多数回滚需求。


本节小结

OpenCode 的 Snapshot 系统使用独立的 Git 仓库(存储在 ~/.local/share/opencode/snapshot/ 中)来跟踪项目文件的状态变化。核心操作 track() 使用 git write-tree 创建轻量级的 tree 对象作为快照(不创建 commit),restore()revert() 分别支持全量恢复和文件级精细回滚。自动清理任务每小时运行一次,使用 git gc --prune=7.days 清理过期数据。这种设计在保证零干扰(不污染项目 Git 历史)的同时,以极低的开销实现了细粒度的文件变更追踪和回滚能力。

Last updated