CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | export type DiffLine = {
type: "add" | "del" | "ctx" | "hunk";
content: string;
oldLine: number | null;
newLine: number | null;
wsOnly?: boolean;
};
export type ParsedFile = {
path: string;
additions: number;
deletions: number;
isBinary: boolean;
lines: DiffLine[];
};
export type SplitRow = { left: DiffLine | null; right: DiffLine | null };
export function parseDiff(raw: string): ParsedFile[] {
const files: ParsedFile[] = [];
const diffRe = /^diff --git a\/(.+?) b\/.+$/;
const hunkRe = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
let current: ParsedFile | null = null;
let oldLine = 0;
let newLine = 0;
for (const line of raw.replace(/\r\n/g, "\n").split("\n")) {
if (line === "") continue; // skip trailing split artifacts
const dm = line.match(diffRe);
if (dm) {
if (current) files.push(current);
current = { path: dm[1], additions: 0, deletions: 0, isBinary: false, lines: [] };
oldLine = 0;
newLine = 0;
continue;
}
if (!current) continue;
if (
line.startsWith("index ") ||
line.startsWith("--- ") ||
line.startsWith("+++ ") ||
line.startsWith("new file") ||
line.startsWith("deleted file") ||
line.startsWith("old mode") ||
line.startsWith("new mode") ||
line.startsWith("rename ")
) continue;
if (line.startsWith("Binary files")) {
current.isBinary = true;
continue;
}
if (current.isBinary) continue;
const hm = line.match(hunkRe);
if (hm) {
oldLine = parseInt(hm[1], 10);
newLine = parseInt(hm[2], 10);
current.lines.push({ type: "hunk", content: line, oldLine: null, newLine: null });
continue;
}
if (line.startsWith("+")) {
current.additions++;
current.lines.push({ type: "add", content: line.slice(1), oldLine: null, newLine: newLine++ });
} else if (line.startsWith("-")) {
current.deletions++;
current.lines.push({ type: "del", content: line.slice(1), oldLine: oldLine++, newLine: null });
} else {
current.lines.push({ type: "ctx", content: line.length > 0 ? line.slice(1) : "", oldLine: oldLine++, newLine: newLine++ });
}
}
if (current) files.push(current);
// Whitespace-only detection: scan adjacent del/add pairs
for (const file of files) {
let i = 0;
while (i < file.lines.length) {
if (file.lines[i].type !== "del") { i++; continue; }
const delStart = i;
while (i < file.lines.length && file.lines[i].type === "del") i++;
const addStart = i;
while (i < file.lines.length && file.lines[i].type === "add") i++;
const pairCount = Math.min(i - addStart, addStart - delStart);
for (let j = 0; j < pairCount; j++) {
const del = file.lines[delStart + j];
const add = file.lines[addStart + j];
if (del.content !== add.content && del.content.trim() === add.content.trim()) {
del.wsOnly = true;
add.wsOnly = true;
}
}
}
}
return files;
}
export function pairLines(lines: DiffLine[]): SplitRow[] {
const rows: SplitRow[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
if (line.type === "ctx" || line.type === "hunk") {
rows.push({ left: line, right: line });
i++;
} else if (line.type === "del") {
const dels: DiffLine[] = [];
const adds: DiffLine[] = [];
while (i < lines.length && lines[i].type === "del") dels.push(lines[i++]);
while (i < lines.length && lines[i].type === "add") adds.push(lines[i++]);
const len = Math.max(dels.length, adds.length);
for (let j = 0; j < len; j++) rows.push({ left: dels[j] ?? null, right: adds[j] ?? null });
} else {
rows.push({ left: null, right: lines[i++] });
}
}
return rows;
}
|