Gemini CLI Checkpointing 详解:基于 Shadow Git 仓库自动快照与 /restore 回滚机制
【免费下载链接】gemini-cliAn open-source AI agent that brings the power of Gemini directly into your terminal.项目地址: https://gitcode.com/GitHub_Trending/gemi/gemini-cli
Gemini CLI 的 Checkpointing(检查点)功能会在 AI 工具修改文件之前,自动为项目状态保存一份"快照",让你可以放心地让write_file、edit等工具改动代码,并在需要时通过/restore命令瞬间回滚到改动前的文件状态与对话历史。本文以 官方 Checkpointing 文档 为主线,结合 核心实现代码 与 Shadow Git 服务 的源码,完整讲解该功能的工作原理、配置方法、数据落盘位置,以及回滚命令的底层调用链,帮助你既会用、也懂其原理。
一、Checkpointing 是什么,何时触发
Checkpointing 的核心价值在于"安全实验":当 Agent 即将执行一次会修改文件系统的操作时,CLI 会先为当前项目打一个检查点。如果这次改动不理想,你可以一键回到检查点时刻的完整状态。
从源码看,检查点并不是对每一次工具调用都创建,而是有明确的触发条件。在 流式处理钩子 中,CLI 会筛选出"可回滚的工具调用"(restorable tool calls):
const restorableToolCalls = toolCalls.filter( (toolCall) => EDIT_TOOL_NAMES.has(toolCall.request.name) && toolCall.status === CoreToolCallStatus.AwaitingApproval, );其中EDIT_TOOL_NAMES定义在 工具名常量表:
export const EDIT_TOOL_NAMES = new Set([EDIT_TOOL_NAME, WRITE_FILE_TOOL_NAME]);也就是说,只有write_file和edit(对应文档中提到的replace一类文件修改工具)这两类会改动文件的工具,且处于"等待用户批准"状态时,才会进入快照流程。这符合文档描述的语义:当你批准一个修改文件系统的工具时,CLI 自动创建检查点。
二、一个检查点包含什么
文档指出,每个检查点由三部分组成,这三部分恰好对应ToolCallData结构体(定义于 checkpointUtils.ts):
- Git 快照(commitHash):在用户主目录下的一个"影子 Git 仓库"(shadow Git repository,位于
~/.gemini/history/<project_hash>)中提交一次 commit,完整记录当时项目文件的状态。它不会干扰项目自己的 Git 仓库。 - 对话历史(history / clientHistory):截至该时刻与 Agent 的全部对话,既保存了 UI 层的
HistoryItem列表,也保存了模型层的clientHistory(@google/genai的Content[])。 - 工具调用(toolCall):即将执行的那次工具调用本身,包括工具名和参数(
name+args),另附带messageId用于关联来源消息。
export interface ToolCallData<HistoryType = unknown, ArgsType = unknown> { history?: HistoryType; clientHistory?: readonly Content[]; commitHash?: string; toolCall: { name: string; args: ArgsType; }; messageId?: string; }写入磁盘前,该结构还会经过 Zod 校验(getToolCallDataSchema),确保toolCall.name、toolCall.args等字段类型正确,恢复时同样会用这套 schema 做safeParse,校验失败的文件会被判定为无效检查点。
检查点文件命名规则
检查点文件名由 generateCheckpointFileName 生成:
const timestamp = new Date() .toISOString() .replace(/:/g, '-') .replace(/\./g, '_'); const toolName = toolCall.name; const fileName = path.basename(toolFilePath); return `${timestamp}-${fileName}-${toolName}`;即"ISO 时间戳(冒号与点被替换为连字符/下划线)+ 被修改文件的基础名 + 工具名",例如2025-06-22T10-00-00_000Z-my-file.txt-write_file,与文档中的示例完全一致。值得注意的是:如果工具调用参数中没有file_path字符串,generateCheckpointFileName返回null,该调用会被跳过(源码会记录 "Skipping restorable tool call due to missing file_path" 错误)。
三、Shadow Git 仓库的隔离原理
检查点之所以"不干扰你自己的 Git 仓库",靠的是GitService精心构造的一套隔离环境(gitService.ts)。
3.1 独立的仓库身份与配置
影子仓库位于~/.gemini/history/<project_id>(Storage.getHistoryDir()返回该路径,见 storage.ts)。初始化时setupShadowGitRepository()会:
- 在仓库目录写入一份专属的
.gitconfig,作者固定为Gemini CLI <gemini-cli@google.com>,并显式关闭 GPG 签名(commit.gpgsign = false),避免继承用户的签名偏好; - 用
GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM环境变量把 git 的全局与系统配置指到这份隔离配置上,并清空继承来的GIT_DIR、GIT_WORK_TREE,防止用户环境变量破坏隔离; - 对首次初始化的仓库执行
git init(初始分支main)+ 一个空提交,保证仓库始终有 HEAD。
影子仓库的操作通过shadowGitRepository访问器完成,它把GIT_DIR指向~/.gemini/history/<id>/.git、GIT_WORK_TREE指向你的项目根目录——这是典型的"bare + worktree 分离"用法,因此你的项目目录里不会出现任何影子仓库的文件。
3.2 打快照:createFileSnapshot
async createFileSnapshot(message: string): Promise<string> { const repo = this.shadowGitRepository; await repo.add('.'); const status = await repo.status(); if (status.isClean()) { // If no changes are staged, return the current HEAD commit hash return await this.getCurrentCommitHash(); } const commitResult = await repo.commit(message, { '--no-verify': null }); return commitResult.commit; }逻辑是:先git add .把项目全部文件加入影子仓库索引;如果状态干净(相对上一个快照无变化),直接复用当前 HEAD 的 commit hash,避免产生冗余提交;否则以Snapshot for <tool_name>为消息提交。另外影子仓库的 git 操作开启了 simple-git 的整套 "unsafe" 选项(SHADOW_REPO_UNSAFE_OPTIONS),源码注释说明这是为了让这个内部、隔离的状态管理仓库不受用户本地PAGER、EDITOR、SSH等环境的影响而稳定工作。
3.3 回滚:restoreProjectFromSnapshot
async restoreProjectFromSnapshot(commitHash: string): Promise<void> { const repo = this.shadowGitRepository; await repo.raw(['restore', '--source', commitHash, '.']); // Removes any untracked files that were introduced post snapshot. await repo.clean('f', ['-d']); }恢复分两步:git restore --source <commitHash> .把所有已跟踪文件恢复到快照时刻的内容;随后git clean -fd删除快照之后新增的未跟踪文件——这正是文档所说"把项目所有文件恢复到快照捕获的状态"。由于 worktree 指向你的项目目录,这个操作直接改写工作区文件,但对项目自己的.git目录没有任何影响。
四、检查点数据存在哪里
两类数据分别落盘,均为纯本地存储:
| 数据 | 位置 | 说明 |
|---|---|---|
| Git 文件快照 | ~/.gemini/history/<project_id>/(含.git) | 影子仓库,commit 历史即项目状态史 |
| 对话历史 + 工具调用 | ~/.gemini/tmp/<project_id>/checkpoints/ | 每个检查点一个 JSON 文件(ToolCallData序列化) |
路径实现见 Storage 类:getHistoryDir()拼接~/.gemini/history/<id>,getProjectTempCheckpointsDir()拼接~/.gemini/tmp/<id>/checkpoints。
一个值得注意的细节:文档中写作<project_hash>,而从源码结构看,<id>实际来自 项目注册表 的getShortId(),且performMigration()会把旧的 sha256 哈希目录迁移为新的短 ID 目录。因此旧版用户看到的十六进制哈希目录,当前版本下可能呈现为短标识符,两者指代的都是"当前项目的唯一标识"。
检查点的创建与写盘流程汇总在processRestorableToolCalls()(checkpointUtils.ts):
- 对每个待执行的可回滚工具调用,先调
createFileSnapshot()拿 commit hash;若快照失败,则降级为使用当前 HEAD hash(getCurrentCommitHash()),并记录告警; - 若两者都拿不到 hash(例如 Git 未安装),记错误并跳过该调用;
- 组装
ToolCallData(含当时的完整对话历史与clientHistory),以<文件名>.json存入checkpointsToWrite映射,最终由 useGeminiStream.ts 写入检查点目录。
五、如何启用 Checkpointing
该功能默认关闭(配置 schema 中general.checkpointing.enabled的default: false,且requiresRestart: true,见 settingsSchema.ts;配置读取逻辑在 config.ts)。启用方式是编辑settings.json,加入:
{ "general": { "checkpointing": { "enabled": true } } }注意:
--checkpointing命令行标志已在版本 0.11.0 中移除,现在只能通过settings.json配置文件启用。
启用后还有一个硬性前提:Git 必须可用。GitService.initialize()会先执行git --version探测,若失败直接抛出 "Checkpointing is enabled, but Git is not installed" 错误,要求你安装 Git 或关闭该功能。集成测试 checkpointing.test.ts 覆盖了启用后的端到端行为,可作为功能可用性的验证参考。
六、使用 /restore 命令回滚
启用后检查点自动创建,管理统一走/restore命令。该命令的实现位于 restoreCommand.ts。
6.1 列出可用检查点
不带参数执行:
/restore命令会读取~/.gemini/tmp/<project_id>/checkpoints下的所有.json文件,若为空提示 "No restorable tool calls found.",否则输出可用检查点列表(经formatCheckpointDisplayList格式化,去掉.json后缀后逐行展示)。文件名即"时间戳-文件名-工具名",例如2025-06-22T10-00-00_000Z-my-file.txt-write_file。
6.2 恢复到指定检查点
/restore <checkpoint_file>例如:
/restore 2025-06-22T10-00-00_000Z-my-file.txt-write_file参数可以带或不带.json后缀(源码会自动补全);若文件不存在则报 "File not found"。执行后的完整动作由 performRestore 这个异步生成器按序产出:
load_history:把检查点中的history(UI 历史项)与clientHistory(模型对话)加载回会话,CLI 中的对话即"回到"检查点时刻;- Git 恢复:调用
gitService.restoreProjectFromSnapshot(commitHash)把工作区文件与快照对齐,成功则提示 "Restored project to the state before the tool call."; - 重新提议工具调用:
/restore命令最终返回{ type: 'tool', toolName, toolArgs },也就是把原来那次工具调用重新提交到审批界面——你可以选择再次执行、修改参数,或者干脆忽略它,这正是文档所说的 "Re-propose the original tool call"。
6.3 恢复失败的典型场景
源码对 Git 恢复做了针对性的错误处理:当git restore报出 "unable to read tree" 时,说明检查点引用的 commit hash 已不在影子仓库中——通常发生在仓库被重新克隆、重置或旧 commit 被垃圾回收之后,此时会明确告知 "This checkpoint cannot be restored" 并终止;若 Git 服务本身不可用,则提示需处于 git 环境。这类"hash 失联"是影子仓库方案的固有边界:检查点只在本地、生命周期与本地磁盘上的~/.gemini/history/绑定。
七、边界情况与故障降级小结
结合文档与源码,可以梳理出检查点机制的完整健壮性设计:
| 场景 | 行为 |
|---|---|
| Git 未安装但功能已启用 | 初始化阶段直接报错,要求安装 Git 或关闭 checkpointing |
| 创建快照失败(如目录不可访问) | 降级使用当前 HEAD hash,并记录告警继续 |
| 快照与当前 hash 均不可得 | 跳过该工具调用,记录 "Checkpointing may not be working properly" |
工具参数缺少file_path | 跳过检查点,记录 "missing file_path" |
| 快照后工作区无变化 | 复用现有 HEAD commit,不产生冗余提交 |
| 检查点 JSON 损坏/字段不符 schema | /restore时解析失败并报错,不影响其他检查点 |
| commit hash 已被 GC/重置 | 提示 "unable to read tree",该检查点不可恢复 |
八、与 Rewind 的区别及适用建议
Gemini CLI 还有一个 Rewind 功能,同样面向"回退"场景;从功能定位看,Checkpointing 专注于工具调用前的文件+会话原子快照,由文件修改类工具的审批流程自动触发,数据落在影子 Git 仓库与项目临时目录中。适用建议:
- 频繁让 Agent 改写项目文件时,开启
general.checkpointing.enabled,把/restore当作安全网; - 注意检查点目录会随工具调用次数增长,
~/.gemini/history/与~/.gemini/tmp/<id>/checkpoints/可按需清理(均为本地目录,删除即放弃对应回滚能力); - 在容器或 CI 等环境中使用前,先确认
git --version可用,这是功能的前置依赖。
参考路径
- 功能文档:docs/cli/checkpointing.md
- 检查点核心逻辑:packages/core/src/utils/checkpointUtils.ts
- 影子 Git 仓库实现:packages/core/src/services/gitService.ts
- 恢复命令核心:packages/core/src/commands/restore.ts
- /restore 斜杠命令:packages/cli/src/ui/commands/restoreCommand.ts
- 检查点触发点:packages/cli/src/ui/hooks/useGeminiStream.ts
- 存储路径定义:packages/core/src/config/storage.ts
- 配置 schema:packages/cli/src/config/settingsSchema.ts
- 集成测试:integration-tests/checkpointing.test.ts
【免费下载链接】gemini-cliAn open-source AI agent that brings the power of Gemini directly into your terminal.项目地址: https://gitcode.com/GitHub_Trending/gemi/gemini-cli
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考