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