diff --git a/lib/diff.js b/lib/diff.js index c9e4fb0b665f2..cc27f93ce292b 100644 --- a/lib/diff.js +++ b/lib/diff.js @@ -1,5 +1,7 @@ const fs = require('fs') +const { EOL } = require('os') const { promisify } = require('util') +const ansi = require('ansi-styles') const Arborist = require('@npmcli/arborist') const jsDiff = require('diff') const pacote = require('pacote') @@ -50,6 +52,8 @@ const getMime = () => { 'patents', 'readme', 'ts', + 'yml', + 'yaml', 'flow' ], 'text/plain') @@ -112,7 +116,34 @@ const untar = ({ files, item, prefix, opts, refs }) => .end(item) }) -const printDiff = ({ files, refs, versions }) => { +const colorizeDiff = ({ res, headerLength }) => { + const colors = { + charsRemoved: ansi.bgRed, + charsAdded: ansi.bgGreen, + removed: ansi.red, + added: ansi.green, + header: ansi.yellow, + section: ansi.magenta + } + const colorize = (str, colorId) => { + var { open, close } = colors[colorId] + // avoid highlighting the "\n" (would highlight till the end of the line) + return str.replace(/[^\n\r]+/g, open + '$&' + close) + } + + // this RegExp will include all the `\n` chars into the lines, easier to join + const lines = res.split(/^/m) + + const start = colorize(lines.slice(0, headerLength || 2).join(''), 'header') + const end = lines.slice(headerLength || 2).join('') + .replace(/^-.*/gm, colorize('$&', 'removed')) + .replace(/^\+.*/gm, colorize('$&', 'added')) + .replace(/^@@.+@@/gm, colorize('$&', 'section')) + + return start + end +} + +const printDiff = ({ files, opts, refs, versions }) => { for (const filename of files.values()) { const names = { a: `a/${filename}`, @@ -135,9 +166,31 @@ const printDiff = ({ files, refs, versions }) => { if (contents.a === contents.b) continue - let res + let res = '' + let headerLength = 0 + const header = str => { + headerLength++ + res += `${str}${EOL}` + } + + // manually build a git diff-compatible header + header(`diff --git ${names.a} ${names.b}`) + if (modes.a === modes.b) { + fileMode = filenames.a.mode + } else { + if (modes.a && modes.b) { + header(`old mode ${modes.a}`) + header(`new mode ${modes.b}`) + } else if (modes.a && !modes.b) { + header(`deleted file mode ${modes.a}`) + } else if (!modes.a && modes.b) { + header(`new file mode ${modes.b}`) + } + } + header(`index ${versions.a}..${versions.b} ${fileMode}`) + if (diffFileType(filename)) { - res = jsDiff.createTwoFilesPatch( + res += jsDiff.createTwoFilesPatch( names.a, names.b, contents.a || '', @@ -149,27 +202,17 @@ const printDiff = ({ files, refs, versions }) => { '===================================================================\n', '' ) + headerLength += 2 } else { - res = `--- ${names.a}\n+++ ${names.b}` + header(`--- ${names.a}`) + header(`+++ ${names.b}`) } - output(`diff --git ${names.a} ${names.b}`) - - if (modes.a === modes.b) { - fileMode = filenames.a.mode - } else { - if (modes.a && modes.b) { - output(`old mode ${modes.a}`) - output(`new mode ${modes.b}`) - } else if (modes.a && !modes.b) { - output(`deleted file mode ${modes.a}`) - } else if (!modes.a && modes.b) { - output(`new file mode ${modes.b}`) - } - } - - output(`index ${versions.a}..${versions.b} ${fileMode}`) - output(res) + output( + opts.color + ? colorizeDiff({ res, headerLength }) + : res + ) } } @@ -224,6 +267,7 @@ const diffSelf = async () => { printDiff({ files, + opts, refs, versions }) @@ -293,6 +337,7 @@ const diffComparison = async (specs) => { printDiff({ files, + opts, refs, versions })