Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

diff.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

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