08 · Git 检查点、Worktree 与回退
如何用临时 index 和隐藏 refs 捕获 Agent 变更,同时不破坏用户暂存区。
问题比 git commit 更难
Agent 开始前和完成后都需要一个可比较、可恢复的工作区状态。但直接运行 git add -A && git commit 会:
- 修改用户真实 index,把未准备提交的文件变成 staged;
- 改变当前分支和 HEAD;
- 触发 hooks 或签名配置;
- 把产品内部 checkpoint 混入用户历史;
- 在多 worktree 仓库中碰错 git dir。
T3 Code 的目标不是替用户提交,而是创建旁路的不可见快照。
抽象边界
CheckpointStore.ts
不直接写 Git 命令,而是委托 VCS driver 的可选 checkpoint capability。这样非 Git 项目仍可运行,只是没有回退能力;
未来其他 VCS 也可以提供自己的实现。
CheckpointReactor 将业务时机与 VCS 细节分开:它知道 turn、baseline、completed、revert 和 diff,
Git driver 只知道如何捕获/恢复一个工作目录。
隐藏 ref 命名
Git 实现使用类似下面的 ref:
refs/t3/checkpoints/<base64url(threadId)>/turn/<n>Thread ID 先做 base64url 编码,避免任意字符破坏 ref 语法;turn number 形成稳定顺序。ref 不出现在普通 branch 列表, 但仍是 Git 可达对象,垃圾回收不会立即删除 commit/tree。
捕获算法:临时 index 是关键
GitVcsDriver.ts 的核心流程可抽象为:
GIT_INDEX_FILE=<temporary-index> git read-tree HEAD # 有 HEAD 时
GIT_INDEX_FILE=<temporary-index> git add -A
tree=$(GIT_INDEX_FILE=<temporary-index> git write-tree)
commit=$(printf <message> | git commit-tree "$tree" -p <parent>)
git update-ref <hidden-ref> "$commit"实现并不是原样 shell 脚本,但 Git plumbing 意义相同:
- 在 git common dir 附近创建隔离 index;
- 从 HEAD 初始化基线(空仓库则处理无 HEAD);
- 将当前工作树的 tracked/untracked 变化写入临时 index;
write-tree创建树对象;commit-tree创建不切换分支的 commit;update-ref原子移动 T3 隐藏 ref;- 删除临时 index。
这是整套设计最值得迁移的技巧:Git index 是可替换的文件,不必为了得到 tree 而修改用户当前暂存状态。
为什么使用 common dir
Git worktree 有工作树自己的 git dir,也有共享 object database/ref 的 common dir。临时 index 与隐藏 ref 必须在正确的仓库语境中创建,
否则 linked worktree、submodule 或 bare/特殊布局会出现路径错误。GitVcsDriverCore.ts 集中处理 Git 命令和路径解析,
上层 driver 组合 checkpoint、diff 与 source-control 语义。
完成 checkpoint 与 diff
Turn 前的 baseline 和 turn 后的 completed ref 都指向完整树。Diff 可直接在两个 commit/tree 间计算,不依赖工作区此刻是否又被用户修改:
baseline commit --git diff--> completed commit这使代码审查视图稳定。Diff blob 按需存储,并有约 10 MB上限;超过上限应返回明确的截断/不可用状态, 不能把 WebSocket 与移动端内存作为无限传输通道。
回退算法
恢复 checkpoint 时,Git driver 以目标 commit 为 source,同时更新 worktree 和 staged state,并清理快照之外的 untracked 文件:
git restore --source <checkpoint> --worktree --staged ...
git clean -fd ...之后还需要把 index 恢复到产品定义的期望状态。这里的风险远高于捕获:git clean -fd 会删除未跟踪文件。
因此回退必须是明确用户动作、严格限定工作目录与 checkpoint,并在业务层显示影响,不能作为自动错误恢复随意执行。
Checkpoint 不是无损撤销所有外部变化
它捕获 Git 可表示的工作树内容,不会回滚数据库、启动的服务、已发送网络请求、被忽略文件或仓库外文件。 UI 应把它称为代码/工作区回退,而不是系统级时间机器。
与 Git worktree 的配合
T3 Code 可以让 thread 在独立 worktree 中运行,从而隔离并行 Agent。Thread 保存 environment/runtime mode 和 worktree 关联, 首次 turn 的标题/branch 生成可能为它命名。Checkpoint ref 仍以 thread/turn 标识,不依赖 branch 名,因此重命名 UI 标题不会破坏历史。
并行 worktree 降低文件冲突,却引入生命周期问题:创建失败、分支已存在、主仓库被移动、thread 删除后的清理。 这些操作应由 Server/VCS 服务集中拥有,客户端只发送意图。
边界情况
| 情况 | 设计关注点 |
|---|---|
| 新仓库没有 HEAD | 从空树构造 checkpoint,不假设 parent commit |
| 未跟踪文件 | add -A 纳入快照;回退时目标外文件可能被清理 |
.gitignore 文件 | 通常不捕获,不能承诺恢复 |
| Linked worktree | 正确解析 git dir/common dir,避免 ref/object 错位 |
| 大二进制变化 | diff 上限与不可预览状态,tree 本身仍可捕获 |
| 捕获中断 | 临时 index 清理,ref 只在完整 commit 后更新 |
| 用户同时编辑 | 快照代表命令执行时读取到的树;上层要协调 turn 边界 |
测试关注点
GitVcsDriver.test.ts
和 core 测试应验证:用户 index 字节/状态不变、HEAD 不动、未跟踪文件进入 checkpoint、空仓库可用、linked worktree 正确、
diff 稳定、回退恢复预期内容、错误后临时文件被释放。
可迁移经验
- 需要“保存当前工作区”时优先使用 Git plumbing +
GIT_INDEX_FILE,不要劫持 porcelain 工作流。 - 产品内部历史放在命名空间明确的隐藏 refs,避免污染用户 branch/commit log。
- 捕获与恢复不是对称风险:恢复会覆盖/删除内容,必须提高授权和确认等级。
- Checkpoint 只覆盖版本控制能表达的副作用;把边界诚实呈现给用户。