概述
Cline Checkpoint(检查点)用于在任务执行过程中自动保存工作区快照,帮助开发者随时查看、比较和恢复代码状态。
它通过创建"影子 Git 仓库"来跟踪工作区变更,实现快照保存、版本比较和状态恢复等功能,为开发者提供了强大的工作区版本控制能力。
设计原理
核心思想
- 影子仓库:创建独立的 Git 仓库来管理检查点,避免与主项目仓库冲突
- 工作树分离 :通过
core.worktree
配置将影子仓库与主工作区关联 - 快照 机制:利用 Git 提交功能保存工作区的完整状态
- 差异比较:基于 Git diff 实现不同检查点间的变更对比
技术实现详解
影子仓库初始化(init)
sequenceDiagram
participant User as 👤 用户
participant Cline as 🤖 Cline
participant LLM as 🧠 大模型
participant Checkpoint as 📋 检查点系统
participant Git as 🗂️ 影子Git仓库
User->>Cline: 发送任务请求
Cline->>LLM: 发送API请求
LLM-->>Cline: 返回响应(工具调用)
Note over Cline: 解析响应内容,执行工具操作
alt 工具执行完成后
Cline->>Checkpoint: 调用saveCheckpoint()
Checkpoint->>Git: 添加所有文件到暂存区
Checkpoint->>Git: 创建提交
Git-->>Checkpoint: 返回提交哈希
Checkpoint-->>Cline: 返回提交哈希
Note over Cline: 更新检查点消息状态
Cline->>User: 显示检查点已创建
end
alt 需要继续对话
Cline->>LLM: 发送下一轮API请求
LLM-->>Cline: 返回新的响应
end
初始化过程包括仓库创建、工作树配置和安全设置:
TypeScript
public async initShadowGit(gitPath: string, cwd: string, taskId: string): Promise<string> {
// 如果仓库已存在,验证工作树配置
if (await fileExistsAtPath(gitPath)) {
const git = simpleGit(path.dirname(gitPath))
const worktree = await git.getConfig("core.worktree")
if (worktree.value !== cwd) {
throw new Error("Checkpoints can only be used in the original workspace: " + worktree.value)
}
// 更新排除文件模式
await writeExcludesFile(gitPath, await getLfsPatterns(this.cwd))
return gitPath
}
// 创建新的影子仓库
const checkpointsDir = path.dirname(gitPath)
const git = simpleGit(checkpointsDir)
await git.init()
// 关键配置:将工作树指向主工作区
await git.addConfig("core.worktree", cwd)
await git.addConfig("commit.gpgSign", "false")
await git.addConfig("user.name", "Cline Checkpoint")
await git.addConfig("user.email", "checkpoint@cline.bot")
// 设置LFS模式和排除文件
const lfsPatterns = await getLfsPatterns(cwd)
await writeExcludesFile(gitPath, lfsPatterns)
// 添加文件并创建初始提交
const addFilesResult = await this.addCheckpointFiles(git)
if (!addFilesResult.success) {
throw new Error("Failed to add at least one file(s) to checkpoints shadow git")
}
await git.commit("initial commit", { "--allow-empty": null })
return gitPath
}
git 配置 core.worktree 的作用
默认情况下,.git 目录位于工作目录的根目录下。通过设置 core.worktree,可以将工作目录移动到其他位置,而 .git 目录单独保留在另一个路径。 在 Cline 检查点系统中,它起到了关键的"桥梁"作用: 核心机制:
- 分离式架构:Git 仓库目录(
.git
)与工作目录可以位于不同位置 - 路径重定向:通过
core.worktree
将影子仓库的工作目录指向主项目的实际工作区 - 透明操作:Git 命令会自动在指定的工作目录中执行,而不是在仓库目录中
实际效果: 影子仓库位置:/path/to/.cline/checkpoints/.git (在该目录执行 git 命令) 工作目录位置:/path/to/main/project (通过 core.worktree 指定)
这样设计的优势:
- 无侵入性:主项目目录保持干净,不会出现额外的
.git
目录 - 版本隔离:影子仓库与主项目的 Git 仓库完全独立,避免冲突
检查点创建(commit)
sequenceDiagram
participant User as 👤 用户
participant Cline as 🤖 Cline
participant LLM as 🧠 大模型
participant Checkpoint as 📋 检查点系统
participant Git as 🗂️ 影子Git仓库
User->>Cline: 发送任务请求
Cline->>LLM: 发送API请求
LLM-->>Cline: 返回响应(工具调用)
Note over Cline: 解析响应内容,执行工具操作
alt 工具执行完成后(如:写文件)
Cline->>Checkpoint: 调用saveCheckpoint()
Checkpoint->>Git: 添加所有文件到暂存区
Checkpoint->>Git: 创建提交
Git-->>Checkpoint: 返回提交哈希
Checkpoint-->>Cline: 返回提交哈希
Note over Cline: 更新检查点消息状态
Cline->>User: 显示检查点已创建
end
alt 需要继续对话
Cline->>LLM: 发送下一轮API请求
LLM-->>Cline: 返回新的响应
end
通过 Git commit 作为检查点,回退时,只需要切换到对应的 commit 即可
TypeScript
public async commit(): Promise<string | undefined> {
try {
const gitPath = await this.getShadowGitPath()
const git = simpleGit(path.dirname(gitPath))
// 添加所有文件到暂存区
await this.addAllFiles(git)
// 创建提交(允许空提交)
const result = await git.commit("checkpoint", {
"--allow-empty": null,
})
// 保存并返回提交哈希
const commitHash = result.commit || ""
this.lastCheckpointHash = commitHash
return commitHash
} catch (error) {
console.error("Failed to create checkpoint:", error)
return undefined
}
}
变更差异分析(diff)
差异分析功能基于 Git diff 实现,支持任意两个检查点间的变更对比:
TypeScript
public async getDiffSet(
lhsHash?: string,
rhsHash?: string,
): Promise<Array<{
relativePath: stringabsolutePath: stringbefore: stringafter: string
}>> {
const gitPath = await this.getShadowGitPath()
const git = simpleGit(path.dirname(gitPath))
// 智能基准选择:如果未指定基准,使用初始提交
let baseHash = lhsHash
if (!baseHash) {
const rootCommit = await git.raw(["rev-list", "--max-parents=0", "HEAD"])
baseHash = rootCommit.trim()
}
// 暂存所有变更,确保未跟踪文件也能被比较
await this.addAllFiles(git)
// 执行差异分析
const diffSummary = rhsHash
? await git.diffSummary([`${baseHash}..${rhsHash}`])
: await git.diffSummary([baseHash])
// 返回结构化的差异数据...
}
getDiffSet 的使用:
TypeScript
const changedFiles = await this.checkpointTracker?.getDiffSet(previousCheckpointHash, hash)
// Open multi-diff editor
await vscode.commands.executeCommand(
"vscode.changes",
seeNewChangesSinceLastTaskCompletion ? "New changes" : "Changes since snapshot",
changedFiles.map((file) => [
vscode.Uri.file(file.absolutePath),
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${file.relativePath}`).with({
query: Buffer.from(file.before ?? "").toString("base64"),
}),
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${file.relativePath}`).with({
query: Buffer.from(file.after ?? "").toString("base64"),
}),
]),
)
恢复检查点(revert)
sequenceDiagram
participant User as 👤 用户
participant Cline as 🤖 Cline
participant LLM as 🧠 大模型
participant Checkpoint as 📋 检查点系统
participant Git as 🗂️ 影子Git仓库
User->>Cline: 点击恢复按钮
Cline->>Checkpoint: 调用resetHead()
Checkpoint->>Git: 验证目标提交存在
Checkpoint->>Git: 执行硬重置
Git-->>Checkpoint: 恢复工作区文件
Checkpoint-->>Cline: 恢复操作完成
Cline-->>User: 显示操作结果
检查点恢复功能通过 Git 的硬重置机制实现工作区状态的完整回滚:
TypeScript
public async resetHead(targetHash: string): Promise<boolean> {
try {
const gitPath = await this.getShadowGitPath()
const git = simpleGit(path.dirname(gitPath))
// 验证目标提交是否存在
try {
await git.show([targetHash, "--name-only"])
} catch (error) {
console.error(`Target commit ${targetHash} does not exist`)
return false
}
// 执行硬重置到目标提交
await git.reset(["--hard", targetHash])
// 更新内部状态
this.lastCheckpointHash = targetHash
console.info(`Successfully reset to checkpoint: ${targetHash}`)
return true
} catch (error) {
console.error("Failed to reset checkpoint:", error)
return false
}
}
总结
Cline 的检查点系统巧妙地利用了 Git 的核心机制,通过影子仓库实现了无侵入式的工作区快照管理。这种设计不仅保证了与主项目版本控制的隔离,还充分发挥了 Git 在文件跟踪、差异比较和状态恢复方面的成熟能力。对于需要实现类似快照功能的项目,可以直接基于 Git 的 worktree、commit 和 reset 等原生功能构建,这样既能获得可靠的版本控制能力,又能避免重复造轮子的复杂性。