This repository has no description
1import type { ChangeContent, ContextContent, FileDiffMetadata, Hunk } from "@pierre/diffs";
2import { LineOp, type DiffFile } from "$lib/api/diff";
3
4// old alone collides when one file is deleted and another renamed onto its
5// path in the same commit so we need new too
6export const diffRowKey = (file: DiffFile): string => `${file.name.old}→${file.name.new}`;
7
8// pierre parses patch text into FileDiffMetadata, but the wire already hands
9// us hunks, so we can skip the text roundtrip by mapping
10export const toFileDiffMetadata = (file: DiffFile): FileDiffMetadata => {
11 const fragments = file.text_fragments ?? [];
12
13 const hunks: Hunk[] = [];
14 const deletionLines: string[] = [];
15 const additionLines: string[] = [];
16 let splitLineCount = 0;
17 let unifiedLineCount = 0;
18 let lastHunkEnd = 0;
19
20 for (const fragment of fragments) {
21 const additionStart = fragment.NewPosition ?? 0;
22 const additionCount = fragment.NewLines ?? 0;
23 const deletionStart = fragment.OldPosition ?? 0;
24 const deletionCount = fragment.OldLines ?? 0;
25
26 const collapsedBefore = Math.max(additionStart - 1 - lastHunkEnd, 0);
27 lastHunkEnd = additionStart + additionCount - 1;
28
29 const hunk: Hunk = {
30 collapsedBefore,
31 additionStart,
32 additionCount,
33 additionLines: 0,
34 additionLineIndex: additionLines.length,
35 deletionStart,
36 deletionCount,
37 deletionLines: 0,
38 deletionLineIndex: deletionLines.length,
39 hunkContent: [],
40 hunkContext: fragment.Comment,
41 hunkSpecs: `@@ -${deletionStart},${deletionCount} +${additionStart},${additionCount} @@`,
42 splitLineStart: 0,
43 splitLineCount: 0,
44 unifiedLineStart: 0,
45 unifiedLineCount: 0,
46 noEOFCRDeletions: false,
47 noEOFCRAdditions: false
48 };
49
50 let current: ContextContent | ChangeContent | null = null;
51 let lastOp: LineOp | null = null;
52 for (const { Op: op, Line: raw } of fragment.Lines ?? []) {
53 if (op === LineOp.Context) {
54 if (current?.type !== "context") {
55 current = {
56 type: "context",
57 lines: 0,
58 additionLineIndex: additionLines.length,
59 deletionLineIndex: deletionLines.length
60 };
61 hunk.hunkContent.push(current);
62 }
63 current.lines++;
64 additionLines.push(raw);
65 deletionLines.push(raw);
66 } else {
67 if (current?.type !== "change") {
68 current = {
69 type: "change",
70 additions: 0,
71 deletions: 0,
72 additionLineIndex: additionLines.length,
73 deletionLineIndex: deletionLines.length
74 };
75 hunk.hunkContent.push(current);
76 }
77 if (op === LineOp.Add) {
78 current.additions++;
79 hunk.additionLines++;
80 additionLines.push(raw);
81 } else {
82 current.deletions++;
83 hunk.deletionLines++;
84 deletionLines.push(raw);
85 }
86 }
87 lastOp = op;
88 }
89
90 // a line without a trailing newline is the patch's "\ No newline"
91 // marker, only possible at the end of the file
92 const last = fragment.Lines?.[fragment.Lines.length - 1];
93 if (last && !last.Line.endsWith("\n")) {
94 if (last.Op === LineOp.Context) {
95 hunk.noEOFCRAdditions = true;
96 hunk.noEOFCRDeletions = true;
97 } else if (last.Op === LineOp.Delete) {
98 hunk.noEOFCRDeletions = true;
99 } else {
100 hunk.noEOFCRAdditions = true;
101 }
102 if (lastOp === LineOp.Add || lastOp === LineOp.Context) {
103 additionLines[additionLines.length - 1] = last.Line;
104 }
105 if (lastOp === LineOp.Delete || lastOp === LineOp.Context) {
106 deletionLines[deletionLines.length - 1] = last.Line;
107 }
108 }
109
110 for (const content of hunk.hunkContent) {
111 if (content.type === "context") {
112 hunk.splitLineCount += content.lines;
113 hunk.unifiedLineCount += content.lines;
114 } else {
115 hunk.splitLineCount += Math.max(content.additions, content.deletions);
116 hunk.unifiedLineCount += content.deletions + content.additions;
117 }
118 }
119 hunk.splitLineStart = splitLineCount + collapsedBefore;
120 hunk.unifiedLineStart = unifiedLineCount + collapsedBefore;
121 splitLineCount += collapsedBefore + hunk.splitLineCount;
122 unifiedLineCount += collapsedBefore + hunk.unifiedLineCount;
123
124 hunks.push(hunk);
125 }
126
127 const type: FileDiffMetadata["type"] = file.is_new
128 ? "new"
129 : file.is_delete
130 ? "deleted"
131 : file.is_rename
132 ? "rename-changed"
133 : "change";
134
135 return {
136 name: file.name.new || file.name.old,
137 prevName: type === "rename-changed" ? file.name.old : undefined,
138 type,
139 hunks,
140 splitLineCount,
141 unifiedLineCount,
142 isPartial: true,
143 deletionLines,
144 additionLines
145 };
146};