513 lines
23 KiB
JavaScript
513 lines
23 KiB
JavaScript
|
|
/**
|
||
|
|
* Parses a unified diff format patch into a structured patch object.
|
||
|
|
*
|
||
|
|
* `parsePatch` has some understanding of Git's particular dialect of unified diff format.
|
||
|
|
* When parsing a Git patch, each index in the result may contain additional
|
||
|
|
* fields (`isRename`, `isBinary`, etc) not included in the data structure returned by
|
||
|
|
* `structuredPatch`; see the `StructuredPatch` interface for a full list.
|
||
|
|
*
|
||
|
|
* @return a JSON object representation of the patch, suitable for use with the `applyPatch`
|
||
|
|
* method. This parses to the same structure returned by `structuredPatch`, except that
|
||
|
|
* `oldFileName` and `newFileName` may be `undefined` if the patch doesn't contain enough
|
||
|
|
* information to determine them (e.g. a hunk-only patch with no file headers).
|
||
|
|
*/
|
||
|
|
export function parsePatch(uniDiff) {
|
||
|
|
const diffstr = uniDiff.split(/\n/), list = [];
|
||
|
|
let i = 0;
|
||
|
|
// These helper functions identify line types that can appear between files
|
||
|
|
// in a multi-file patch. Keeping them in one place avoids subtle
|
||
|
|
// inconsistencies from having the same regexes duplicated in multiple places.
|
||
|
|
// Matches `diff --git ...` lines specifically.
|
||
|
|
function isGitDiffHeader(line) {
|
||
|
|
return (/^diff --git /).test(line);
|
||
|
|
}
|
||
|
|
// Matches lines that denote the start of a new diff's section in a
|
||
|
|
// multi-file patch: `diff --git ...`, `Index: ...`, or `diff -r ...`.
|
||
|
|
function isDiffHeader(line) {
|
||
|
|
return isGitDiffHeader(line)
|
||
|
|
|| (/^Index:\s/).test(line)
|
||
|
|
|| (/^diff(?: -r \w+)+\s/).test(line);
|
||
|
|
}
|
||
|
|
// Matches `--- ...` and `+++ ...` file header lines.
|
||
|
|
function isFileHeader(line) {
|
||
|
|
return (/^(---|\+\+\+)\s/).test(line);
|
||
|
|
}
|
||
|
|
// Matches `@@ ...` hunk header lines.
|
||
|
|
function isHunkHeader(line) {
|
||
|
|
return (/^@@\s/).test(line);
|
||
|
|
}
|
||
|
|
function parseIndex() {
|
||
|
|
var _a;
|
||
|
|
const index = {};
|
||
|
|
index.hunks = [];
|
||
|
|
list.push(index);
|
||
|
|
// Parse diff metadata
|
||
|
|
let seenDiffHeader = false;
|
||
|
|
while (i < diffstr.length) {
|
||
|
|
const line = diffstr[i];
|
||
|
|
// File header (---, +++) or hunk header (@@) found; end parsing diff metadata
|
||
|
|
if (isFileHeader(line) || isHunkHeader(line)) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
// The next two branches handle recognized diff headers. Note that
|
||
|
|
// isDiffHeader deliberately does NOT match arbitrary `diff`
|
||
|
|
// commands like `diff -u -p -r1.1 -r1.2`, because in some
|
||
|
|
// formats (e.g. CVS diffs) such lines appear as metadata within
|
||
|
|
// a single file's header section, after an `Index:` line. See the
|
||
|
|
// diffx documentation (https://diffx.org) for examples.
|
||
|
|
//
|
||
|
|
// In both branches: if we've already seen a diff header for *this*
|
||
|
|
// file and now we encounter another one, it must belong to the
|
||
|
|
// next file, so break.
|
||
|
|
if (isGitDiffHeader(line)) {
|
||
|
|
if (seenDiffHeader) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
seenDiffHeader = true;
|
||
|
|
index.isGit = true;
|
||
|
|
// Parse the old and new filenames from the `diff --git` header and
|
||
|
|
// tentatively set oldFileName and newFileName from them. These may
|
||
|
|
// be overridden below by `rename from` / `rename to` or `copy from` /
|
||
|
|
// `copy to` extended headers, or by --- and +++ lines. But for Git
|
||
|
|
// diffs that lack all of those (e.g. mode-only changes, binary
|
||
|
|
// file changes without rename), these are the only filenames we
|
||
|
|
// get.
|
||
|
|
// parseGitDiffHeader returns null if the header can't be parsed
|
||
|
|
// (e.g. unterminated quoted filename, or unexpected format). In
|
||
|
|
// that case we skip setting filenames here; they may still be
|
||
|
|
// set from --- / +++ or rename from / rename to lines below.
|
||
|
|
const paths = parseGitDiffHeader(line);
|
||
|
|
if (paths) {
|
||
|
|
index.oldFileName = paths.oldFileName;
|
||
|
|
index.newFileName = paths.newFileName;
|
||
|
|
}
|
||
|
|
// Consume Git extended headers (`old mode`, `new mode`, `rename from`,
|
||
|
|
// `rename to`, `similarity index`, `index`, `Binary files ... differ`,
|
||
|
|
// etc.)
|
||
|
|
i++;
|
||
|
|
while (i < diffstr.length) {
|
||
|
|
const extLine = diffstr[i];
|
||
|
|
// Stop consuming extended headers if we hit a file header,
|
||
|
|
// hunk header, or another diff header.
|
||
|
|
if (isFileHeader(extLine) || isHunkHeader(extLine) || isDiffHeader(extLine)) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
// Parse `rename from` / `rename to` lines - these give us
|
||
|
|
// unambiguous filenames. These lines don't include the
|
||
|
|
// a/ and b/ prefixes that appear in the `diff --git` header
|
||
|
|
// and --- / +++ lines, so we add them for consistency.
|
||
|
|
// Git C-style quotes filenames containing special characters
|
||
|
|
// (tabs, newlines, backslashes, double quotes), so we must
|
||
|
|
// unquote them when present.
|
||
|
|
const renameFromMatch = (/^rename from (.*)/).exec(extLine);
|
||
|
|
if (renameFromMatch) {
|
||
|
|
index.oldFileName = 'a/' + unquoteIfQuoted(renameFromMatch[1]);
|
||
|
|
index.isRename = true;
|
||
|
|
}
|
||
|
|
const renameToMatch = (/^rename to (.*)/).exec(extLine);
|
||
|
|
if (renameToMatch) {
|
||
|
|
index.newFileName = 'b/' + unquoteIfQuoted(renameToMatch[1]);
|
||
|
|
index.isRename = true;
|
||
|
|
}
|
||
|
|
// Parse copy from / copy to lines similarly
|
||
|
|
const copyFromMatch = (/^copy from (.*)/).exec(extLine);
|
||
|
|
if (copyFromMatch) {
|
||
|
|
index.oldFileName = 'a/' + unquoteIfQuoted(copyFromMatch[1]);
|
||
|
|
index.isCopy = true;
|
||
|
|
}
|
||
|
|
const copyToMatch = (/^copy to (.*)/).exec(extLine);
|
||
|
|
if (copyToMatch) {
|
||
|
|
index.newFileName = 'b/' + unquoteIfQuoted(copyToMatch[1]);
|
||
|
|
index.isCopy = true;
|
||
|
|
}
|
||
|
|
const newFileModeMatch = (/^new file mode (\d+)/).exec(extLine);
|
||
|
|
if (newFileModeMatch) {
|
||
|
|
index.isCreate = true;
|
||
|
|
index.newMode = newFileModeMatch[1];
|
||
|
|
}
|
||
|
|
const deletedFileModeMatch = (/^deleted file mode (\d+)/).exec(extLine);
|
||
|
|
if (deletedFileModeMatch) {
|
||
|
|
index.isDelete = true;
|
||
|
|
index.oldMode = deletedFileModeMatch[1];
|
||
|
|
}
|
||
|
|
const oldModeMatch = (/^old mode (\d+)/).exec(extLine);
|
||
|
|
if (oldModeMatch) {
|
||
|
|
index.oldMode = oldModeMatch[1];
|
||
|
|
}
|
||
|
|
const newModeMatch = (/^new mode (\d+)/).exec(extLine);
|
||
|
|
if (newModeMatch) {
|
||
|
|
index.newMode = newModeMatch[1];
|
||
|
|
}
|
||
|
|
if ((/^Binary files /).test(extLine)) {
|
||
|
|
index.isBinary = true;
|
||
|
|
}
|
||
|
|
i++;
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
else if (isDiffHeader(line)) {
|
||
|
|
if (seenDiffHeader) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
seenDiffHeader = true;
|
||
|
|
// For Mercurial-style headers like
|
||
|
|
// diff -r 9117c6561b0b -r 273ce12ad8f1 .hgignore
|
||
|
|
// or Index: headers like
|
||
|
|
// Index: something with multiple words
|
||
|
|
// we extract the trailing filename as the index.
|
||
|
|
//
|
||
|
|
// TODO: It seems awkward that we indiscriminately trim off
|
||
|
|
// trailing whitespace here. Theoretically, couldn't that
|
||
|
|
// be meaningful - e.g. if the patch represents a diff of a
|
||
|
|
// file whose name ends with a space? Seems wrong to nuke
|
||
|
|
// it. But this behaviour has been around since v2.2.1 in
|
||
|
|
// 2015, so if it's going to change, it should be done
|
||
|
|
// cautiously and in a new major release, for
|
||
|
|
// backwards-compat reasons.
|
||
|
|
// -- ExplodingCabbage
|
||
|
|
const headerMatch = (/^(?:Index:|diff(?: -r \w+)+)\s+/).exec(line);
|
||
|
|
if (headerMatch) {
|
||
|
|
index.index = line.substring(headerMatch[0].length).trim();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
i++;
|
||
|
|
}
|
||
|
|
// Parse file headers if they are defined. Unified diff requires them, but
|
||
|
|
// there's no technical issues to have an isolated hunk without file header
|
||
|
|
parseFileHeader(index);
|
||
|
|
parseFileHeader(index);
|
||
|
|
// If we got one file header but not the other, that's a malformed patch.
|
||
|
|
if ((index.oldFileName === undefined) !== (index.newFileName === undefined)) {
|
||
|
|
throw new Error('Missing ' + (index.oldFileName !== undefined ? '"+++ ..."' : '"--- ..."')
|
||
|
|
+ ' file header for ' + ((_a = index.oldFileName) !== null && _a !== void 0 ? _a : index.newFileName));
|
||
|
|
}
|
||
|
|
while (i < diffstr.length) {
|
||
|
|
const line = diffstr[i];
|
||
|
|
if (isDiffHeader(line) || isFileHeader(line) || (/^===================================================================/).test(line)) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
else if (isHunkHeader(line)) {
|
||
|
|
index.hunks.push(parseHunk());
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
// Skip blank lines and any other unrecognized content between
|
||
|
|
// or after hunks. Real-world examples of such content include:
|
||
|
|
// - `Only in <dir>: <file>` from GNU `diff -r`
|
||
|
|
// - `Property changes on:` sections from `svn diff`
|
||
|
|
// - Trailing prose or commentary in email patches
|
||
|
|
// GNU `patch` tolerates all of these, and so do we.
|
||
|
|
i++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Parses the old and new filenames from a `diff --git` header line.
|
||
|
|
*
|
||
|
|
* The format is:
|
||
|
|
* diff --git a/<old-path> b/<new-path>
|
||
|
|
*
|
||
|
|
* When filenames contain special characters (including newlines, tabs,
|
||
|
|
* backslashes, or double quotes), Git quotes them with C-style escaping:
|
||
|
|
* diff --git "a/file\twith\ttabs.txt" "b/file\twith\ttabs.txt"
|
||
|
|
*
|
||
|
|
* When filenames don't contain special characters and the old and new names
|
||
|
|
* are the same, we can unambiguously split on ` b/` by finding where the
|
||
|
|
* two halves (including their a/ and b/ prefixes) yield matching bare names.
|
||
|
|
*
|
||
|
|
* A pathological case exists in which we cannot reliably determine the paths
|
||
|
|
* from the `diff --git` header. This case is when the following are true:
|
||
|
|
* - the old and new file paths differ
|
||
|
|
* - they are both unquoted (i.e. contain no special characters)
|
||
|
|
* - at least one of the underlying file paths includes the substring ` b/`
|
||
|
|
* In this scenario, we do not know which occurrence of ` b/` indicates the
|
||
|
|
* start of the new file path, so the header is inherently ambiguous. We thus
|
||
|
|
* select a possible interpretation arbitrarily and return that.
|
||
|
|
*
|
||
|
|
* Fortunately, this ambiguity should never matter, because in any patch
|
||
|
|
* genuinely output by Git in which this pathological scenario occurs, there
|
||
|
|
* must also be `rename from`/`rename to` or `copy from`/`copy to` extended
|
||
|
|
* headers present below the `diff --git` header. `parseIndex` will parse
|
||
|
|
* THOSE headers, from which we CAN unambiguously determine the filenames,
|
||
|
|
* and will discard the result returned by this function.
|
||
|
|
*
|
||
|
|
* Returns null if the header can't be parsed at all — e.g. a quoted filename
|
||
|
|
* has an unterminated quote, or if the unquoted header doesn't match the
|
||
|
|
* expected `a/... b/...` format. In that case, the caller (parseIndex)
|
||
|
|
* skips setting oldFileName/newFileName from this header, but they may
|
||
|
|
* still be set later from `---`/`+++` lines or `rename from`/`rename to`
|
||
|
|
* extended headers; if none of those are present either, they'll remain
|
||
|
|
* undefined in the output.
|
||
|
|
*/
|
||
|
|
function parseGitDiffHeader(line) {
|
||
|
|
// Strip the "diff --git " prefix
|
||
|
|
const rest = line.substring('diff --git '.length);
|
||
|
|
// Handle quoted paths: "a/path" "b/path"
|
||
|
|
// Git quotes paths when they contain characters like newlines, tabs,
|
||
|
|
// backslashes, or double quotes (but notably not spaces).
|
||
|
|
if (rest.startsWith('"')) {
|
||
|
|
const oldPath = parseQuotedFileName(rest);
|
||
|
|
if (oldPath === null) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const afterOld = rest.substring(oldPath.rawLength + 1); // +1 for space
|
||
|
|
let newFileName;
|
||
|
|
if (afterOld.startsWith('"')) {
|
||
|
|
const newPath = parseQuotedFileName(afterOld);
|
||
|
|
if (newPath === null) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
newFileName = newPath.fileName;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
newFileName = afterOld;
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
oldFileName: oldPath.fileName,
|
||
|
|
newFileName
|
||
|
|
};
|
||
|
|
}
|
||
|
|
// Check if the second path is quoted
|
||
|
|
// e.g. diff --git a/simple "b/renamed\nnewline.txt"
|
||
|
|
const quoteIdx = rest.indexOf('"');
|
||
|
|
if (quoteIdx > 0) {
|
||
|
|
const oldFileName = rest.substring(0, quoteIdx - 1);
|
||
|
|
const newPath = parseQuotedFileName(rest.substring(quoteIdx));
|
||
|
|
if (newPath === null) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
oldFileName,
|
||
|
|
newFileName: newPath.fileName
|
||
|
|
};
|
||
|
|
}
|
||
|
|
// Unquoted paths. Try to find the split point.
|
||
|
|
// The format is: a/<old-path> b/<new-path>
|
||
|
|
//
|
||
|
|
// Note the potential ambiguity caused by the possibility of the file paths
|
||
|
|
// themselves containing the substring ` b/`, plus the pathological case
|
||
|
|
// described in the comment above.
|
||
|
|
//
|
||
|
|
// Strategy: find all occurrences of " b/" and split on the middle
|
||
|
|
// one. When old and new names are the same (which is the only case where
|
||
|
|
// we can't rely on extended headers later in the patch so HAVE to get
|
||
|
|
// this right), this will always be the correct split.
|
||
|
|
if (rest.startsWith('a/')) {
|
||
|
|
const splits = [];
|
||
|
|
let idx = 0;
|
||
|
|
while (true) {
|
||
|
|
idx = rest.indexOf(' b/', idx + 1);
|
||
|
|
if (idx === -1) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
splits.push(idx);
|
||
|
|
}
|
||
|
|
if (splits.length > 0) {
|
||
|
|
const mid = splits[Math.floor(splits.length / 2)];
|
||
|
|
return {
|
||
|
|
oldFileName: rest.substring(0, mid),
|
||
|
|
newFileName: rest.substring(mid + 1)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Fallback: can't parse, return null
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* If `s` starts with a double quote, unquotes it using C-style escape
|
||
|
|
* rules (as used by Git). Otherwise returns `s` as-is.
|
||
|
|
*/
|
||
|
|
function unquoteIfQuoted(s) {
|
||
|
|
if (s.startsWith('"')) {
|
||
|
|
const parsed = parseQuotedFileName(s);
|
||
|
|
if (parsed) {
|
||
|
|
return parsed.fileName;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return s;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Parses a C-style quoted filename as used by Git or GNU `diff -u`.
|
||
|
|
* Returns the unescaped filename and the raw length consumed (including quotes).
|
||
|
|
*/
|
||
|
|
function parseQuotedFileName(s) {
|
||
|
|
if (!s.startsWith('"')) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
let result = '';
|
||
|
|
let j = 1; // skip opening quote
|
||
|
|
while (j < s.length) {
|
||
|
|
if (s[j] === '"') {
|
||
|
|
return { fileName: result, rawLength: j + 1 };
|
||
|
|
}
|
||
|
|
if (s[j] === '\\' && j + 1 < s.length) {
|
||
|
|
j++;
|
||
|
|
switch (s[j]) {
|
||
|
|
case 'a':
|
||
|
|
result += '\x07';
|
||
|
|
break;
|
||
|
|
case 'b':
|
||
|
|
result += '\b';
|
||
|
|
break;
|
||
|
|
case 'f':
|
||
|
|
result += '\f';
|
||
|
|
break;
|
||
|
|
case 'n':
|
||
|
|
result += '\n';
|
||
|
|
break;
|
||
|
|
case 'r':
|
||
|
|
result += '\r';
|
||
|
|
break;
|
||
|
|
case 't':
|
||
|
|
result += '\t';
|
||
|
|
break;
|
||
|
|
case 'v':
|
||
|
|
result += '\v';
|
||
|
|
break;
|
||
|
|
case '\\':
|
||
|
|
result += '\\';
|
||
|
|
break;
|
||
|
|
case '"':
|
||
|
|
result += '"';
|
||
|
|
break;
|
||
|
|
case '0':
|
||
|
|
case '1':
|
||
|
|
case '2':
|
||
|
|
case '3':
|
||
|
|
case '4':
|
||
|
|
case '5':
|
||
|
|
case '6':
|
||
|
|
case '7': {
|
||
|
|
// C-style octal escapes represent raw bytes. Collect
|
||
|
|
// consecutive octal-escaped bytes and decode as UTF-8.
|
||
|
|
// Validate that we have a full 3-digit octal escape
|
||
|
|
if (j + 2 >= s.length || s[j + 1] < '0' || s[j + 1] > '7' || s[j + 2] < '0' || s[j + 2] > '7') {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const bytes = [parseInt(s.substring(j, j + 3), 8)];
|
||
|
|
j += 3;
|
||
|
|
while (s[j] === '\\' && s[j + 1] >= '0' && s[j + 1] <= '7') {
|
||
|
|
if (j + 3 >= s.length || s[j + 2] < '0' || s[j + 2] > '7' || s[j + 3] < '0' || s[j + 3] > '7') {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
bytes.push(parseInt(s.substring(j + 1, j + 4), 8));
|
||
|
|
j += 4;
|
||
|
|
}
|
||
|
|
result += new TextDecoder('utf-8').decode(new Uint8Array(bytes));
|
||
|
|
continue; // j already points at the next character
|
||
|
|
}
|
||
|
|
// Note that in C, there are also three kinds of hex escape sequences:
|
||
|
|
// - \xhh
|
||
|
|
// - \uhhhh
|
||
|
|
// - \Uhhhhhhhh
|
||
|
|
// We do not bother to parse them here because, so far as we know,
|
||
|
|
// they are never emitted by any tools that generate unified diff
|
||
|
|
// format diffs, and so for now jsdiff does not consider them legal.
|
||
|
|
default: return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
result += s[j];
|
||
|
|
}
|
||
|
|
j++;
|
||
|
|
}
|
||
|
|
// Unterminated quote
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
// Parses the --- and +++ headers, if none are found, no lines
|
||
|
|
// are consumed.
|
||
|
|
function parseFileHeader(index) {
|
||
|
|
const fileHeaderMatch = (/^(---|\+\+\+)\s+/).exec(diffstr[i]);
|
||
|
|
if (fileHeaderMatch) {
|
||
|
|
const prefix = fileHeaderMatch[1], data = diffstr[i].substring(3).trim().split('\t', 2), header = (data[1] || '').trim();
|
||
|
|
let fileName = data[0];
|
||
|
|
if (fileName.startsWith('"')) {
|
||
|
|
fileName = unquoteIfQuoted(fileName);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
fileName = fileName.replace(/\\\\/g, '\\');
|
||
|
|
}
|
||
|
|
if (prefix === '---') {
|
||
|
|
index.oldFileName = fileName;
|
||
|
|
index.oldHeader = header;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
index.newFileName = fileName;
|
||
|
|
index.newHeader = header;
|
||
|
|
}
|
||
|
|
i++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Parses a hunk
|
||
|
|
// This assumes that we are at the start of a hunk.
|
||
|
|
function parseHunk() {
|
||
|
|
var _a;
|
||
|
|
const chunkHeaderIndex = i, chunkHeaderLine = diffstr[i++], chunkHeader = chunkHeaderLine.split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
||
|
|
const hunk = {
|
||
|
|
oldStart: +chunkHeader[1],
|
||
|
|
oldLines: typeof chunkHeader[2] === 'undefined' ? 1 : +chunkHeader[2],
|
||
|
|
newStart: +chunkHeader[3],
|
||
|
|
newLines: typeof chunkHeader[4] === 'undefined' ? 1 : +chunkHeader[4],
|
||
|
|
lines: []
|
||
|
|
};
|
||
|
|
// Unified Diff Format quirk: If the chunk size is 0,
|
||
|
|
// the first number is one lower than one would expect.
|
||
|
|
// https://www.artima.com/weblogs/viewpost.jsp?thread=164293
|
||
|
|
if (hunk.oldLines === 0) {
|
||
|
|
hunk.oldStart += 1;
|
||
|
|
}
|
||
|
|
if (hunk.newLines === 0) {
|
||
|
|
hunk.newStart += 1;
|
||
|
|
}
|
||
|
|
let addCount = 0, removeCount = 0;
|
||
|
|
for (; i < diffstr.length && (removeCount < hunk.oldLines || addCount < hunk.newLines || ((_a = diffstr[i]) === null || _a === void 0 ? void 0 : _a.startsWith('\\'))); i++) {
|
||
|
|
const operation = (diffstr[i].length == 0 && i != (diffstr.length - 1)) ? ' ' : diffstr[i][0];
|
||
|
|
if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\') {
|
||
|
|
hunk.lines.push(diffstr[i]);
|
||
|
|
if (operation === '+') {
|
||
|
|
addCount++;
|
||
|
|
}
|
||
|
|
else if (operation === '-') {
|
||
|
|
removeCount++;
|
||
|
|
}
|
||
|
|
else if (operation === ' ') {
|
||
|
|
addCount++;
|
||
|
|
removeCount++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
throw new Error(`Hunk at line ${chunkHeaderIndex + 1} contained invalid line ${diffstr[i]}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Handle the empty block count case
|
||
|
|
if (!addCount && hunk.newLines === 1) {
|
||
|
|
hunk.newLines = 0;
|
||
|
|
}
|
||
|
|
if (!removeCount && hunk.oldLines === 1) {
|
||
|
|
hunk.oldLines = 0;
|
||
|
|
}
|
||
|
|
// Perform sanity checking
|
||
|
|
if (addCount !== hunk.newLines) {
|
||
|
|
throw new Error('Added line count did not match for hunk at line ' + (chunkHeaderIndex + 1));
|
||
|
|
}
|
||
|
|
if (removeCount !== hunk.oldLines) {
|
||
|
|
throw new Error('Removed line count did not match for hunk at line ' + (chunkHeaderIndex + 1));
|
||
|
|
}
|
||
|
|
// Check for extra hunk-body-like lines after the declared line counts
|
||
|
|
// were exhausted. If the very next line starts with ' ', '+', or '-',
|
||
|
|
// the hunk's line counts were probably wrong — unless it's a file
|
||
|
|
// header (--- or +++), which legitimately appears immediately after a
|
||
|
|
// hunk in multi-file diffs without Index lines.
|
||
|
|
if (i < diffstr.length && diffstr[i] && (/^[+ -]/).test(diffstr[i])
|
||
|
|
&& !isFileHeader(diffstr[i])) {
|
||
|
|
throw new Error('Hunk at line ' + (chunkHeaderIndex + 1)
|
||
|
|
+ ' has more lines than expected (expected '
|
||
|
|
+ hunk.oldLines + ' old lines and ' + hunk.newLines + ' new lines)');
|
||
|
|
}
|
||
|
|
return hunk;
|
||
|
|
}
|
||
|
|
while (i < diffstr.length) {
|
||
|
|
parseIndex();
|
||
|
|
}
|
||
|
|
return list;
|
||
|
|
}
|