|
|
@ -1,152 +1,131 @@ |
|
|
|
import { maybeStopWatch } from '../../system/stopwatch'; |
|
|
|
import { getLines } from '../../system/string'; |
|
|
|
import type { GitDiffFile, GitDiffHunkLine, GitDiffLine, GitDiffShortStat } from '../models/diff'; |
|
|
|
import { GitDiffHunk } from '../models/diff'; |
|
|
|
import type { GitDiffFile, GitDiffHunk, GitDiffHunkLine, GitDiffShortStat } from '../models/diff'; |
|
|
|
import type { GitFile, GitFileStatus } from '../models/file'; |
|
|
|
|
|
|
|
const shortStatDiffRegex = /(\d+)\s+files? changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/; |
|
|
|
const unifiedDiffRegex = /^@@ -([\d]+)(?:,([\d]+))? \+([\d]+)(?:,([\d]+))? @@(?:.*?)\n([\s\S]*?)(?=^@@)/gm; |
|
|
|
|
|
|
|
export function parseGitFileDiff(data: string, includeContents: boolean = false): GitDiffFile | undefined { |
|
|
|
function parseHunkHeaderPart(headerPart: string) { |
|
|
|
const [startS, countS] = headerPart.split(','); |
|
|
|
const count = Number(countS) || 1; |
|
|
|
const start = Number(startS); |
|
|
|
return { count: count, position: { start: start, end: start + count - 1 } }; |
|
|
|
} |
|
|
|
|
|
|
|
export function parseGitFileDiff(data: string, includeContents = false): GitDiffFile | undefined { |
|
|
|
using sw = maybeStopWatch('Git.parseFileDiff', { log: false, logLevel: 'debug' }); |
|
|
|
if (!data) return undefined; |
|
|
|
|
|
|
|
const hunks: GitDiffHunk[] = []; |
|
|
|
|
|
|
|
let previousStart; |
|
|
|
let previousCount; |
|
|
|
let currentStart; |
|
|
|
let currentCount; |
|
|
|
let hunk; |
|
|
|
|
|
|
|
let match; |
|
|
|
do { |
|
|
|
match = unifiedDiffRegex.exec(`${data}\n@@`); |
|
|
|
if (match == null) break; |
|
|
|
|
|
|
|
[, previousStart, previousCount, currentStart, currentCount, hunk] = match; |
|
|
|
|
|
|
|
previousCount = Number(previousCount) || 0; |
|
|
|
previousStart = Number(previousStart) || 0; |
|
|
|
currentCount = Number(currentCount) || 0; |
|
|
|
currentStart = Number(currentStart) || 0; |
|
|
|
|
|
|
|
hunks.push( |
|
|
|
new GitDiffHunk( |
|
|
|
// Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
|
|
|
|
` ${hunk}`.substr(1), |
|
|
|
{ |
|
|
|
count: currentCount === 0 ? 1 : currentCount, |
|
|
|
position: { |
|
|
|
start: currentStart, |
|
|
|
end: currentStart + (currentCount > 0 ? currentCount - 1 : 0), |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
count: previousCount === 0 ? 1 : previousCount, |
|
|
|
position: { |
|
|
|
start: previousStart, |
|
|
|
end: previousStart + (previousCount > 0 ? previousCount - 1 : 0), |
|
|
|
}, |
|
|
|
}, |
|
|
|
), |
|
|
|
); |
|
|
|
} while (true); |
|
|
|
|
|
|
|
sw?.stop({ suffix: ` parsed ${hunks.length} hunks` }); |
|
|
|
|
|
|
|
if (!hunks.length) return undefined; |
|
|
|
|
|
|
|
const diff: GitDiffFile = { |
|
|
|
contents: includeContents ? data : undefined, |
|
|
|
hunks: hunks, |
|
|
|
}; |
|
|
|
return diff; |
|
|
|
} |
|
|
|
|
|
|
|
export function parseGitDiffHunk(hunk: GitDiffHunk): { |
|
|
|
lines: GitDiffHunkLine[]; |
|
|
|
state: 'added' | 'changed' | 'removed'; |
|
|
|
} { |
|
|
|
using sw = maybeStopWatch('Git.parseDiffHunk', { log: false, logLevel: 'debug' }); |
|
|
|
|
|
|
|
const currentStart = hunk.current.position.start; |
|
|
|
const previousStart = hunk.previous.position.start; |
|
|
|
|
|
|
|
const currentLines: (GitDiffLine | undefined)[] = |
|
|
|
currentStart > previousStart |
|
|
|
? new Array(currentStart - previousStart).fill(undefined, 0, currentStart - previousStart) |
|
|
|
: []; |
|
|
|
const previousLines: (GitDiffLine | undefined)[] = |
|
|
|
previousStart > currentStart |
|
|
|
? new Array(previousStart - currentStart).fill(undefined, 0, previousStart - currentStart) |
|
|
|
: []; |
|
|
|
|
|
|
|
let hasAddedOrChanged; |
|
|
|
let hasRemoved; |
|
|
|
|
|
|
|
let removed = 0; |
|
|
|
for (const l of getLines(hunk.contents)) { |
|
|
|
switch (l[0]) { |
|
|
|
case '+': |
|
|
|
hasAddedOrChanged = true; |
|
|
|
currentLines.push({ |
|
|
|
line: ` ${l.substring(1)}`, |
|
|
|
state: 'added', |
|
|
|
}); |
|
|
|
|
|
|
|
if (removed > 0) { |
|
|
|
removed--; |
|
|
|
} else { |
|
|
|
previousLines.push(undefined); |
|
|
|
} |
|
|
|
|
|
|
|
break; |
|
|
|
const lines = data.split('\n'); |
|
|
|
|
|
|
|
case '-': |
|
|
|
hasRemoved = true; |
|
|
|
removed++; |
|
|
|
|
|
|
|
previousLines.push({ |
|
|
|
line: ` ${l.substring(1)}`, |
|
|
|
state: 'removed', |
|
|
|
}); |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
default: |
|
|
|
while (removed > 0) { |
|
|
|
removed--; |
|
|
|
currentLines.push(undefined); |
|
|
|
} |
|
|
|
|
|
|
|
currentLines.push({ line: l, state: 'unchanged' }); |
|
|
|
previousLines.push({ line: l, state: 'unchanged' }); |
|
|
|
|
|
|
|
break; |
|
|
|
// Skip header
|
|
|
|
let i = -1; |
|
|
|
while (i < lines.length) { |
|
|
|
if (lines[++i].startsWith('@@')) { |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
while (removed > 0) { |
|
|
|
removed--; |
|
|
|
currentLines.push(undefined); |
|
|
|
} |
|
|
|
// Parse hunks
|
|
|
|
let line; |
|
|
|
while (i < lines.length) { |
|
|
|
line = lines[i]; |
|
|
|
if (!line.startsWith('@@')) continue; |
|
|
|
|
|
|
|
const content = line.slice(4, -3); |
|
|
|
const [previousHeaderPart, currentHeaderPart] = content.split(' +'); |
|
|
|
|
|
|
|
const current = parseHunkHeaderPart(currentHeaderPart); |
|
|
|
const previous = parseHunkHeaderPart(previousHeaderPart); |
|
|
|
|
|
|
|
const hunkLines = new Map<number, GitDiffHunkLine>(); |
|
|
|
let fileLineNumber = current.position.start; |
|
|
|
|
|
|
|
line = lines[++i]; |
|
|
|
const contentStartLine = i; |
|
|
|
|
|
|
|
// Parse hunks lines
|
|
|
|
while (i < lines.length && !line.startsWith('@@')) { |
|
|
|
switch (line[0]) { |
|
|
|
// deleted
|
|
|
|
case '-': { |
|
|
|
let deletedLineNumber = fileLineNumber; |
|
|
|
while (line?.startsWith('-')) { |
|
|
|
hunkLines.set(deletedLineNumber++, { |
|
|
|
current: undefined, |
|
|
|
previous: line.slice(1), |
|
|
|
state: 'removed', |
|
|
|
}); |
|
|
|
line = lines[++i]; |
|
|
|
} |
|
|
|
|
|
|
|
if (line?.startsWith('+')) { |
|
|
|
let addedLineNumber = fileLineNumber; |
|
|
|
while (line?.startsWith('+')) { |
|
|
|
const hunkLine = hunkLines.get(addedLineNumber); |
|
|
|
if (hunkLine != null) { |
|
|
|
hunkLine.current = line.slice(1); |
|
|
|
hunkLine.state = 'changed'; |
|
|
|
} else { |
|
|
|
hunkLines.set(addedLineNumber, { |
|
|
|
current: line.slice(1), |
|
|
|
previous: undefined, |
|
|
|
state: 'added', |
|
|
|
}); |
|
|
|
} |
|
|
|
addedLineNumber++; |
|
|
|
line = lines[++i]; |
|
|
|
} |
|
|
|
fileLineNumber = addedLineNumber; |
|
|
|
} else { |
|
|
|
fileLineNumber = deletedLineNumber; |
|
|
|
} |
|
|
|
break; |
|
|
|
} |
|
|
|
// added
|
|
|
|
case '+': |
|
|
|
hunkLines.set(fileLineNumber++, { |
|
|
|
current: line.slice(1), |
|
|
|
previous: undefined, |
|
|
|
state: 'added', |
|
|
|
}); |
|
|
|
|
|
|
|
line = lines[++i]; |
|
|
|
break; |
|
|
|
|
|
|
|
// unchanged (context)
|
|
|
|
case ' ': |
|
|
|
hunkLines.set(fileLineNumber++, { |
|
|
|
current: line.slice(1), |
|
|
|
previous: line.slice(1), |
|
|
|
state: 'unchanged', |
|
|
|
}); |
|
|
|
|
|
|
|
line = lines[++i]; |
|
|
|
break; |
|
|
|
|
|
|
|
default: |
|
|
|
line = lines[++i]; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const hunkLines: GitDiffHunkLine[] = []; |
|
|
|
const hunk: GitDiffHunk = { |
|
|
|
contents: `${lines.slice(contentStartLine, i).join('\n')}\n`, |
|
|
|
current: current, |
|
|
|
previous: previous, |
|
|
|
lines: hunkLines, |
|
|
|
}; |
|
|
|
|
|
|
|
for (let i = 0; i < Math.max(currentLines.length, previousLines.length); i++) { |
|
|
|
hunkLines.push({ |
|
|
|
hunk: hunk, |
|
|
|
current: currentLines[i], |
|
|
|
previous: previousLines[i], |
|
|
|
}); |
|
|
|
hunks.push(hunk); |
|
|
|
} |
|
|
|
|
|
|
|
sw?.stop({ suffix: ` parsed ${hunkLines.length} hunk lines` }); |
|
|
|
sw?.stop({ suffix: ` parsed ${hunks.length} hunks` }); |
|
|
|
|
|
|
|
return { |
|
|
|
lines: hunkLines, |
|
|
|
state: hasAddedOrChanged && hasRemoved ? 'changed' : hasAddedOrChanged ? 'added' : 'removed', |
|
|
|
contents: includeContents ? data : undefined, |
|
|
|
hunks: hunks, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|