frieren
a self-hosted git server in one binary — everyone reads, only the owner writes
// Parses the `git show --stat --patch` text the backend returns into a
// structure the diff view can render with per-side line numbers.
export type DiffLine = {
kind: "add" | "del" | "ctx" | "hunk";
old: number | null;
new: number | null;
text: string;
};
export type DiffFile = {
path: string;
adds: number;
dels: number;
binary: boolean;
lines: DiffLine[];
};
export type ParsedPatch = { stat: string; files: DiffFile[] };
export function parsePatch(patch: string): ParsedPatch {
const idx = patch.indexOf("diff --git ");
const stat = (idx === -1 ? patch : patch.slice(0, idx)).trim();
const body = idx === -1 ? "" : patch.slice(idx);
const files: DiffFile[] = [];
let file: DiffFile | null = null;
let oldN = 0;
let newN = 0;
for (const line of body.split("\n")) {
if (line.startsWith("diff --git ")) {
// `diff --git a/path b/path` — take the b/ side.
const m = line.match(/ b\/(.*)$/);
file = { path: m ? m[1] : line, adds: 0, dels: 0, binary: false, lines: [] };
files.push(file);
continue;
}
if (!file) continue;
if (line.startsWith("Binary files ")) {
file.binary = true;
continue;
}
const hunk = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
if (hunk) {
oldN = parseInt(hunk[1], 10);
newN = parseInt(hunk[2], 10);
file.lines.push({ kind: "hunk", old: null, new: null, text: line });
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("similarity") || line.startsWith("rename ") ||
line.startsWith("\\ No newline")
) {
continue;
}
if (line.startsWith("+")) {
file.adds++;
file.lines.push({ kind: "add", old: null, new: newN++, text: line.slice(1) });
} else if (line.startsWith("-")) {
file.dels++;
file.lines.push({ kind: "del", old: oldN++, new: null, text: line.slice(1) });
} else {
file.lines.push({ kind: "ctx", old: oldN++, new: newN++, text: line.slice(1) });
}
}
return { stat, files };
}