Files
ai-terminal-assistant/packages/core/src/git/undo-manager.ts
T
kurihada 5e32375f0e feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更:
- 采用 pnpm workspaces 实现 Monorepo 结构
- 将现有代码迁移到 packages/core
- 新增 packages/server HTTP 服务层

Server 功能:
- REST API: 会话管理、工具管理、配置管理
- WebSocket: 实时双向通信支持
- SSE: 服务端事件推送
- Hono + Bun 作为运行时

API 端点:
- GET/POST /api/sessions - 会话 CRUD
- GET/POST /api/sessions/:id/messages - 消息管理
- GET /api/sessions/:id/events - SSE 事件流
- WS /api/ws/:sessionId - WebSocket 连接
- GET/POST /api/tools - 工具管理
- GET/PUT /api/config - 配置管理
2025-12-12 10:42:20 +08:00

195 lines
4.9 KiB
TypeScript

/**
* 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<UndoResult> {
// 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 };
}
}