import path from "path"; import type { ChangeRecord, TransactionState, BeginChangeOptions, CommitResult, RollbackResult, } from "./types"; import { createSnapshot, writeSnapshot, readSnapshotById, restoreSnapshot } from "./snapshot"; const MANDU_DIR = ".mandu"; const HISTORY_DIR = "history"; const CHANGES_FILE = "changes.json"; const ACTIVE_FILE = "active.json"; /** * 변경 ID 생성 (YYYYMMDD-HHmmss-xxx) */ function generateChangeId(): string { const now = new Date(); const date = now.toISOString().slice(0, 10).replace(/-/g, ""); const time = now.toISOString().slice(11, 19).replace(/:/g, ""); const random = Math.random().toString(36).slice(2, 5); return `${date}-${time}-${random}`; } /** * History 디렉토리 경로 */ function getHistoryDir(rootDir: string): string { return path.join(rootDir, MANDU_DIR, HISTORY_DIR); } /** * Changes 파일 경로 */ function getChangesPath(rootDir: string): string { return path.join(getHistoryDir(rootDir), CHANGES_FILE); } /** * Active 파일 경로 */ function getActivePath(rootDir: string): string { return path.join(getHistoryDir(rootDir), ACTIVE_FILE); } /** * 모든 변경 기록 읽기 */ async function readChanges(rootDir: string): Promise { const changesPath = getChangesPath(rootDir); try { const file = Bun.file(changesPath); if (!(await file.exists())) { return []; } return await file.json(); } catch { return []; } } /** * 변경 기록 저장 */ async function writeChanges(rootDir: string, changes: ChangeRecord[]): Promise { const changesPath = getChangesPath(rootDir); const historyDir = getHistoryDir(rootDir); // 디렉토리 확보 await Bun.write(path.join(historyDir, ".gitkeep"), ""); await Bun.write(changesPath, JSON.stringify(changes, null, 2)); } /** * 현재 트랜잭션 상태 읽기 */ async function readActiveState(rootDir: string): Promise { const activePath = getActivePath(rootDir); try { const file = Bun.file(activePath); if (!(await file.exists())) { return { active: false, changeId: null, snapshotId: null }; } return await file.json(); } catch { return { active: false, changeId: null, snapshotId: null }; } } /** * 트랜잭션 상태 저장 */ async function writeActiveState(rootDir: string, state: TransactionState): Promise { const activePath = getActivePath(rootDir); const historyDir = getHistoryDir(rootDir); // 디렉토리 확보 await Bun.write(path.join(historyDir, ".gitkeep"), ""); await Bun.write(activePath, JSON.stringify(state, null, 2)); } /** * 활성 트랜잭션 존재 여부 확인 */ export async function hasActiveTransaction(rootDir: string): Promise { const state = await readActiveState(rootDir); return state.active; } /** * 현재 활성 트랜잭션 정보 조회 */ export async function getActiveTransaction(rootDir: string): Promise { const state = await readActiveState(rootDir); if (!state.active || !state.changeId) { return null; } const changes = await readChanges(rootDir); return changes.find((c) => c.id === state.changeId) || null; } /** * 변경 트랜잭션 시작 * - 스냅샷 생성 후 활성 트랜잭션으로 등록 */ export async function beginChange( rootDir: string, options: BeginChangeOptions = {} ): Promise { // 이미 활성 트랜잭션이 있는지 확인 const existingState = await readActiveState(rootDir); if (existingState.active) { const existingChange = await getActiveTransaction(rootDir); throw new Error( `이미 활성화된 트랜잭션이 있습니다: ${existingState.changeId}${existingChange?.message ? ` (${existingChange.message})` : ""}` ); } // 스냅샷 생성 const snapshot = await createSnapshot(rootDir); await writeSnapshot(rootDir, snapshot); // 변경 기록 생성 const changeId = generateChangeId(); const change: ChangeRecord = { id: changeId, message: options.message, status: "active", createdAt: new Date().toISOString(), snapshotId: snapshot.id, autoGenerated: options.autoGenerated, }; // 변경 기록 추가 const changes = await readChanges(rootDir); changes.push(change); await writeChanges(rootDir, changes); // 활성 상태 업데이트 const activeState: TransactionState = { active: true, changeId: change.id, snapshotId: snapshot.id, }; await writeActiveState(rootDir, activeState); return change; } /** * 변경 트랜잭션 커밋 * - 현재 상태를 확정하고 트랜잭션 종료 */ export async function commitChange(rootDir: string, changeId?: string): Promise { const state = await readActiveState(rootDir); if (!state.active) { throw new Error("활성화된 트랜잭션이 없습니다"); } const targetChangeId = changeId || state.changeId; if (!targetChangeId) { throw new Error("커밋할 변경 ID를 찾을 수 없습니다"); } if (changeId && state.changeId !== changeId) { throw new Error(`지정된 변경(${changeId})이 현재 활성 트랜잭션(${state.changeId})과 다릅니다`); } // 변경 기록 업데이트 const changes = await readChanges(rootDir); const changeIndex = changes.findIndex((c) => c.id === targetChangeId); if (changeIndex === -1) { throw new Error(`변경 기록을 찾을 수 없습니다: ${targetChangeId}`); } const change = changes[changeIndex]; change.status = "committed"; await writeChanges(rootDir, changes); // 활성 상태 초기화 await writeActiveState(rootDir, { active: false, changeId: null, snapshotId: null, }); return { success: true, changeId: targetChangeId, message: change.message, }; } /** * 변경 트랜잭션 롤백 * - 스냅샷으로 복원하고 트랜잭션 종료 */ export async function rollbackChange(rootDir: string, changeId?: string): Promise { const state = await readActiveState(rootDir); // 활성 트랜잭션이 없으면 changeId로 찾기 let targetChangeId: string; let targetSnapshotId: string; if (state.active && state.changeId) { targetChangeId = changeId || state.changeId; if (changeId && state.changeId !== changeId) { throw new Error(`지정된 변경(${changeId})이 현재 활성 트랜잭션(${state.changeId})과 다릅니다`); } targetSnapshotId = state.snapshotId!; } else if (changeId) { // 활성 트랜잭션이 없지만 changeId가 지정된 경우 const changes = await readChanges(rootDir); const change = changes.find((c) => c.id === changeId); if (!change) { throw new Error(`변경 기록을 찾을 수 없습니다: ${changeId}`); } targetChangeId = changeId; targetSnapshotId = change.snapshotId; } else { throw new Error("롤백할 트랜잭션이 없습니다"); } // 스냅샷 읽기 const snapshot = await readSnapshotById(rootDir, targetSnapshotId); if (!snapshot) { throw new Error(`스냅샷을 찾을 수 없습니다: ${targetSnapshotId}`); } // 스냅샷으로 복원 const restoreResult = await restoreSnapshot(rootDir, snapshot); // 변경 기록 업데이트 const changes = await readChanges(rootDir); const changeIndex = changes.findIndex((c) => c.id === targetChangeId); if (changeIndex !== -1) { changes[changeIndex].status = "rolled_back"; await writeChanges(rootDir, changes); } // 활성 상태 초기화 await writeActiveState(rootDir, { active: false, changeId: null, snapshotId: null, }); return { success: restoreResult.success, changeId: targetChangeId, restoreResult, }; } /** * 트랜잭션 상태 조회 */ export async function getTransactionStatus( rootDir: string ): Promise<{ state: TransactionState; change: ChangeRecord | null }> { const state = await readActiveState(rootDir); const change = state.active ? await getActiveTransaction(rootDir) : null; return { state, change }; }