conflicter.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. var logger = process.logging || require('./log');
  2. var fs = require('fs');
  3. var path = require('path');
  4. var events = require('events');
  5. var diff = require('diff');
  6. var prompt = require('../actions/prompt');
  7. var log = logger('conflicter');
  8. var async = require('async');
  9. var isBinaryFile = require('isbinaryfile');
  10. var chalk = require('chalk');
  11. var conflicter = module.exports = Object.create(events.EventEmitter.prototype);
  12. conflicter.conflicts = [];
  13. conflicter.add = function add(conflict) {
  14. if (typeof conflict === 'string') {
  15. conflict = {
  16. file: conflict,
  17. content: fs.readFileSync(conflict, 'utf8')
  18. };
  19. }
  20. if (!conflict.file) {
  21. throw new Error('Missing conflict.file option');
  22. }
  23. if (conflict.content === undefined) {
  24. throw new Error('Missing conflict.content option');
  25. }
  26. this.conflicts.push(conflict);
  27. return this;
  28. };
  29. conflicter.reset = function reset() {
  30. this.conflicts = [];
  31. return this;
  32. };
  33. conflicter.pop = function pop() {
  34. return this.conflicts.pop();
  35. };
  36. conflicter.shift = function shift() {
  37. return this.conflicts.shift();
  38. };
  39. conflicter.resolve = function resolve(cb) {
  40. var resolveConflicts = function (conflict) {
  41. return function (next) {
  42. if (!conflict) {
  43. return next();
  44. }
  45. conflicter.collision(conflict.file, conflict.content, function (status) {
  46. conflicter.emit('resolved:' + conflict.file, {
  47. status: status,
  48. callback: next
  49. });
  50. });
  51. };
  52. };
  53. async.series(this.conflicts.map(resolveConflicts), function (err) {
  54. if (err) {
  55. cb();
  56. return self.emit('error', err);
  57. }
  58. conflicter.reset();
  59. cb();
  60. }.bind(this));
  61. };
  62. conflicter._ask = function (filepath, content, cb) {
  63. // for this particular use case, might use prompt module directly to avoid
  64. // the additional "Are you sure?" prompt
  65. var self = this;
  66. var config = [{
  67. type: 'expand',
  68. message: 'Overwrite ' + filepath + '?',
  69. choices: [{
  70. key: 'y',
  71. name: 'overwrite',
  72. value: function (cb) {
  73. log.force(filepath);
  74. return cb('force');
  75. }
  76. }, {
  77. key: 'n',
  78. name: 'do not overwrite',
  79. value: function (cb) {
  80. log.skip(filepath);
  81. return cb('skip');
  82. }
  83. }, {
  84. key: 'a',
  85. name: 'overwrite this and all others',
  86. value: function (cb) {
  87. log.force(filepath);
  88. self.force = true;
  89. return cb('force');
  90. }
  91. }, {
  92. key: 'x',
  93. name: 'abort',
  94. value: function (cb) {
  95. log.writeln('Aborting ...');
  96. return process.exit(0);
  97. }
  98. }, {
  99. key: 'd',
  100. name: 'show the differences between the old and the new',
  101. value: function (cb) {
  102. console.log(conflicter.diff(fs.readFileSync(filepath, 'utf8'), content));
  103. return self._ask(filepath, content, cb);
  104. }
  105. }],
  106. name: 'overwrite'
  107. }];
  108. process.nextTick(function () {
  109. this.emit('prompt', config);
  110. this.emit('conflict', filepath);
  111. }.bind(this));
  112. prompt(config, function (result) {
  113. result.overwrite(function (action) {
  114. cb(action);
  115. });
  116. });
  117. };
  118. conflicter.collision = function collision(filepath, content, cb) {
  119. var self = this;
  120. if (!fs.existsSync(filepath)) {
  121. log.create(filepath);
  122. return cb('create');
  123. }
  124. var encoding = null;
  125. if (!isBinaryFile(path.resolve(filepath))) {
  126. encoding = 'utf8';
  127. }
  128. var actual = fs.readFileSync(path.resolve(filepath), encoding);
  129. // In case of binary content, `actual` and `content` are `Buffer` objects,
  130. // we just can't compare those 2 objects with standard `===`,
  131. // so we convert each binary content to an hexadecimal string first, and then compare them with standard `===`
  132. //
  133. // For not binary content, we can directly compare the 2 strings this way
  134. if ((!encoding && (actual.toString('hex') === content.toString('hex'))) ||
  135. (actual === content)) {
  136. log.identical(filepath);
  137. return cb('identical');
  138. }
  139. if (self.force) {
  140. log.force(filepath);
  141. return cb('force');
  142. }
  143. log.conflict(filepath);
  144. conflicter._ask(filepath, content, cb);
  145. };
  146. conflicter.colorDiffAdded = chalk.bgGreen;
  147. conflicter.colorDiffRemoved = chalk.bgRed;
  148. // below is borrowed code from visionmedia's excellent mocha (and its reporter)
  149. conflicter.diff = function _diff(actual, expected) {
  150. var msg = diff.diffLines(actual, expected).map(function (str) {
  151. if (str.added) {
  152. return conflicter.colorLines('Added', str.value);
  153. }
  154. if (str.removed) {
  155. return conflicter.colorLines('Removed', str.value);
  156. }
  157. return str.value;
  158. }).join('');
  159. // legend
  160. msg = '\n' +
  161. conflicter.colorDiffRemoved('removed') +
  162. ' ' +
  163. conflicter.colorDiffAdded('added') +
  164. '\n\n' +
  165. msg +
  166. '\n';
  167. return msg;
  168. };
  169. conflicter.colorLines = function colorLines(name, str) {
  170. return str.split('\n').map(function (str) {
  171. return conflicter['colorDiff' + name](str);
  172. }).join('\n');
  173. };