create.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { diffLines } from '../diff/line.js';
  2. export function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {
  3. let optionsObj;
  4. if (!options) {
  5. optionsObj = {};
  6. }
  7. else if (typeof options === 'function') {
  8. optionsObj = { callback: options };
  9. }
  10. else {
  11. optionsObj = options;
  12. }
  13. if (typeof optionsObj.context === 'undefined') {
  14. optionsObj.context = 4;
  15. }
  16. // We copy this into its own variable to placate TypeScript, which thinks
  17. // optionsObj.context might be undefined in the callbacks below.
  18. const context = optionsObj.context;
  19. // @ts-expect-error (runtime check for something that is correctly a static type error)
  20. if (optionsObj.newlineIsToken) {
  21. throw new Error('newlineIsToken may not be used with patch-generation functions, only with diffing functions');
  22. }
  23. if (!optionsObj.callback) {
  24. return diffLinesResultToPatch(diffLines(oldStr, newStr, optionsObj));
  25. }
  26. else {
  27. const { callback } = optionsObj;
  28. diffLines(oldStr, newStr, Object.assign(Object.assign({}, optionsObj), { callback: (diff) => {
  29. const patch = diffLinesResultToPatch(diff);
  30. // TypeScript is unhappy without the cast because it does not understand that `patch` may
  31. // be undefined here only if `callback` is StructuredPatchCallbackAbortable:
  32. callback(patch);
  33. } }));
  34. }
  35. function diffLinesResultToPatch(diff) {
  36. // STEP 1: Build up the patch with no "\ No newline at end of file" lines and with the arrays
  37. // of lines containing trailing newline characters. We'll tidy up later...
  38. if (!diff) {
  39. return;
  40. }
  41. diff.push({ value: '', lines: [] }); // Append an empty value to make cleanup easier
  42. function contextLines(lines) {
  43. return lines.map(function (entry) { return ' ' + entry; });
  44. }
  45. const hunks = [];
  46. let oldRangeStart = 0, newRangeStart = 0, curRange = [], oldLine = 1, newLine = 1;
  47. for (let i = 0; i < diff.length; i++) {
  48. const current = diff[i], lines = current.lines || splitLines(current.value);
  49. current.lines = lines;
  50. if (current.added || current.removed) {
  51. // If we have previous context, start with that
  52. if (!oldRangeStart) {
  53. const prev = diff[i - 1];
  54. oldRangeStart = oldLine;
  55. newRangeStart = newLine;
  56. if (prev) {
  57. curRange = context > 0 ? contextLines(prev.lines.slice(-context)) : [];
  58. oldRangeStart -= curRange.length;
  59. newRangeStart -= curRange.length;
  60. }
  61. }
  62. // Output our changes
  63. for (const line of lines) {
  64. curRange.push((current.added ? '+' : '-') + line);
  65. }
  66. // Track the updated file position
  67. if (current.added) {
  68. newLine += lines.length;
  69. }
  70. else {
  71. oldLine += lines.length;
  72. }
  73. }
  74. else {
  75. // Identical context lines. Track line changes
  76. if (oldRangeStart) {
  77. // Close out any changes that have been output (or join overlapping)
  78. if (lines.length <= context * 2 && i < diff.length - 2) {
  79. // Overlapping
  80. for (const line of contextLines(lines)) {
  81. curRange.push(line);
  82. }
  83. }
  84. else {
  85. // end the range and output
  86. const contextSize = Math.min(lines.length, context);
  87. for (const line of contextLines(lines.slice(0, contextSize))) {
  88. curRange.push(line);
  89. }
  90. const hunk = {
  91. oldStart: oldRangeStart,
  92. oldLines: (oldLine - oldRangeStart + contextSize),
  93. newStart: newRangeStart,
  94. newLines: (newLine - newRangeStart + contextSize),
  95. lines: curRange
  96. };
  97. hunks.push(hunk);
  98. oldRangeStart = 0;
  99. newRangeStart = 0;
  100. curRange = [];
  101. }
  102. }
  103. oldLine += lines.length;
  104. newLine += lines.length;
  105. }
  106. }
  107. // Step 2: eliminate the trailing `\n` from each line of each hunk, and, where needed, add
  108. // "\ No newline at end of file".
  109. for (const hunk of hunks) {
  110. for (let i = 0; i < hunk.lines.length; i++) {
  111. if (hunk.lines[i].endsWith('\n')) {
  112. hunk.lines[i] = hunk.lines[i].slice(0, -1);
  113. }
  114. else {
  115. hunk.lines.splice(i + 1, 0, '\\ No newline at end of file');
  116. i++; // Skip the line we just added, then continue iterating
  117. }
  118. }
  119. }
  120. return {
  121. oldFileName: oldFileName, newFileName: newFileName,
  122. oldHeader: oldHeader, newHeader: newHeader,
  123. hunks: hunks
  124. };
  125. }
  126. }
  127. /**
  128. * creates a unified diff patch.
  129. * @param patch either a single structured patch object (as returned by `structuredPatch`) or an array of them (as returned by `parsePatch`)
  130. */
  131. export function formatPatch(patch) {
  132. if (Array.isArray(patch)) {
  133. return patch.map(formatPatch).join('\n');
  134. }
  135. const ret = [];
  136. if (patch.oldFileName == patch.newFileName) {
  137. ret.push('Index: ' + patch.oldFileName);
  138. }
  139. ret.push('===================================================================');
  140. ret.push('--- ' + patch.oldFileName + (typeof patch.oldHeader === 'undefined' ? '' : '\t' + patch.oldHeader));
  141. ret.push('+++ ' + patch.newFileName + (typeof patch.newHeader === 'undefined' ? '' : '\t' + patch.newHeader));
  142. for (let i = 0; i < patch.hunks.length; i++) {
  143. const hunk = patch.hunks[i];
  144. // Unified Diff Format quirk: If the chunk size is 0,
  145. // the first number is one lower than one would expect.
  146. // https://www.artima.com/weblogs/viewpost.jsp?thread=164293
  147. if (hunk.oldLines === 0) {
  148. hunk.oldStart -= 1;
  149. }
  150. if (hunk.newLines === 0) {
  151. hunk.newStart -= 1;
  152. }
  153. ret.push('@@ -' + hunk.oldStart + ',' + hunk.oldLines
  154. + ' +' + hunk.newStart + ',' + hunk.newLines
  155. + ' @@');
  156. for (const line of hunk.lines) {
  157. ret.push(line);
  158. }
  159. }
  160. return ret.join('\n') + '\n';
  161. }
  162. export function createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {
  163. if (typeof options === 'function') {
  164. options = { callback: options };
  165. }
  166. if (!(options === null || options === void 0 ? void 0 : options.callback)) {
  167. const patchObj = structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options);
  168. if (!patchObj) {
  169. return;
  170. }
  171. return formatPatch(patchObj);
  172. }
  173. else {
  174. const { callback } = options;
  175. structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, Object.assign(Object.assign({}, options), { callback: patchObj => {
  176. if (!patchObj) {
  177. callback(undefined);
  178. }
  179. else {
  180. callback(formatPatch(patchObj));
  181. }
  182. } }));
  183. }
  184. }
  185. export function createPatch(fileName, oldStr, newStr, oldHeader, newHeader, options) {
  186. return createTwoFilesPatch(fileName, fileName, oldStr, newStr, oldHeader, newHeader, options);
  187. }
  188. /**
  189. * Split `text` into an array of lines, including the trailing newline character (where present)
  190. */
  191. function splitLines(text) {
  192. const hasTrailingNl = text.endsWith('\n');
  193. const result = text.split('\n').map(line => line + '\n');
  194. if (hasTrailingNl) {
  195. result.pop();
  196. }
  197. else {
  198. result.push(result.pop().slice(0, -1));
  199. }
  200. return result;
  201. }