1 | module.exports = (input) => {
|
2 | if (!input) return [];
|
3 | if (typeof input !== "string" || input.match(/^\s+$/)) return [];
|
4 |
|
5 | const lines = input.split("\n");
|
6 | if (lines.length === 0) return [];
|
7 |
|
8 | const files = [];
|
9 | let currentFile = null;
|
10 | let currentChunk = null;
|
11 | let deletedLineCounter = 0;
|
12 | let addedLineCounter = 0;
|
13 | let currentFileChanges = null;
|
14 |
|
15 | const normal = (line) => {
|
16 | currentChunk?.changes.push({
|
17 | type: "normal",
|
18 | normal: true,
|
19 | ln1: deletedLineCounter++,
|
20 | ln2: addedLineCounter++,
|
21 | content: line,
|
22 | });
|
23 | currentFileChanges.oldLines--;
|
24 | currentFileChanges.newLines--;
|
25 | };
|
26 |
|
27 | const start = (line) => {
|
28 | const [fromFileName, toFileName] = parseFiles(line) ?? [];
|
29 |
|
30 | currentFile = {
|
31 | chunks: [],
|
32 | deletions: 0,
|
33 | additions: 0,
|
34 | from: fromFileName,
|
35 | to: toFileName,
|
36 | };
|
37 |
|
38 | files.push(currentFile);
|
39 | };
|
40 |
|
41 | const restart = () => {
|
42 | if (!currentFile || currentFile.chunks.length) start();
|
43 | };
|
44 |
|
45 | const newFile = () => {
|
46 | restart();
|
47 | currentFile.new = true;
|
48 | currentFile.from = "/dev/null";
|
49 | };
|
50 |
|
51 | const deletedFile = () => {
|
52 | restart();
|
53 | currentFile.deleted = true;
|
54 | currentFile.to = "/dev/null";
|
55 | };
|
56 |
|
57 | const index = (line) => {
|
58 | restart();
|
59 | currentFile.index = line.split(" ").slice(1);
|
60 | };
|
61 |
|
62 | const fromFile = (line) => {
|
63 | restart();
|
64 | currentFile.from = parseOldOrNewFile(line);
|
65 | };
|
66 |
|
67 | const toFile = (line) => {
|
68 | restart();
|
69 | currentFile.to = parseOldOrNewFile(line);
|
70 | };
|
71 |
|
72 | const toNumOfLines = (number) => +(number || 1);
|
73 |
|
74 | const chunk = (line, match) => {
|
75 | if (!currentFile) return;
|
76 |
|
77 | const [oldStart, oldNumLines, newStart, newNumLines] = match.slice(1);
|
78 |
|
79 | deletedLineCounter = +oldStart;
|
80 | addedLineCounter = +newStart;
|
81 | currentChunk = {
|
82 | content: line,
|
83 | changes: [],
|
84 | oldStart: +oldStart,
|
85 | oldLines: toNumOfLines(oldNumLines),
|
86 | newStart: +newStart,
|
87 | newLines: toNumOfLines(newNumLines),
|
88 | };
|
89 | currentFileChanges = {
|
90 | oldLines: toNumOfLines(oldNumLines),
|
91 | newLines: toNumOfLines(newNumLines),
|
92 | };
|
93 | currentFile.chunks.push(currentChunk);
|
94 | };
|
95 |
|
96 | const del = (line) => {
|
97 | if (!currentChunk) return;
|
98 |
|
99 | currentChunk.changes.push({
|
100 | type: "del",
|
101 | del: true,
|
102 | ln: deletedLineCounter++,
|
103 | content: line,
|
104 | });
|
105 | currentFile.deletions++;
|
106 | currentFileChanges.oldLines--;
|
107 | };
|
108 |
|
109 | const add = (line) => {
|
110 | if (!currentChunk) return;
|
111 |
|
112 | currentChunk.changes.push({
|
113 | type: "add",
|
114 | add: true,
|
115 | ln: addedLineCounter++,
|
116 | content: line,
|
117 | });
|
118 | currentFile.additions++;
|
119 | currentFileChanges.newLines--;
|
120 | };
|
121 |
|
122 | const eof = (line) => {
|
123 | if (!currentChunk) return;
|
124 |
|
125 | const [mostRecentChange] = currentChunk.changes.slice(-1);
|
126 |
|
127 | currentChunk.changes.push({
|
128 | type: mostRecentChange.type,
|
129 | [mostRecentChange.type]: true,
|
130 | ln1: mostRecentChange.ln1,
|
131 | ln2: mostRecentChange.ln2,
|
132 | ln: mostRecentChange.ln,
|
133 | content: line,
|
134 | });
|
135 | };
|
136 |
|
137 | const schemaHeaders = [
|
138 | [/^diff\s/, start],
|
139 | [/^new file mode \d+$/, newFile],
|
140 | [/^deleted file mode \d+$/, deletedFile],
|
141 | [/^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$/, index],
|
142 | [/^---\s/, fromFile],
|
143 | [/^\+\+\+\s/, toFile],
|
144 | [/^@@\s+-(\d+),?(\d+)?\s+\+(\d+),?(\d+)?\s@@/, chunk],
|
145 | [/^\\ No newline at end of file$/, eof],
|
146 | ];
|
147 |
|
148 | const schemaContent = [
|
149 | [/^-/, del],
|
150 | [/^\+/, add],
|
151 | [/^\s+/, normal],
|
152 | ];
|
153 |
|
154 | const parseContentLine = (line) => {
|
155 | for (const [pattern, handler] of schemaContent) {
|
156 | const match = line.match(pattern);
|
157 | if (match) {
|
158 | handler(line, match);
|
159 | break;
|
160 | }
|
161 | }
|
162 | if (
|
163 | currentFileChanges.oldLines === 0 &&
|
164 | currentFileChanges.newLines === 0
|
165 | ) {
|
166 | currentFileChanges = null;
|
167 | }
|
168 | };
|
169 |
|
170 | const parseHeaderLine = (line) => {
|
171 | for (const [pattern, handler] of schemaHeaders) {
|
172 | const match = line.match(pattern);
|
173 | if (match) {
|
174 | handler(line, match);
|
175 | break;
|
176 | }
|
177 | }
|
178 | };
|
179 |
|
180 | const parseLine = (line) => {
|
181 | if (currentFileChanges) {
|
182 | parseContentLine(line);
|
183 | } else {
|
184 | parseHeaderLine(line);
|
185 | }
|
186 | return;
|
187 | };
|
188 |
|
189 | for (const line of lines) parseLine(line);
|
190 |
|
191 | return files;
|
192 | };
|
193 |
|
194 | const fileNameDiffRegex =
|
195 | /(a|i|w|c|o|1|2)\/.*(?=["']? ["']?(b|i|w|c|o|1|2)\/)|(b|i|w|c|o|1|2)\/.*$/g;
|
196 | const gitFileHeaderRegex = /^(a|b|i|w|c|o|1|2)\//;
|
197 | const parseFiles = (line) => {
|
198 | let fileNames = line?.match(fileNameDiffRegex);
|
199 | return fileNames?.map((fileName) =>
|
200 | fileName.replace(gitFileHeaderRegex, "").replace(/("|')$/, "")
|
201 | );
|
202 | };
|
203 |
|
204 | const qoutedFileNameRegex = /^\\?['"]|\\?['"]$/g;
|
205 | const parseOldOrNewFile = (line) => {
|
206 | let fileName = leftTrimChars(line, "-+").trim();
|
207 | fileName = removeTimeStamp(fileName);
|
208 | return fileName
|
209 | .replace(qoutedFileNameRegex, "")
|
210 | .replace(gitFileHeaderRegex, "");
|
211 | };
|
212 |
|
213 | const leftTrimChars = (string, trimmingChars) => {
|
214 | string = makeString(string);
|
215 | if (!trimmingChars && String.prototype.trimLeft) return string.trimLeft();
|
216 |
|
217 | let trimmingString = formTrimmingString(trimmingChars);
|
218 |
|
219 | return string.replace(new RegExp(`^${trimmingString}+`), "");
|
220 | };
|
221 |
|
222 | const timeStampRegex =
|
223 | /\t.*|\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d(.\d+)?\s(\+|-)\d\d\d\d/;
|
224 | const removeTimeStamp = (string) => {
|
225 | const timeStamp = timeStampRegex.exec(string);
|
226 | if (timeStamp) {
|
227 | string = string.substring(0, timeStamp.index).trim();
|
228 | }
|
229 | return string;
|
230 | };
|
231 |
|
232 | const formTrimmingString = (trimmingChars) => {
|
233 | if (trimmingChars === null || trimmingChars === undefined) return "\\s";
|
234 | else if (trimmingChars instanceof RegExp) return trimmingChars.source;
|
235 | return `[${makeString(trimmingChars).replace(
|
236 | /([.*+?^=!:${}()|[\]/\\])/g,
|
237 | "\\$1"
|
238 | )}]`;
|
239 | };
|
240 |
|
241 | const makeString = (itemToConvert) => (itemToConvert ?? "") + "";
|