import type { ChangeContent, ContextContent, FileDiffMetadata, Hunk } from "@pierre/diffs"; import { LineOp, type DiffFile } from "$lib/api/diff"; // old alone collides when one file is deleted and another renamed onto its // path in the same commit so we need new too export const diffRowKey = (file: DiffFile): string => `${file.name.old}→${file.name.new}`; // pierre parses patch text into FileDiffMetadata, but the wire already hands // us hunks, so we can skip the text roundtrip by mapping export const toFileDiffMetadata = (file: DiffFile): FileDiffMetadata => { const fragments = file.text_fragments ?? []; const hunks: Hunk[] = []; const deletionLines: string[] = []; const additionLines: string[] = []; let splitLineCount = 0; let unifiedLineCount = 0; let lastHunkEnd = 0; for (const fragment of fragments) { const additionStart = fragment.NewPosition ?? 0; const additionCount = fragment.NewLines ?? 0; const deletionStart = fragment.OldPosition ?? 0; const deletionCount = fragment.OldLines ?? 0; const collapsedBefore = Math.max(additionStart - 1 - lastHunkEnd, 0); lastHunkEnd = additionStart + additionCount - 1; const hunk: Hunk = { collapsedBefore, additionStart, additionCount, additionLines: 0, additionLineIndex: additionLines.length, deletionStart, deletionCount, deletionLines: 0, deletionLineIndex: deletionLines.length, hunkContent: [], hunkContext: fragment.Comment, hunkSpecs: `@@ -${deletionStart},${deletionCount} +${additionStart},${additionCount} @@`, splitLineStart: 0, splitLineCount: 0, unifiedLineStart: 0, unifiedLineCount: 0, noEOFCRDeletions: false, noEOFCRAdditions: false }; let current: ContextContent | ChangeContent | null = null; let lastOp: LineOp | null = null; for (const { Op: op, Line: raw } of fragment.Lines ?? []) { if (op === LineOp.Context) { if (current?.type !== "context") { current = { type: "context", lines: 0, additionLineIndex: additionLines.length, deletionLineIndex: deletionLines.length }; hunk.hunkContent.push(current); } current.lines++; additionLines.push(raw); deletionLines.push(raw); } else { if (current?.type !== "change") { current = { type: "change", additions: 0, deletions: 0, additionLineIndex: additionLines.length, deletionLineIndex: deletionLines.length }; hunk.hunkContent.push(current); } if (op === LineOp.Add) { current.additions++; hunk.additionLines++; additionLines.push(raw); } else { current.deletions++; hunk.deletionLines++; deletionLines.push(raw); } } lastOp = op; } // a line without a trailing newline is the patch's "\ No newline" // marker, only possible at the end of the file const last = fragment.Lines?.[fragment.Lines.length - 1]; if (last && !last.Line.endsWith("\n")) { if (last.Op === LineOp.Context) { hunk.noEOFCRAdditions = true; hunk.noEOFCRDeletions = true; } else if (last.Op === LineOp.Delete) { hunk.noEOFCRDeletions = true; } else { hunk.noEOFCRAdditions = true; } if (lastOp === LineOp.Add || lastOp === LineOp.Context) { additionLines[additionLines.length - 1] = last.Line; } if (lastOp === LineOp.Delete || lastOp === LineOp.Context) { deletionLines[deletionLines.length - 1] = last.Line; } } for (const content of hunk.hunkContent) { if (content.type === "context") { hunk.splitLineCount += content.lines; hunk.unifiedLineCount += content.lines; } else { hunk.splitLineCount += Math.max(content.additions, content.deletions); hunk.unifiedLineCount += content.deletions + content.additions; } } hunk.splitLineStart = splitLineCount + collapsedBefore; hunk.unifiedLineStart = unifiedLineCount + collapsedBefore; splitLineCount += collapsedBefore + hunk.splitLineCount; unifiedLineCount += collapsedBefore + hunk.unifiedLineCount; hunks.push(hunk); } const type: FileDiffMetadata["type"] = file.is_new ? "new" : file.is_delete ? "deleted" : file.is_rename ? "rename-changed" : "change"; return { name: file.name.new || file.name.old, prevName: type === "rename-changed" ? file.name.old : undefined, type, hunks, splitLineCount, unifiedLineCount, isPartial: true, deletionLines, additionLines }; };