/** * Undo 管理器 * * 参考 aider 的 undo 实现 * 仅支持撤销 AI 生成的提交,且需要通过多项安全检查 */ import type { GitRepo } from './repo.js'; import type { UndoConfig, UndoEntry, UndoResult, CommitInfo } from './types.js'; export class UndoManager { private repo: GitRepo; private config: UndoConfig; /** Undo 历史 */ private history: UndoEntry[] = []; constructor(repo: GitRepo, config: UndoConfig) { this.repo = repo; this.config = config; } /** * 记录提交到 undo 历史 */ recordCommit(commitHash: string, shortHash: string, message: string, files: string[]): void { const entry: UndoEntry = { id: `undo-${Date.now()}`, timestamp: Date.now(), commitHash: shortHash, message, files, canUndo: true, }; this.history.push(entry); // 限制历史记录数量 if (this.history.length > this.config.maxHistory) { this.history = this.history.slice(-this.config.maxHistory); } } /** * 执行 undo 操作 * * 安全检查清单(参考 aider): * 1. 提交是否由 AI 生成 * 2. 提交是否已推送到远程 * 3. 是否为合并提交 * 4. 文件是否有未提交的更改 */ async undo(): Promise { // 1. 检查是否有可撤销的提交 if (this.history.length === 0) { return { success: false, message: 'Nothing to undo. No AI commits in this session.', }; } // 2. 获取最后一条记录 const lastEntry = this.history[this.history.length - 1]; // 3. 获取 HEAD 提交 const headShortHash = await this.repo.getHeadShortHash(); if (!headShortHash) { return { success: false, message: 'Unable to get HEAD commit.', }; } // 4. 验证最后的提交是否与记录匹配 if (headShortHash !== lastEntry.commitHash) { return { success: false, message: `The last commit (${headShortHash}) does not match the recorded AI commit (${lastEntry.commitHash}). The repository may have changed since the AI edit.`, }; } // 5. 验证是否为 AI 生成的提交 if (!this.repo.isAICommit(headShortHash)) { return { success: false, message: `The commit ${headShortHash} was not made by AI in this session.`, }; } // 6. 检查提交是否已推送到远程 const headHash = await this.repo.getHeadCommit(); if (headHash) { const isPushed = await this.repo.isCommitPushed(headHash); if (isPushed) { return { success: false, message: 'The commit has already been pushed to remote. Undo is not safe.', }; } } // 7. 检查文件是否有未提交的更改 for (const file of lastEntry.files) { const isDirty = await this.repo.isDirty(file); if (isDirty) { return { success: false, message: `The file ${file} has uncommitted changes. Please commit or stash them first.`, }; } } // 8. 执行撤销操作 try { // 恢复文件到上一个版本 const restoredFiles: string[] = []; for (const file of lastEntry.files) { try { await this.repo.checkoutFile('HEAD~1', file); restoredFiles.push(file); } catch { // 文件可能在前一个提交中不存在(新建的文件) // 这种情况下跳过 } } // 软重置 HEAD await this.repo.resetSoft('HEAD~1'); // 从历史中移除 this.history.pop(); // 从 AI 提交记录中移除 this.repo.removeAICommitHash(lastEntry.commitHash); return { success: true, message: `Undone: ${lastEntry.commitHash} - ${lastEntry.message}`, commitHash: lastEntry.commitHash, restoredFiles, }; } catch (error) { return { success: false, message: `Undo failed: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * 预览将要撤销的内容 */ getUndoPreview(): UndoEntry | null { if (this.history.length === 0) { return null; } return this.history[this.history.length - 1]; } /** * 获取 undo 历史 */ getHistory(): UndoEntry[] { return [...this.history]; } /** * 清除历史 */ clearHistory(): void { this.history = []; } /** * 检查是否可以 undo */ async canUndo(): Promise<{ canUndo: boolean; reason?: string }> { if (this.history.length === 0) { return { canUndo: false, reason: 'No AI commits to undo' }; } const lastEntry = this.history[this.history.length - 1]; const headShortHash = await this.repo.getHeadShortHash(); if (headShortHash !== lastEntry.commitHash) { return { canUndo: false, reason: 'Repository has changed since last AI commit', }; } return { canUndo: true }; } }