/** * 自动提交管理器 * * 参考 aider 的 auto_commit 实现 * 支持 immediate、batch、manual 三种模式 */ import { minimatch } from 'minimatch'; import type { GitRepo } from './repo.js'; import type { AutoCommitConfig, CommitResult } from './types.js'; import { MessageGenerator } from './message-generator.js'; export class AutoCommitManager { private repo: GitRepo; private config: AutoCommitConfig; private messageGenerator: MessageGenerator; /** 待提交的文件 */ private pendingFiles: Set = new Set(); /** 批量提交定时器 */ private batchTimer: NodeJS.Timeout | null = null; /** 提交回调 */ private onCommitCallback?: (result: CommitResult) => void; constructor(repo: GitRepo, config: AutoCommitConfig, messageGenerator: MessageGenerator) { this.repo = repo; this.config = config; this.messageGenerator = messageGenerator; } /** * 设置提交回调 */ setOnCommit(callback: (result: CommitResult) => void): void { this.onCommitCallback = callback; } /** * 文件变更后调用 */ async onFileChanged(filePath: string, changeType: 'create' | 'modify' | 'delete'): Promise { if (!this.config.enabled) { return; } // 检查是否应该排除 if (this.shouldExclude(filePath)) { return; } // 如果启用脏文件提交,检查文件是否为脏状态 if (this.config.dirtyCommits && changeType !== 'create') { const isDirty = await this.repo.isDirty(filePath); if (isDirty) { // 在新的编辑之前,先提交脏文件 await this.commitDirtyFile(filePath); } } // 添加到待提交列表 this.pendingFiles.add(filePath); // 根据模式处理 switch (this.config.mode) { case 'immediate': await this.executeCommit(); break; case 'batch': this.scheduleBatchCommit(); break; case 'manual': // 不自动提交,只记录 break; } } /** * 计划批量提交 */ private scheduleBatchCommit(): void { if (this.batchTimer) { clearTimeout(this.batchTimer); } this.batchTimer = setTimeout(async () => { this.batchTimer = null; await this.executeCommit(); }, this.config.batchDelay); } /** * 执行提交 */ private async executeCommit(): Promise { if (this.pendingFiles.size === 0) { return null; } const files = Array.from(this.pendingFiles); this.pendingFiles.clear(); try { // 获取差异用于生成消息 const diff = await this.repo.getDiff({ staged: false }); // 生成提交消息 const message = this.messageGenerator.generate(diff, files); // 执行提交 const result = await this.repo.commit({ files, message, aiEdits: true, }); // 调用回调 if (result.success && this.onCommitCallback) { this.onCommitCallback(result); } return result; } catch (error) { // 恢复 pending 状态 files.forEach((f) => this.pendingFiles.add(f)); return { success: false, error: error instanceof Error ? error.message : String(error), }; } } /** * 提交脏文件 */ private async commitDirtyFile(filePath: string): Promise { try { await this.repo.commit({ files: [filePath], message: `chore: save changes to ${filePath} before AI edit`, aiEdits: false, // 用户的脏文件,不标记为 AI 编辑 }); } catch { // 脏文件提交失败不影响主流程 } } /** * 检查文件是否应该排除 */ private shouldExclude(filePath: string): boolean { return this.config.excludePatterns.some((pattern) => minimatch(filePath, pattern, { matchBase: true }) ); } /** * 强制立即提交 */ async flush(): Promise { if (this.batchTimer) { clearTimeout(this.batchTimer); this.batchTimer = null; } return this.executeCommit(); } /** * 获取待提交文件 */ getPendingFiles(): string[] { return Array.from(this.pendingFiles); } /** * 清除待提交文件 */ clearPending(): void { if (this.batchTimer) { clearTimeout(this.batchTimer); this.batchTimer = null; } this.pendingFiles.clear(); } /** * 是否有待提交文件 */ hasPendingFiles(): boolean { return this.pendingFiles.size > 0; } }