2026-06-14 01:39:49 +03:00
|
|
|
import {
|
|
|
|
|
parseDiffFromFile,
|
|
|
|
|
parsePatchFiles,
|
|
|
|
|
processFile,
|
|
|
|
|
trimPatchContext,
|
|
|
|
|
type FileDiffMetadata,
|
|
|
|
|
} from '@pierre/diffs';
|
2026-06-13 23:55:50 +03:00
|
|
|
|
|
|
|
|
const PATCH_DIFF_CACHE_LIMIT = 64;
|
2026-06-14 01:39:49 +03:00
|
|
|
const DEFAULT_PATCH_CONTEXT_LINES = 3;
|
2026-06-13 23:55:50 +03:00
|
|
|
const patchFileDiffCache = new Map<string, FileDiffMetadata>();
|
|
|
|
|
|
2026-06-14 01:39:49 +03:00
|
|
|
export const fileDiffFromContent = (
|
|
|
|
|
file: string,
|
|
|
|
|
before: string,
|
|
|
|
|
after: string,
|
|
|
|
|
contextLines = DEFAULT_PATCH_CONTEXT_LINES
|
|
|
|
|
): FileDiffMetadata => {
|
2026-06-13 23:55:50 +03:00
|
|
|
if (!before && !after) {
|
|
|
|
|
return emptyFileDiff(file);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return parseDiffFromFile(
|
|
|
|
|
{ name: file, contents: before },
|
|
|
|
|
{ name: file, contents: after },
|
2026-06-14 01:39:49 +03:00
|
|
|
{ context: contextLines },
|
2026-06-13 23:55:50 +03:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-14 01:39:49 +03:00
|
|
|
export const fileDiffFromPatch = (
|
|
|
|
|
file: string,
|
|
|
|
|
patch: string,
|
|
|
|
|
contextLines = DEFAULT_PATCH_CONTEXT_LINES
|
|
|
|
|
): FileDiffMetadata => {
|
|
|
|
|
const key = `${file}\0${contextLines}\0${patch}`;
|
2026-06-13 23:55:50 +03:00
|
|
|
const cached = patchFileDiffCache.get(key);
|
|
|
|
|
if (cached) {
|
|
|
|
|
patchFileDiffCache.delete(key);
|
|
|
|
|
patchFileDiffCache.set(key, cached);
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const completeContents = completePatchContents(patch);
|
|
|
|
|
const value = completeContents
|
2026-06-14 01:39:49 +03:00
|
|
|
? (processFile(trimPatchContext(patch, contextLines), {
|
|
|
|
|
oldFile: { name: file, contents: completeContents.before },
|
|
|
|
|
newFile: { name: file, contents: completeContents.after },
|
|
|
|
|
}) ?? emptyFileDiff(file))
|
2026-06-13 23:55:50 +03:00
|
|
|
: (parsePatchFiles(withPatchHeader(file, patch))[0]?.files[0] ?? emptyFileDiff(file));
|
|
|
|
|
patchFileDiffCache.set(key, value);
|
|
|
|
|
|
|
|
|
|
while (patchFileDiffCache.size > PATCH_DIFF_CACHE_LIMIT) {
|
|
|
|
|
const firstKey = patchFileDiffCache.keys().next().value;
|
|
|
|
|
if (!firstKey) break;
|
|
|
|
|
patchFileDiffCache.delete(firstKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return value;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const withPatchHeader = (file: string, patch: string): string => {
|
|
|
|
|
if (!patch.trim()) {
|
|
|
|
|
return patch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (patch.startsWith('diff --git ') || /^--- [^\n]*\r?\n\+\+\+ /m.test(patch)) {
|
|
|
|
|
return patch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `Index: ${file}\n===================================================================\n--- ${file}\t\n+++ ${file}\t\n${patch}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const completePatchContents = (patch: string): { before: string; after: string } | undefined => {
|
|
|
|
|
if (!patch.startsWith('diff --git ') && !/^--- [^\n]*\t?\r?\n\+\+\+ [^\n]*\t?(?:\r?\n|$)/m.test(patch)) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hunkMatches = [...patch.matchAll(/^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@[^\n]*(?:\r?\n|$)/gm)];
|
|
|
|
|
if (hunkMatches.length !== 1) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hunk = hunkMatches[0];
|
|
|
|
|
const oldStart = Number.parseInt(hunk[1] ?? '', 10);
|
|
|
|
|
const newStart = Number.parseInt(hunk[2] ?? '', 10);
|
|
|
|
|
if (oldStart > 1 || newStart > 1 || !Number.isFinite(oldStart) || !Number.isFinite(newStart)) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hunkStart = (hunk.index ?? 0) + hunk[0].length;
|
|
|
|
|
const body = patch.slice(hunkStart);
|
|
|
|
|
const before: Array<{ text: string; newline: boolean }> = [];
|
|
|
|
|
const after: Array<{ text: string; newline: boolean }> = [];
|
|
|
|
|
let previous: '-' | '+' | ' ' | undefined;
|
|
|
|
|
|
|
|
|
|
for (const rawLine of body.split(/\r?\n/)) {
|
|
|
|
|
if (rawLine.startsWith('diff --git ') || rawLine.startsWith('@@ ')) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rawLine.startsWith('\\')) {
|
|
|
|
|
if (previous === '-' || previous === ' ') {
|
|
|
|
|
const value = before.at(-1);
|
|
|
|
|
if (value) value.newline = false;
|
|
|
|
|
}
|
|
|
|
|
if (previous === '+' || previous === ' ') {
|
|
|
|
|
const value = after.at(-1);
|
|
|
|
|
if (value) value.newline = false;
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rawLine.startsWith('-')) {
|
|
|
|
|
before.push({ text: rawLine.slice(1), newline: true });
|
|
|
|
|
previous = '-';
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rawLine.startsWith('+')) {
|
|
|
|
|
after.push({ text: rawLine.slice(1), newline: true });
|
|
|
|
|
previous = '+';
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!rawLine.startsWith(' ')) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
before.push({ text: rawLine.slice(1), newline: true });
|
|
|
|
|
after.push({ text: rawLine.slice(1), newline: true });
|
|
|
|
|
previous = ' ';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
before: joinPatchLines(before),
|
|
|
|
|
after: joinPatchLines(after),
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const joinPatchLines = (lines: Array<{ text: string; newline: boolean }>): string =>
|
|
|
|
|
lines.map((line) => line.text + (line.newline ? '\n' : '')).join('');
|
|
|
|
|
|
|
|
|
|
const emptyFileDiff = (file: string): FileDiffMetadata =>
|
|
|
|
|
parseDiffFromFile({ name: file, contents: '' }, { name: file, contents: '' });
|