// Minimal unified-diff / git format-patch parser for pull request render. // Handles the real `patch` field stored on sh.tangled.repo.pull records. export interface DiffLine { type: "add" | "del" | "ctx"; text: string; } export interface DiffHunk { header: string; lines: DiffLine[]; } export interface DiffFile { from: string; to: string; hunks: DiffHunk[]; binary: boolean; } function cleanPath(p: string): string { return p .replace(/^a\//, "") .replace(/^b\//, ""); } export function parsePatch(patch: string): DiffFile[] { if (!patch) return []; const files: DiffFile[] = []; let file: DiffFile | null = null; let hunk: DiffHunk | null = null; const lines = patch.replace(/\r\n/g, "\n").split("\n"); for (const raw of lines) { const line = raw; if (line.startsWith("diff --git ")) { if (file) files.push(file); file = { from: "", to: "", hunks: [], binary: false }; hunk = null; const m = /diff --git a\/(\S+) b\/(\S+)/.exec(line); file.from = m ? cleanPath(m[1]) : ""; file.to = m ? cleanPath(m[2]) : ""; continue; } if (!file) continue; if (line.startsWith("Binary files ") || line.startsWith("GIT binary patch")) { file.binary = true; continue; } if (line.startsWith("@@ ")) { hunk = { header: line, lines: [] }; file.hunks.push(hunk); continue; } if (hunk) { const first = line[0]; if (first === "+") { hunk.lines.push({ type: "add", text: line.slice(1) }); } else if (first === "-") { hunk.lines.push({ type: "del", text: line.slice(1) }); } else if (line === "\\ No newline at end of file") { hunk.lines.push({ type: "ctx", text: line }); } else { hunk.lines.push({ type: "ctx", text: line }); } } } if (file) files.push(file); return files; } export function patchStats(patch: string): { additions: number; deletions: number; files: number } { const stats = { additions: 0, deletions: 0, files: 0 }; for (const f of parsePatch(patch)) { stats.files += 1; for (const h of f.hunks) { for (const l of h.lines) { if (l.type === "add") stats.additions += 1; if (l.type === "del") stats.deletions += 1; } } } return stats; }