conflicter.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. 'use strict';
  2. const fs = require('fs');
  3. const path = require('path');
  4. const async = require('async');
  5. const detectConflict = require('detect-conflict');
  6. const _ = require('lodash');
  7. const typedError = require('error/typed');
  8. const binaryDiff = require('./binary-diff');
  9. const AbortedError = typedError({
  10. type: 'AbortedError',
  11. message: 'Process aborted by user'
  12. });
  13. /**
  14. * The Conflicter is a module that can be used to detect conflict between files. Each
  15. * Generator file system helpers pass files through this module to make sure they don't
  16. * break a user file.
  17. *
  18. * When a potential conflict is detected, we prompt the user and ask them for
  19. * confirmation before proceeding with the actual write.
  20. *
  21. * @constructor
  22. * @property {Boolean} force - same as the constructor argument
  23. *
  24. * @param {TerminalAdapter} adapter - The generator adapter
  25. * @param {Boolean} force - When set to true, we won't check for conflict. (the
  26. * conflicter become a passthrough)
  27. */
  28. class Conflicter {
  29. constructor(adapter, force) {
  30. this.force = force === true;
  31. this.adapter = adapter;
  32. this.conflicts = [];
  33. }
  34. /**
  35. * Add a file to conflicter queue
  36. *
  37. * @param {String} filepath - File destination path
  38. * @param {String} contents - File new contents
  39. * @param {Function} callback - callback to be called once we know if the user want to
  40. * proceed or not.
  41. */
  42. checkForCollision(filepath, contents, callback) {
  43. this.conflicts.push({
  44. file: {
  45. path: path.resolve(filepath),
  46. contents
  47. },
  48. callback
  49. });
  50. }
  51. /**
  52. * Process the _potential conflict_ queue and ask the user to resolve conflict when they
  53. * occur
  54. *
  55. * The user is presented with the following options:
  56. *
  57. * - `Y` Yes, overwrite
  58. * - `n` No, do not overwrite
  59. * - `a` All, overwrite this and all others
  60. * - `x` Exit, abort
  61. * - `d` Diff, show the differences between the old and the new
  62. * - `h` Help, show this help
  63. *
  64. * @param {Function} cb Callback once every conflict are resolved. (note that each
  65. * file can specify it's own callback. See `#checkForCollision()`)
  66. */
  67. resolve(cb) {
  68. cb = cb || (() => {});
  69. const resolveConflicts = conflict => {
  70. return next => {
  71. if (!conflict) {
  72. next();
  73. return;
  74. }
  75. this.collision(conflict.file, status => {
  76. // Remove the resolved conflict from the queue
  77. _.pull(this.conflicts, conflict);
  78. conflict.callback(null, status);
  79. next();
  80. });
  81. };
  82. };
  83. async.series(this.conflicts.map(resolveConflicts), cb.bind(this));
  84. }
  85. /**
  86. * Check if a file conflict with the current version on the user disk
  87. *
  88. * A basic check is done to see if the file exists, if it does:
  89. *
  90. * 1. Read its content from `fs`
  91. * 2. Compare it with the provided content
  92. * 3. If identical, mark it as is and skip the check
  93. * 4. If diverged, prepare and show up the file collision menu
  94. *
  95. * @param {Object} file File object respecting this interface: { path, contents }
  96. * @param {Function} cb Callback receiving a status string ('identical', 'create',
  97. * 'skip', 'force')
  98. * @return {null} nothing
  99. */
  100. collision(file, cb) {
  101. const rfilepath = path.relative(process.cwd(), file.path);
  102. if (!fs.existsSync(file.path)) {
  103. this.adapter.log.create(rfilepath);
  104. cb('create');
  105. return;
  106. }
  107. if (this.force) {
  108. this.adapter.log.force(rfilepath);
  109. cb('force');
  110. return;
  111. }
  112. if (detectConflict(file.path, file.contents)) {
  113. this.adapter.log.conflict(rfilepath);
  114. this._ask(file, cb);
  115. } else {
  116. this.adapter.log.identical(rfilepath);
  117. cb('identical');
  118. }
  119. }
  120. /**
  121. * Actual prompting logic
  122. * @private
  123. * @param {Object} file
  124. * @param {Function} cb
  125. */
  126. _ask(file, cb) {
  127. const rfilepath = path.relative(process.cwd(), file.path);
  128. const prompt = {
  129. name: 'action',
  130. type: 'expand',
  131. message: `Overwrite ${rfilepath}?`,
  132. choices: [{
  133. key: 'y',
  134. name: 'overwrite',
  135. value: 'write'
  136. }, {
  137. key: 'n',
  138. name: 'do not overwrite',
  139. value: 'skip'
  140. }, {
  141. key: 'a',
  142. name: 'overwrite this and all others',
  143. value: 'force'
  144. }, {
  145. key: 'x',
  146. name: 'abort',
  147. value: 'abort'
  148. }]
  149. };
  150. // Only offer diff option for files
  151. if (fs.statSync(file.path).isFile()) {
  152. prompt.choices.push({
  153. key: 'd',
  154. name: 'show the differences between the old and the new',
  155. value: 'diff'
  156. });
  157. }
  158. this.adapter.prompt([prompt], result => {
  159. if (result.action === 'abort') {
  160. this.adapter.log.writeln('Aborting ...');
  161. throw new AbortedError();
  162. }
  163. if (result.action === 'diff') {
  164. if (binaryDiff.isBinary(file.path, file.contents)) {
  165. this.adapter.log.writeln(binaryDiff.diff(file.path, file.contents));
  166. } else {
  167. const existing = fs.readFileSync(file.path);
  168. this.adapter.diff(existing.toString(), (file.contents || '').toString());
  169. }
  170. return this._ask(file, cb);
  171. }
  172. if (result.action === 'force') {
  173. this.force = true;
  174. }
  175. if (result.action === 'write') {
  176. result.action = 'force';
  177. }
  178. this.adapter.log[result.action](rfilepath);
  179. return cb(result.action);
  180. });
  181. }
  182. }
  183. module.exports = Conflicter;