import { isWindowsishPath, normalizePathForComparison } from '@shared/utils/platformPath'; import type { FileChangeSummary, HunkDecision } from '@shared/types'; function normalizeReviewPath(filePath: string, forceCaseInsensitive = false): string { const normalized = normalizePathForComparison(filePath); return forceCaseInsensitive || isWindowsReviewPath(filePath) ? normalized.toLowerCase() : normalized; } function isWindowsReviewPath(filePath: string): boolean { return isWindowsishPath(filePath) || filePath.includes('\\'); } function normalizeReviewAlias(alias: string, forceCaseInsensitive = false): string { const slashNormalized = alias.replace(/\\/g, '/'); const caseInsensitive = forceCaseInsensitive || alias.includes('\\'); const relationMatch = /^(rename|copy):(.+)->(.+)$/.exec(slashNormalized); if (relationMatch) { const oldPath = normalizeReviewPath(relationMatch[2] ?? '', caseInsensitive); const newPath = normalizeReviewPath(relationMatch[3] ?? '', caseInsensitive); return `${relationMatch[1]}:${oldPath}->${newPath}`; } const pathKeyMatch = /^(path|create|delete):(.+)$/.exec(slashNormalized); if (pathKeyMatch) { return `${pathKeyMatch[1]}:${normalizeReviewPath(pathKeyMatch[2] ?? '', caseInsensitive)}`; } return normalizeReviewPath(alias, caseInsensitive); } export function getFileReviewKey(file: Pick): string { return file.changeKey ?? file.filePath; } export function getReviewKeyForFilePath( files: readonly Pick[] | null | undefined, filePath: string ): string { const file = files?.find((candidate) => reviewPathsEqual(candidate.filePath, filePath)); return file ? getFileReviewKey(file) : filePath; } function reviewPathsEqual(left: string, right: string): boolean { const caseInsensitive = isWindowsReviewPath(left) || isWindowsReviewPath(right); return normalizeReviewPath(left, caseInsensitive) === normalizeReviewPath(right, caseInsensitive); } export function buildHunkDecisionKey(reviewKey: string, index: number): string { return `${reviewKey}:${index}`; } export function parseHunkDecisionKey(key: string): { reviewKey: string; index: number } | null { const match = /^(.*):(\d+)$/.exec(key); if (!match) { return null; } return { reviewKey: match[1] ?? '', index: Number.parseInt(match[2] ?? '', 10), }; } export function normalizePersistedReviewState( files: readonly Pick[], state: { fileDecisions?: Record; hunkDecisions?: Record; hunkContextHashesByFile?: Record>; } ): { fileDecisions: Record; hunkDecisions: Record; hunkContextHashesByFile: Record>; } { const reviewKeyByAlias = new Map(); const caseInsensitiveAliases = new Set(); const addAlias = (alias: string, reviewKey: string, forceCaseInsensitive = false): void => { reviewKeyByAlias.set(alias, reviewKey); const normalized = normalizeReviewAlias(alias, forceCaseInsensitive); reviewKeyByAlias.set(normalized, reviewKey); if (forceCaseInsensitive) { caseInsensitiveAliases.add(normalized); } }; const resolveReviewKey = (alias: string): string | undefined => { const caseInsensitiveAlias = normalizeReviewAlias(alias, true); return ( reviewKeyByAlias.get(alias) ?? reviewKeyByAlias.get(normalizeReviewAlias(alias)) ?? (caseInsensitiveAliases.has(caseInsensitiveAlias) ? reviewKeyByAlias.get(caseInsensitiveAlias) : undefined) ); }; for (const file of files) { const reviewKey = getFileReviewKey(file); const forceCaseInsensitive = isWindowsReviewPath(file.filePath); addAlias(reviewKey, reviewKey, forceCaseInsensitive); addAlias(file.filePath, reviewKey, forceCaseInsensitive); } const fileDecisions: Record = {}; for (const [key, decision] of Object.entries(state.fileDecisions ?? {})) { const reviewKey = resolveReviewKey(key); if (reviewKey) { fileDecisions[reviewKey] = decision; } } const hunkDecisions: Record = {}; for (const [key, decision] of Object.entries(state.hunkDecisions ?? {})) { const parsed = parseHunkDecisionKey(key); if (!parsed) { continue; } const reviewKey = resolveReviewKey(parsed.reviewKey); if (reviewKey) { hunkDecisions[buildHunkDecisionKey(reviewKey, parsed.index)] = decision; } } const hunkContextHashesByFile: Record> = {}; for (const [key, hashes] of Object.entries(state.hunkContextHashesByFile ?? {})) { const reviewKey = resolveReviewKey(key); if (reviewKey) { hunkContextHashesByFile[reviewKey] = hashes; } } return { fileDecisions, hunkDecisions, hunkContextHashesByFile }; }