var logger = process.logging || require('./log'); var fs = require('fs'); var path = require('path'); var events = require('events'); var diff = require('diff'); var prompt = require('../actions/prompt'); var log = logger('conflicter'); var async = require('async'); var isBinaryFile = require('isbinaryfile'); var chalk = require('chalk'); var conflicter = module.exports = Object.create(events.EventEmitter.prototype); conflicter.conflicts = []; conflicter.add = function add(conflict) { if (typeof conflict === 'string') { conflict = { file: conflict, content: fs.readFileSync(conflict, 'utf8') }; } if (!conflict.file) { throw new Error('Missing conflict.file option'); } if (conflict.content === undefined) { throw new Error('Missing conflict.content option'); } this.conflicts.push(conflict); return this; }; conflicter.reset = function reset() { this.conflicts = []; return this; }; conflicter.pop = function pop() { return this.conflicts.pop(); }; conflicter.shift = function shift() { return this.conflicts.shift(); }; conflicter.resolve = function resolve(cb) { var resolveConflicts = function (conflict) { return function (next) { if (!conflict) { return next(); } conflicter.collision(conflict.file, conflict.content, function (status) { conflicter.emit('resolved:' + conflict.file, { status: status, callback: next }); }); }; }; async.series(this.conflicts.map(resolveConflicts), function (err) { if (err) { cb(); return self.emit('error', err); } conflicter.reset(); cb(); }.bind(this)); }; conflicter._ask = function (filepath, content, cb) { // for this particular use case, might use prompt module directly to avoid // the additional "Are you sure?" prompt var self = this; var config = [{ type: 'expand', message: 'Overwrite ' + filepath + '?', choices: [{ key: 'y', name: 'overwrite', value: function (cb) { log.force(filepath); return cb('force'); } }, { key: 'n', name: 'do not overwrite', value: function (cb) { log.skip(filepath); return cb('skip'); } }, { key: 'a', name: 'overwrite this and all others', value: function (cb) { log.force(filepath); self.force = true; return cb('force'); } }, { key: 'x', name: 'abort', value: function (cb) { log.writeln('Aborting ...'); return process.exit(0); } }, { key: 'd', name: 'show the differences between the old and the new', value: function (cb) { console.log(conflicter.diff(fs.readFileSync(filepath, 'utf8'), content)); return self._ask(filepath, content, cb); } }], name: 'overwrite' }]; process.nextTick(function () { this.emit('prompt', config); this.emit('conflict', filepath); }.bind(this)); prompt(config, function (result) { result.overwrite(function (action) { cb(action); }); }); }; conflicter.collision = function collision(filepath, content, cb) { var self = this; if (!fs.existsSync(filepath)) { log.create(filepath); return cb('create'); } var encoding = null; if (!isBinaryFile(path.resolve(filepath))) { encoding = 'utf8'; } var actual = fs.readFileSync(path.resolve(filepath), encoding); // In case of binary content, `actual` and `content` are `Buffer` objects, // we just can't compare those 2 objects with standard `===`, // so we convert each binary content to an hexadecimal string first, and then compare them with standard `===` // // For not binary content, we can directly compare the 2 strings this way if ((!encoding && (actual.toString('hex') === content.toString('hex'))) || (actual === content)) { log.identical(filepath); return cb('identical'); } if (self.force) { log.force(filepath); return cb('force'); } log.conflict(filepath); conflicter._ask(filepath, content, cb); }; conflicter.colorDiffAdded = chalk.bgGreen; conflicter.colorDiffRemoved = chalk.bgRed; // below is borrowed code from visionmedia's excellent mocha (and its reporter) conflicter.diff = function _diff(actual, expected) { var msg = diff.diffLines(actual, expected).map(function (str) { if (str.added) { return conflicter.colorLines('Added', str.value); } if (str.removed) { return conflicter.colorLines('Removed', str.value); } return str.value; }).join(''); // legend msg = '\n' + conflicter.colorDiffRemoved('removed') + ' ' + conflicter.colorDiffAdded('added') + '\n\n' + msg + '\n'; return msg; }; conflicter.colorLines = function colorLines(name, str) { return str.split('\n').map(function (str) { return conflicter['colorDiff' + name](str); }).join('\n'); };