Repository Public
1/**2 * Reconstruct the ORIGINAL text of a file from its current text and a unified diff3 * (`git diff` output for that single file), by reverse-applying every hunk.4 * Returns null when the diff cannot be applied cleanly (caller falls back to raw diff text).5 */6export function reverseApplyUnifiedDiff(modified: string, diff: string): string | null {7 const lines = diff.split('\n');8 interface Hunk { newStart: number; newCount: number; oldLines: string[]; newLines: string[] }9 const hunks: Hunk[] = [];10 let cur: Hunk | null = null;11 let oldNoEol = false;12 let newNoEol = false;13 let last: '-' | '+' | ' ' | null = null;14 for (const line of lines) {15 const m = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);16 if (m) {17 cur = { newStart: Number(m[1]), newCount: m[2] === undefined ? 1 : Number(m[2]), oldLines: [], newLines: [] };18 hunks.push(cur);19 last = null;20 continue;21 }22 if (!cur) continue;23 if (line.startsWith('\\')) {24 if (last === '-') oldNoEol = true;25 else if (last === '+') newNoEol = true;26 else oldNoEol = newNoEol = true;27 continue;28 }29 const tag = line[0];30 if (tag === ' ') {31 cur.oldLines.push(line.slice(1));32 cur.newLines.push(line.slice(1));33 last = ' ';34 } else if (tag === '-') {35 cur.oldLines.push(line.slice(1));36 last = '-';37 } else if (tag === '+') {38 cur.newLines.push(line.slice(1));39 last = '+';40 } else if (line === '' ) {41 continue; // trailing split artefact42 } else {43 cur = null; // next file header or garbage44 }45 }46 if (hunks.length === 0) return diff.trim() === '' ? modified : null;47 const endsWithEol = modified.endsWith('\n');48 const body = modified === '' ? [] : (endsWithEol ? modified.slice(0, -1) : modified).split('\n');49 for (const h of [...hunks].reverse()) {50 const idx = h.newCount === 0 ? h.newStart : h.newStart - 1;51 const present = body.slice(idx, idx + h.newLines.length);52 if (present.length !== h.newLines.length || present.some((l, i) => l !== h.newLines[i])) return null;53 body.splice(idx, h.newLines.length, ...h.oldLines);54 }55 if (body.length === 0) return '';56 const origEol = oldNoEol ? false : newNoEol ? true : endsWithEol;57 return body.join('\n') + (origEol ? '\n' : '');58}59