231 lines
5.6 KiB
JavaScript
231 lines
5.6 KiB
JavaScript
export const parseDiff = (input) => {
|
|
if (!input) return [];
|
|
if (typeof input !== "string" || input.match(/^\s+$/)) return [];
|
|
|
|
const lines = input.split("\n");
|
|
if (lines.length === 0) return [];
|
|
|
|
const files = [];
|
|
let currentFile = null;
|
|
let currentChunk = null;
|
|
let deletedLineCounter = 0;
|
|
let addedLineCounter = 0;
|
|
let currentFileChanges = null;
|
|
|
|
const normal = (line) => {
|
|
currentChunk?.changes.push({
|
|
type: "normal",
|
|
normal: true,
|
|
ln1: deletedLineCounter++,
|
|
ln2: addedLineCounter++,
|
|
content: line,
|
|
});
|
|
currentFileChanges.oldLines--;
|
|
currentFileChanges.newLines--;
|
|
};
|
|
|
|
const start = (line) => {
|
|
const [fromFileName, toFileName] = parseFiles(line) ?? [];
|
|
|
|
currentFile = {
|
|
chunks: [],
|
|
deletions: 0,
|
|
additions: 0,
|
|
from: fromFileName,
|
|
to: toFileName,
|
|
};
|
|
|
|
files.push(currentFile);
|
|
};
|
|
|
|
const restart = () => {
|
|
if (!currentFile || currentFile.chunks.length) start();
|
|
};
|
|
|
|
const newFile = () => {
|
|
restart();
|
|
currentFile.new = true;
|
|
currentFile.from = "/dev/null";
|
|
};
|
|
|
|
const deletedFile = () => {
|
|
restart();
|
|
currentFile.deleted = true;
|
|
currentFile.to = "/dev/null";
|
|
};
|
|
|
|
const index = (line) => {
|
|
restart();
|
|
currentFile.index = line.split(" ").slice(1);
|
|
};
|
|
|
|
const fromFile = (line) => {
|
|
restart();
|
|
currentFile.from = parseOldOrNewFile(line);
|
|
};
|
|
|
|
const toFile = (line) => {
|
|
restart();
|
|
currentFile.to = parseOldOrNewFile(line);
|
|
};
|
|
|
|
const toNumOfLines = (number) => +(number || 1);
|
|
|
|
const chunk = (line, match) => {
|
|
if (!currentFile) return;
|
|
|
|
const [oldStart, oldNumLines, newStart, newNumLines] = match.slice(1);
|
|
|
|
deletedLineCounter = +oldStart;
|
|
addedLineCounter = +newStart;
|
|
currentChunk = {
|
|
content: line,
|
|
changes: [],
|
|
oldStart: +oldStart,
|
|
oldLines: toNumOfLines(oldNumLines),
|
|
newStart: +newStart,
|
|
newLines: toNumOfLines(newNumLines),
|
|
};
|
|
currentFileChanges = {
|
|
oldLines: toNumOfLines(oldNumLines),
|
|
newLines: toNumOfLines(newNumLines),
|
|
};
|
|
currentFile.chunks.push(currentChunk);
|
|
};
|
|
|
|
const del = (line) => {
|
|
if (!currentChunk) return;
|
|
|
|
currentChunk.changes.push({
|
|
type: "del",
|
|
del: true,
|
|
ln: deletedLineCounter++,
|
|
content: line,
|
|
});
|
|
currentFile.deletions++;
|
|
currentFileChanges.oldLines--;
|
|
};
|
|
|
|
const add = (line) => {
|
|
if (!currentChunk) return;
|
|
|
|
currentChunk.changes.push({
|
|
type: "add",
|
|
add: true,
|
|
ln: addedLineCounter++,
|
|
content: line,
|
|
});
|
|
currentFile.additions++;
|
|
currentFileChanges.newLines--;
|
|
};
|
|
|
|
const eof = (line) => {
|
|
if (!currentChunk) return;
|
|
|
|
const [mostRecentChange] = currentChunk.changes.slice(-1);
|
|
|
|
currentChunk.changes.push({
|
|
type: mostRecentChange.type,
|
|
[mostRecentChange.type]: true,
|
|
ln1: mostRecentChange.ln1,
|
|
ln2: mostRecentChange.ln2,
|
|
ln: mostRecentChange.ln,
|
|
content: line,
|
|
});
|
|
};
|
|
|
|
const schemaHeaders = [
|
|
[/^diff\s/, start],
|
|
[/^new file mode \d+$/, newFile],
|
|
[/^deleted file mode \d+$/, deletedFile],
|
|
[/^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$/, index],
|
|
[/^---\s/, fromFile],
|
|
[/^\+\+\+\s/, toFile],
|
|
[/^@@\s+-(\d+),?(\d+)?\s+\+(\d+),?(\d+)?\s@@/, chunk],
|
|
[/^\\ No newline at end of file$/, eof],
|
|
];
|
|
|
|
const schemaContent = [
|
|
[/^-/, del],
|
|
[/^\+/, add],
|
|
[/^\s+/, normal],
|
|
];
|
|
|
|
const parseContentLine = (line) => {
|
|
for (const [pattern, handler] of schemaContent) {
|
|
const match = line.match(pattern);
|
|
if (match) {
|
|
handler(line, match);
|
|
break;
|
|
}
|
|
}
|
|
if (currentFileChanges.oldLines === 0 && currentFileChanges.newLines === 0) {
|
|
currentFileChanges = null;
|
|
}
|
|
};
|
|
|
|
const parseHeaderLine = (line) => {
|
|
for (const [pattern, handler] of schemaHeaders) {
|
|
const match = line.match(pattern);
|
|
if (match) {
|
|
handler(line, match);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
const parseLine = (line) => {
|
|
if (currentFileChanges) {
|
|
parseContentLine(line);
|
|
} else {
|
|
parseHeaderLine(line);
|
|
}
|
|
return;
|
|
};
|
|
|
|
for (const line of lines) parseLine(line);
|
|
|
|
return files;
|
|
};
|
|
|
|
const fileNameDiffRegex = /a\/.*(?=["']? ["']?b\/)|b\/.*$/g;
|
|
const gitFileHeaderRegex = /^(a|b)\//;
|
|
const parseFiles = (line) => {
|
|
let fileNames = line?.match(fileNameDiffRegex);
|
|
return fileNames?.map((fileName) => fileName.replace(gitFileHeaderRegex, "").replace(/("|')$/, ""));
|
|
};
|
|
|
|
const qoutedFileNameRegex = /^\\?['"]|\\?['"]$/g;
|
|
const parseOldOrNewFile = (line) => {
|
|
let fileName = leftTrimChars(line, "-+").trim();
|
|
fileName = removeTimeStamp(fileName);
|
|
return fileName.replace(qoutedFileNameRegex, "").replace(gitFileHeaderRegex, "");
|
|
};
|
|
|
|
const leftTrimChars = (string, trimmingChars) => {
|
|
string = makeString(string);
|
|
if (!trimmingChars && String.prototype.trimLeft) return string.trimLeft();
|
|
|
|
let trimmingString = formTrimmingString(trimmingChars);
|
|
|
|
return string.replace(new RegExp(`^${trimmingString}+`), "");
|
|
};
|
|
|
|
const timeStampRegex = /\t.*|\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d(.\d+)?\s(\+|-)\d\d\d\d/;
|
|
const removeTimeStamp = (string) => {
|
|
const timeStamp = timeStampRegex.exec(string);
|
|
if (timeStamp) {
|
|
string = string.substring(0, timeStamp.index).trim();
|
|
}
|
|
return string;
|
|
};
|
|
|
|
const formTrimmingString = (trimmingChars) => {
|
|
if (trimmingChars === null || trimmingChars === undefined) return "\\s";
|
|
else if (trimmingChars instanceof RegExp) return trimmingChars.source;
|
|
return `[${makeString(trimmingChars).replace(/([.*+?^=!:${}()|[\]/\\])/g, "\\$1")}]`;
|
|
};
|
|
|
|
const makeString = (itemToConvert) => (itemToConvert ?? "") + "";
|
|
|
|
export default parseDiff;
|