// From https://github.com/sergeyt/parse-diff module.exports = function (input) { if (!input) { return [] } if (input.match(/^\s+$/)) { return [] } const lines = input.split('\n') if (lines.length === 0) { return [] } const files = [] let file = null let lnDel = 0 let lnAdd = 0 let current = null const start = function (line) { file = { chunks: [], deletions: 0, additions: 0 } files.push(file) if (!file.to && !file.from) { const fileNames = parseFile(line) if (fileNames) { file.from = fileNames[0] file.to = fileNames[1] } } } const restart = function () { if (!file || file.chunks.length) { return start() } } const newFile = function () { restart() file.new = true file.from = '/dev/null' } const deletedFile = function () { restart() file.deleted = true file.to = '/dev/null' } const index = function (line) { restart() file.index = line.split(' ').slice(1) } const fromFile = function (line) { restart() file.from = parseFileFallback(line) } const toFile = function (line) { restart() file.to = parseFileFallback(line) } const binary = function (line) { file.binary = true } const chunk = function (line, match) { let newStart, oldStart lnDel = (oldStart = +match[1]) const oldLines = +(match[2] || 0) lnAdd = (newStart = +match[3]) const newLines = +(match[4] || 0) current = { content: line, changes: [], oldStart, oldLines, newStart, newLines } file.chunks.push(current) } const del = function (line) { if (!current) return current.changes.push({ type: 'del', del: true, ln: lnDel++, content: line }) file.deletions++ } const add = function (line) { if (!current) return current.changes.push({ type: 'add', add: true, ln: lnAdd++, content: line }) file.additions++ } const normal = function (line) { if (!current) return current.changes.push({ type: 'normal', normal: true, ln1: lnDel++, ln2: lnAdd++, content: line }) } const eof = function (line) { const recentChange = current.changes[current.changes.length - 1] return current.changes.push({ type: recentChange.type, [recentChange.type]: true, ln1: recentChange.ln1, ln2: recentChange.ln2, ln: recentChange.ln, content: line }) } const schema = [ // todo beter regexp to avoid detect normal line starting with diff [/^\s+/, normal], [/^diff\s/, start], [/^new file mode \d+$/, newFile], [/^deleted file mode \d+$/, deletedFile], [/^Binary files/, binary], [/^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$/, index], [/^---\s/, fromFile], [/^\+\+\+\s/, toFile], [/^@@\s+-(\d+),?(\d+)?\s+\+(\d+),?(\d+)?\s@@/, chunk], [/^-/, del], [/^\+/, add], [/^\\ No newline at end of file$/, eof] ] const parse = function (line) { for (const p of schema) { const m = line.match(p[0]) if (m) { p[1](line, m) return true } } return false } for (const line of lines) { parse(line) } return files } function parseFile (s) { if (!s) return const result = /\sa\/(.*)\sb\/(.*)/.exec(s) return [result[1], result[2]] } // fallback function to overwrite file.from and file.to if executed function parseFileFallback (s) { s = ltrim(s, '-') s = ltrim(s, '+') s = s.trim() // ignore possible time stamp const t = (/\t.*|\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d(.\d+)?\s(\+|-)\d\d\d\d/).exec(s) if (t) { s = s.substring(0, t.index).trim() } // ignore git prefixes a/ or b/ if (s.match((/^(a|b)\//))) { return s.substr(2) } else { return s } } function ltrim (s, chars) { s = makeString(s) if (!chars && trimLeft) { return trimLeft.call(s) } chars = defaultToWhiteSpace(chars) return s.replace(new RegExp(`^${chars}+`), '') } const makeString = s => s === null ? '' : s + '' const { trimLeft } = String.prototype function defaultToWhiteSpace (chars) { if (chars === null) { return '\\s' } if (chars.source) { return chars.source } return `[${escapeRegExp(chars)}]` } const escapeRegExp = s => makeString(s).replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1')