reslop 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
@@ -36,19 +36,24 @@ const wholeChanged = (a, b) => ({
36
36
  newSpans: b.length ? [{ text: b.join(''), changed: true }] : [],
37
37
  });
38
38
 
39
+ const sharedEnds = (a, b) => {
40
+ let lo = 0;
41
+ while (lo < a.length && lo < b.length && a[lo] === b[lo]) lo += 1;
42
+ let hiA = a.length;
43
+ let hiB = b.length;
44
+ while (hiA > lo && hiB > lo && a[hiA - 1] === b[hiB - 1]) {
45
+ hiA -= 1;
46
+ hiB -= 1;
47
+ }
48
+ return { lo, hiA, hiB };
49
+ };
50
+
39
51
  const lcsLength = (a, b) => {
40
52
  const n = a.length;
41
53
  const m = b.length;
42
54
  if (n === 0 || m === 0) return 0;
43
55
  if (n * m > LCS_CELL_LIMIT) {
44
- let lo = 0;
45
- while (lo < n && lo < m && a[lo] === b[lo]) lo += 1;
46
- let hiA = n;
47
- let hiB = m;
48
- while (hiA > lo && hiB > lo && a[hiA - 1] === b[hiB - 1]) {
49
- hiA -= 1;
50
- hiB -= 1;
51
- }
56
+ const { lo, hiA } = sharedEnds(a, b);
52
57
  return lo + (n - hiA);
53
58
  }
54
59
  let prev = new Uint32Array(m + 1);
@@ -74,7 +79,7 @@ const tokenSimilarity = (a, b) => {
74
79
  return (2 * lcs) / (a.length + b.length);
75
80
  };
76
81
 
77
- const fillLcsTable = (a, b) => {
82
+ const lcsTable = (a, b) => {
78
83
  const n = a.length;
79
84
  const m = b.length;
80
85
  const dp = new Array(n + 1);
@@ -91,7 +96,8 @@ const fillLcsTable = (a, b) => {
91
96
  return dp;
92
97
  };
93
98
 
94
- const tracebackLcs = (a, b, dp) => {
99
+ const lcsOps = (a, b) => {
100
+ const dp = lcsTable(a, b);
95
101
  const rev = new Array(a.length + b.length - dp[a.length][b.length]);
96
102
  let n = 0;
97
103
  let i = a.length;
@@ -101,44 +107,29 @@ const tracebackLcs = (a, b, dp) => {
101
107
  rev[n++] = { type: 'eq', text: a[i - 1] };
102
108
  i -= 1;
103
109
  j -= 1;
104
- continue;
105
- }
106
- if (dp[i - 1][j] >= dp[i][j - 1]) {
110
+ } else if (dp[i - 1][j] >= dp[i][j - 1]) {
107
111
  rev[n++] = { type: 'del', text: a[i - 1] };
108
112
  i -= 1;
109
- continue;
113
+ } else {
114
+ rev[n++] = { type: 'add', text: b[j - 1] };
115
+ j -= 1;
110
116
  }
111
- rev[n++] = { type: 'add', text: b[j - 1] };
112
- j -= 1;
113
117
  }
114
- while (i > 0) {
115
- rev[n++] = { type: 'del', text: a[i - 1] };
116
- i -= 1;
117
- }
118
- while (j > 0) {
119
- rev[n++] = { type: 'add', text: b[j - 1] };
120
- j -= 1;
121
- }
122
- rev.reverse();
123
- return rev;
118
+ for (; i > 0; i--) rev[n++] = { type: 'del', text: a[i - 1] };
119
+ for (; j > 0; j--) rev[n++] = { type: 'add', text: b[j - 1] };
120
+ return rev.reverse();
124
121
  };
125
122
 
126
- const lcsOps = (a, b) => tracebackLcs(a, b, fillLcsTable(a, b));
127
-
128
- const lcsSpans = (a, b) => {
123
+ const opsSpans = (ops) => {
129
124
  const oldParts = [];
130
125
  const newParts = [];
131
- for (const op of lcsOps(a, b)) {
132
- if (op.type === 'eq') {
133
- oldParts.push({ text: op.text, changed: false });
134
- newParts.push({ text: op.text, changed: false });
135
- continue;
126
+ for (const op of ops) {
127
+ if (op.type !== 'add') {
128
+ oldParts.push({ text: op.text, changed: op.type === 'del' });
136
129
  }
137
- if (op.type === 'del') {
138
- oldParts.push({ text: op.text, changed: true });
139
- continue;
130
+ if (op.type !== 'del') {
131
+ newParts.push({ text: op.text, changed: op.type === 'add' });
140
132
  }
141
- newParts.push({ text: op.text, changed: true });
142
133
  }
143
134
  return {
144
135
  oldSpans: collapseSpans(oldParts),
@@ -146,67 +137,35 @@ const lcsSpans = (a, b) => {
146
137
  };
147
138
  };
148
139
 
149
- const trimSharedEnds = (a, b) => {
150
- let lo = 0;
151
- while (lo < a.length && lo < b.length && a[lo] === b[lo]) lo += 1;
152
- let hiA = a.length;
153
- let hiB = b.length;
154
- while (hiA > lo && hiB > lo && a[hiA - 1] === b[hiB - 1]) {
155
- hiA -= 1;
156
- hiB -= 1;
157
- }
158
- return {
159
- prefix: a.slice(0, lo),
160
- suffix: a.slice(hiA),
161
- midA: a.slice(lo, hiA),
162
- midB: b.slice(lo, hiB),
163
- };
164
- };
165
-
166
- const diffTokenMiddle = (midA, midB) => {
167
- if (midA.length * midB.length > LCS_CELL_LIMIT) {
168
- return wholeChanged(midA, midB);
169
- }
170
- if (midA.length === 0 && midB.length === 0) {
171
- return { oldSpans: [], newSpans: [] };
172
- }
173
- if (midA.length === 0 || midB.length === 0) return wholeChanged(midA, midB);
174
- return lcsSpans(midA, midB);
175
- };
176
-
177
140
  const joinAffix = (prefix, mid, suffix) => {
178
- const hasPrefix = prefix.length > 0;
179
- const hasSuffix = suffix.length > 0;
180
- const extra = (hasPrefix ? 1 : 0) + (hasSuffix ? 1 : 0);
181
- const parts = new Array(mid.length + extra);
182
- const at = hasPrefix ? 1 : 0;
183
- if (hasPrefix) {
184
- parts[0] = { text: prefix.join(''), changed: false };
185
- }
186
- for (let i = 0; i < mid.length; i++) parts[at + i] = mid[i];
187
- if (hasSuffix) {
188
- parts[at + mid.length] = { text: suffix.join(''), changed: false };
189
- }
141
+ const parts = [];
142
+ if (prefix.length) parts.push({ text: prefix.join(''), changed: false });
143
+ for (const span of mid) parts.push(span);
144
+ if (suffix.length) parts.push({ text: suffix.join(''), changed: false });
190
145
  return collapseSpans(parts);
191
146
  };
192
147
 
193
- const joinTrimmedSpans = (prefix, mid, suffix) => ({
194
- oldSpans: joinAffix(prefix, mid.oldSpans, suffix),
195
- newSpans: joinAffix(prefix, mid.newSpans, suffix),
196
- });
148
+ const diffTrimmed = (a, b, spansOfOps) => {
149
+ const { lo, hiA, hiB } = sharedEnds(a, b);
150
+ const midA = a.slice(lo, hiA);
151
+ const midB = b.slice(lo, hiB);
152
+ const tooBig = midA.length * midB.length > LCS_CELL_LIMIT;
153
+ const whole = !midA.length || !midB.length || tooBig;
154
+ const mid = whole ? wholeChanged(midA, midB) : spansOfOps(lcsOps(midA, midB));
155
+ const prefix = a.slice(0, lo);
156
+ const suffix = a.slice(hiA);
157
+ return {
158
+ oldSpans: joinAffix(prefix, mid.oldSpans, suffix),
159
+ newSpans: joinAffix(prefix, mid.newSpans, suffix),
160
+ };
161
+ };
197
162
 
198
163
  const diffTokens = (a, b) => {
199
- if (a.length === 0 && b.length === 0) {
200
- return { oldSpans: [], newSpans: [] };
201
- }
202
- if (a.length === 0 || b.length === 0) return wholeChanged(a, b);
203
- const trimmed = trimSharedEnds(a, b);
204
- const mid = diffTokenMiddle(trimmed.midA, trimmed.midB);
205
- const joined = joinTrimmedSpans(trimmed.prefix, mid, trimmed.suffix);
206
- if (isNoisy(joined.oldSpans) || isNoisy(joined.newSpans)) {
164
+ const spans = diffTrimmed(a, b, opsSpans);
165
+ if (isNoisy(spans.oldSpans) || isNoisy(spans.newSpans)) {
207
166
  return wholeChanged(a, b);
208
167
  }
209
- return joined;
168
+ return spans;
210
169
  };
211
170
 
212
171
  const splitCamel = (part) => {
@@ -214,31 +173,23 @@ const splitCamel = (part) => {
214
173
  return part.match(CAMEL_RE) ?? [part];
215
174
  };
216
175
 
217
- const splitIdentWords = (part) => {
218
- const words = [];
219
- for (const chunk of part.split(/(_+)/)) {
220
- if (chunk === '') continue;
221
- if (chunk.includes('_')) {
222
- words.push(chunk);
223
- continue;
224
- }
225
- for (const word of splitCamel(chunk)) words.push(word);
226
- }
227
- return words;
228
- };
229
-
230
176
  const splitPieces = (text) => {
231
- if (text === '') return [];
232
177
  PIECE_RE.lastIndex = 0;
233
- const coarse = text.match(PIECE_RE) ?? [];
234
178
  const pieces = [];
235
- for (const part of coarse) {
236
- for (const word of splitIdentWords(part)) pieces.push(word);
179
+ for (const part of text.match(PIECE_RE) ?? []) {
180
+ for (const chunk of part.split(/(_+)/)) {
181
+ if (chunk === '') continue;
182
+ if (chunk.includes('_')) {
183
+ pieces.push(chunk);
184
+ continue;
185
+ }
186
+ for (const word of splitCamel(chunk)) pieces.push(word);
187
+ }
237
188
  }
238
189
  return pieces;
239
190
  };
240
191
 
241
- const spansFromWordOps = (ops) => {
192
+ const refineWordOps = (ops) => {
242
193
  const oldParts = [];
243
194
  const newParts = [];
244
195
  let i = 0;
@@ -257,23 +208,18 @@ const spansFromWordOps = (ops) => {
257
208
  else adds.push(ops[i].text);
258
209
  i += 1;
259
210
  }
260
- const one = dels.length === 1 && adds.length === 1;
261
- const close =
262
- one &&
263
- tokenSimilarity(graphemes(dels[0]), graphemes(adds[0])) >=
264
- WORD_REFINE_MIN;
265
- if (close) {
266
- const inner = diffTokens(graphemes(dels[0]), graphemes(adds[0]));
267
- for (const span of inner.oldSpans) oldParts.push(span);
268
- for (const span of inner.newSpans) newParts.push(span);
269
- continue;
270
- }
271
- if (dels.length) {
272
- oldParts.push({ text: dels.join(''), changed: true });
273
- }
274
- if (adds.length) {
275
- newParts.push({ text: adds.join(''), changed: true });
211
+ if (dels.length === 1 && adds.length === 1) {
212
+ const delChars = graphemes(dels[0]);
213
+ const addChars = graphemes(adds[0]);
214
+ if (tokenSimilarity(delChars, addChars) >= WORD_REFINE_MIN) {
215
+ const inner = diffTokens(delChars, addChars);
216
+ for (const span of inner.oldSpans) oldParts.push(span);
217
+ for (const span of inner.newSpans) newParts.push(span);
218
+ continue;
219
+ }
276
220
  }
221
+ if (dels.length) oldParts.push({ text: dels.join(''), changed: true });
222
+ if (adds.length) newParts.push({ text: adds.join(''), changed: true });
277
223
  }
278
224
  return {
279
225
  oldSpans: collapseSpans(oldParts),
@@ -302,7 +248,9 @@ const editSize = (run) => Math.max(textSize(run.del), textSize(run.add));
302
248
 
303
249
  const absorbTinyEquals = (runs) => {
304
250
  for (let k = 1; k < runs.length - 1; k++) {
305
- const [before, eq, after] = runs.slice(k - 1, k + 2);
251
+ const before = runs[k - 1];
252
+ const eq = runs[k];
253
+ const after = runs[k + 1];
306
254
  if (!eq.eq) continue;
307
255
  const size = textSize(eq.del);
308
256
  if (size > editSize(before) || size > editSize(after)) continue;
@@ -327,7 +275,7 @@ const runOps = (runs) => {
327
275
  return ops;
328
276
  };
329
277
 
330
- const cleanOps = (ops) => runOps(absorbTinyEquals(opRuns(ops)));
278
+ const wordSpans = (ops) => refineWordOps(runOps(absorbTinyEquals(opRuns(ops))));
331
279
 
332
280
  const sharedShare = (spans) => {
333
281
  let shared = 0;
@@ -343,23 +291,7 @@ const sharedShare = (spans) => {
343
291
  const diffChars = (oldText, newText) => {
344
292
  const a = splitPieces(oldText);
345
293
  const b = splitPieces(newText);
346
- if (a.length === 0 && b.length === 0) {
347
- return { oldSpans: [], newSpans: [] };
348
- }
349
- if (a.length === 0 || b.length === 0) return wholeChanged(a, b);
350
- const { prefix, suffix, midA, midB } = trimSharedEnds(a, b);
351
- let mid;
352
- if (midA.length === 0 && midB.length === 0) {
353
- mid = { oldSpans: [], newSpans: [] };
354
- } else if (midA.length === 0 || midB.length === 0) {
355
- mid = wholeChanged(midA, midB);
356
- } else if (midA.length * midB.length > LCS_CELL_LIMIT) {
357
- mid = wholeChanged(midA, midB);
358
- } else {
359
- mid = spansFromWordOps(cleanOps(lcsOps(midA, midB)));
360
- }
361
- const oldSpans = joinAffix(prefix, mid.oldSpans, suffix);
362
- const newSpans = joinAffix(prefix, mid.newSpans, suffix);
294
+ const { oldSpans, newSpans } = diffTrimmed(a, b, wordSpans);
363
295
  const oldShare = sharedShare(oldSpans);
364
296
  const newShare = sharedShare(newSpans);
365
297
  if (oldShare < WHOLE_MIN_SHARED && newShare < WHOLE_MIN_SHARED) {
package/lib/diff/items.js CHANGED
@@ -3,36 +3,34 @@
3
3
  const patch = require('./patch.js');
4
4
  const { splitHunk, formatHunkHeader, formatPatch } = patch;
5
5
 
6
- const INDEX_SIDE = {
7
- staged: 'new',
8
- unstaged: 'old',
9
- untracked: 'old',
10
- commit: 'old',
11
- pr: 'old',
6
+ const BINARY_ITEM = {
7
+ hunk: null,
8
+ blockId: null,
9
+ patchAdd: '',
10
+ patchRevert: '',
12
11
  };
13
12
 
14
- const splitLines = (content) => {
15
- if (content === '') return { lines: [''], noNl: true };
16
- const parts = content.split('\n');
17
- const noNl = parts.at(-1) !== '';
18
- if (!noNl) parts.pop();
19
- return { lines: parts, noNl };
20
- };
13
+ const indexSide = (origin) => (origin === 'staged' ? 'new' : 'old');
21
14
 
22
15
  const synthesizeNewFile = (relPath, content) => {
23
- const { lines, noNl } = splitLines(content);
24
- const body = lines.map((text, index) => ({
16
+ const noNl = content === '' || !content.endsWith('\n');
17
+ const texts = noNl ? content.split('\n') : content.slice(0, -1).split('\n');
18
+ const lines = texts.map((text, index) => ({
25
19
  type: 'add',
26
20
  text,
27
- noNl: noNl && index === lines.length - 1,
21
+ noNl: noNl && index === texts.length - 1,
28
22
  blockId: 0,
29
23
  }));
30
- const oldStart = 0;
31
- const oldCount = 0;
32
- const newStart = 1;
33
- const newCount = body.length;
34
- const header = formatHunkHeader(oldStart, oldCount, newStart, newCount);
35
- const hunk = { oldStart, oldCount, newStart, newCount, header, lines: body };
24
+ const newCount = lines.length;
25
+ const header = formatHunkHeader(0, 0, 1, newCount);
26
+ const hunk = {
27
+ oldStart: 0,
28
+ oldCount: 0,
29
+ newStart: 1,
30
+ newCount,
31
+ header,
32
+ lines,
33
+ };
36
34
  const preamble = [
37
35
  `diff --git a/${relPath} b/${relPath}`,
38
36
  'new file mode 100644',
@@ -52,21 +50,16 @@ const synthesizeNewFile = (relPath, content) => {
52
50
 
53
51
  const itemsFromFiles = (files, origin) => {
54
52
  const items = [];
53
+ const side = indexSide(origin);
55
54
  for (const file of files) {
56
55
  if (file.isBinary) {
57
- const hunk = null;
58
- const blockId = null;
59
- const patchAdd = '';
60
- const patchRevert = '';
61
- items.push({ origin, file, hunk, blockId, patchAdd, patchRevert });
56
+ items.push({ origin, file, ...BINARY_ITEM });
62
57
  continue;
63
58
  }
64
59
  for (const rawHunk of file.hunks) {
65
60
  const { hunk, blocks } = splitHunk(rawHunk);
66
- for (const block of blocks) {
67
- const blockId = block.id;
68
- const indexSide = INDEX_SIDE[origin] ?? 'old';
69
- const patchAdd = formatPatch(file, hunk, blockId, indexSide);
61
+ for (const { id: blockId } of blocks) {
62
+ const patchAdd = formatPatch(file, hunk, blockId, side);
70
63
  const patchRevert = formatPatch(file, hunk, blockId, 'new');
71
64
  items.push({ origin, file, hunk, blockId, patchAdd, patchRevert });
72
65
  }
@@ -76,24 +69,17 @@ const itemsFromFiles = (files, origin) => {
76
69
  };
77
70
 
78
71
  const refreshIndexPatches = (items, item) => {
79
- if (!item.hunk || !item.file) return items;
72
+ if (!item.hunk) return items;
73
+ const isPeer = (other) =>
74
+ other.hunk === item.hunk && other.file === item.file;
75
+ const peers = items.filter(isPeer);
80
76
  const sides = {};
81
- for (const other of items) {
82
- if (other.hunk !== item.hunk || other.file !== item.file) continue;
83
- sides[other.blockId] = INDEX_SIDE[other.origin] ?? 'old';
84
- }
85
- const { length } = items;
86
- const next = new Array(length);
87
- for (let i = 0; i < length; i++) {
88
- const other = items[i];
89
- if (other.hunk !== item.hunk || other.file !== item.file) {
90
- next[i] = other;
91
- continue;
92
- }
77
+ for (const peer of peers) sides[peer.blockId] = indexSide(peer.origin);
78
+ return items.map((other) => {
79
+ if (!isPeer(other)) return other;
93
80
  const patchAdd = formatPatch(other.file, other.hunk, other.blockId, sides);
94
- next[i] = { ...other, patchAdd };
95
- }
96
- return next;
81
+ return { ...other, patchAdd };
82
+ });
97
83
  };
98
84
 
99
85
  module.exports = {
package/lib/diff/parse.js CHANGED
@@ -1,57 +1,34 @@
1
1
  'use strict';
2
2
 
3
- const LINE_TYPE = {
4
- '+': 'add',
5
- '-': 'del',
6
- ' ': 'ctx',
7
- };
3
+ const LINE_TYPE = { '+': 'add', '-': 'del', ' ': 'ctx' };
4
+
5
+ const HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/;
8
6
 
9
7
  const parseGitPaths = (line) => {
10
8
  const rest = line.slice('diff --git '.length);
11
9
  const mid = rest.lastIndexOf(' b/');
12
10
  if (mid < 0 || !rest.startsWith('a/')) return { oldPath: '', newPath: '' };
13
- const oldPath = rest.slice(2, mid);
14
- const newPath = rest.slice(mid + 3);
15
- return { oldPath, newPath };
16
- };
17
-
18
- const stripAb = (raw) => {
19
- if (raw === '/dev/null') return raw;
20
- if (raw.startsWith('a/') || raw.startsWith('b/')) return raw.slice(2);
21
- return raw;
11
+ return { oldPath: rest.slice(2, mid), newPath: rest.slice(mid + 3) };
22
12
  };
23
13
 
24
14
  const parsePathLine = (line) => {
25
15
  const space = line.indexOf(' ');
26
16
  if (space < 0) return '';
27
17
  let rest = line.slice(space + 1);
28
- if (rest.startsWith('"') && rest.endsWith('"')) {
29
- rest = rest.slice(1, -1);
30
- }
18
+ if (rest.startsWith('"') && rest.endsWith('"')) rest = rest.slice(1, -1);
31
19
  const tab = rest.indexOf('\t');
32
20
  if (tab >= 0) rest = rest.slice(0, tab);
33
- return stripAb(rest);
21
+ if (rest.startsWith('a/') || rest.startsWith('b/')) return rest.slice(2);
22
+ return rest;
34
23
  };
35
24
 
36
- const parseHunkHeader = (line) => {
37
- const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/.exec(line);
38
- if (!match) return null;
39
- const oldStart = parseInt(match[1], 10);
40
- const oldCount = match[2] === undefined ? 1 : parseInt(match[2], 10);
41
- const newStart = parseInt(match[3], 10);
42
- const newCount = match[4] === undefined ? 1 : parseInt(match[4], 10);
43
- return { oldStart, oldCount, newStart, newCount, header: line };
44
- };
45
-
46
- const closeFile = (file, files) => {
47
- if (file) files.push(file);
48
- };
25
+ const parseCount = (text) => (text === undefined ? 1 : parseInt(text, 10));
49
26
 
50
- const createFile = (line) => {
51
- const paths = parseGitPaths(line);
52
- return {
53
- oldPath: paths.oldPath,
54
- newPath: paths.newPath,
27
+ const startGitFile = (line, ctx) => {
28
+ if (ctx.file) ctx.files.push(ctx.file);
29
+ ctx.hunk = null;
30
+ ctx.file = {
31
+ ...parseGitPaths(line),
55
32
  isNew: false,
56
33
  isDeleted: false,
57
34
  isBinary: false,
@@ -60,12 +37,6 @@ const createFile = (line) => {
60
37
  };
61
38
  };
62
39
 
63
- const startGitFile = (line, ctx) => {
64
- ctx.hunk = null;
65
- closeFile(ctx.file, ctx.files);
66
- ctx.file = createFile(line);
67
- };
68
-
69
40
  const markBinary = (line, ctx) => {
70
41
  ctx.file.isBinary = true;
71
42
  ctx.file.preamble.push(line);
@@ -97,9 +68,16 @@ const setNewPath = (line, ctx) => {
97
68
  };
98
69
 
99
70
  const startHunk = (line, ctx) => {
100
- const meta = parseHunkHeader(line);
101
- if (!meta) return;
102
- ctx.hunk = { ...meta, lines: [] };
71
+ const match = HUNK_RE.exec(line);
72
+ if (!match) return;
73
+ ctx.hunk = {
74
+ oldStart: parseInt(match[1], 10),
75
+ oldCount: parseCount(match[2]),
76
+ newStart: parseInt(match[3], 10),
77
+ newCount: parseCount(match[4]),
78
+ header: line,
79
+ lines: [],
80
+ };
103
81
  ctx.file.hunks.push(ctx.hunk);
104
82
  };
105
83
 
@@ -121,11 +99,8 @@ const takeHunkLine = (line, hunk) => {
121
99
  return;
122
100
  }
123
101
  const type = LINE_TYPE[line[0]];
124
- if (type) {
125
- hunk.lines.push({ type, text: line.slice(1), noNl: false });
126
- return;
127
- }
128
- hunk.lines.push({ type: 'ctx', text: line, noNl: false });
102
+ const text = type ? line.slice(1) : line;
103
+ hunk.lines.push({ type: type ?? 'ctx', text, noNl: false });
129
104
  };
130
105
 
131
106
  const parseDiff = (text) => {
@@ -143,13 +118,10 @@ const parseDiff = (text) => {
143
118
  continue;
144
119
  }
145
120
  if (!ctx.file) continue;
146
- if (!ctx.hunk) {
147
- ctx.file.preamble.push(line);
148
- continue;
149
- }
150
- takeHunkLine(line, ctx.hunk);
121
+ if (ctx.hunk) takeHunkLine(line, ctx.hunk);
122
+ else ctx.file.preamble.push(line);
151
123
  }
152
- closeFile(ctx.file, files);
124
+ if (ctx.file) files.push(ctx.file);
153
125
  return files;
154
126
  };
155
127
 
package/lib/diff/patch.js CHANGED
@@ -3,14 +3,7 @@
3
3
  const align = require('./align.js');
4
4
  const { isCtxType } = align;
5
5
 
6
- const LINE_MARK = {
7
- add: '+',
8
- del: '-',
9
- ctx: ' ',
10
- warn: ' ',
11
- note: ' ',
12
- noteSep: ' ',
13
- };
6
+ const LINE_MARK = { add: '+', del: '-' };
14
7
 
15
8
  const splitHunk = (hunk) => {
16
9
  const { oldStart, oldCount, newStart, newCount, header } = hunk;
@@ -33,9 +26,8 @@ const splitHunk = (hunk) => {
33
26
  return { hunk: nextHunk, blocks };
34
27
  };
35
28
 
36
- const otherContext = (contextSide, blockId) => {
37
- if (contextSide === 'new') return 'new';
38
- if (contextSide === 'old') return 'old';
29
+ const sideOf = (contextSide, blockId) => {
30
+ if (typeof contextSide === 'string') return contextSide;
39
31
  return contextSide[blockId] ?? 'old';
40
32
  };
41
33
 
@@ -46,12 +38,8 @@ const flattenBlock = (hunk, blockId, contextSide) => {
46
38
  out.push(line);
47
39
  continue;
48
40
  }
49
- const side = otherContext(contextSide, line.blockId);
50
- if (side === 'old' && line.type === 'del') {
51
- out.push({ type: 'ctx', text: line.text, noNl: line.noNl });
52
- continue;
53
- }
54
- if (side === 'new' && line.type === 'add') {
41
+ const kept = sideOf(contextSide, line.blockId) === 'old' ? 'del' : 'add';
42
+ if (line.type === kept) {
55
43
  out.push({ type: 'ctx', text: line.text, noNl: line.noNl });
56
44
  }
57
45
  }
@@ -62,8 +50,8 @@ const countSides = (lines) => {
62
50
  let oldCount = 0;
63
51
  let newCount = 0;
64
52
  for (const line of lines) {
65
- if (isCtxType(line.type) || line.type === 'del') oldCount += 1;
66
- if (isCtxType(line.type) || line.type === 'add') newCount += 1;
53
+ if (line.type !== 'add') oldCount += 1;
54
+ if (line.type !== 'del') newCount += 1;
67
55
  }
68
56
  return { oldCount, newCount };
69
57
  };
@@ -71,24 +59,15 @@ const countSides = (lines) => {
71
59
  const formatHunkHeader = (oldStart, oldCount, newStart, newCount) =>
72
60
  `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`;
73
61
 
74
- const formatBodyLine = (line) => {
75
- const rows = [`${LINE_MARK[line.type] ?? ' '}${line.text}`];
76
- if (line.noNl) rows.push('\');
77
- return rows;
78
- };
79
-
80
62
  const formatPatch = (file, hunk, blockId, contextSide) => {
81
63
  const lines = flattenBlock(hunk, blockId, contextSide);
82
- const counts = countSides(lines);
83
- const header = formatHunkHeader(
84
- hunk.oldStart,
85
- counts.oldCount,
86
- hunk.newStart,
87
- counts.newCount,
88
- );
64
+ const { oldCount, newCount } = countSides(lines);
65
+ const { oldStart, newStart } = hunk;
66
+ const header = formatHunkHeader(oldStart, oldCount, newStart, newCount);
89
67
  const parts = [...file.preamble, header];
90
68
  for (const line of lines) {
91
- for (const row of formatBodyLine(line)) parts.push(row);
69
+ parts.push(`${LINE_MARK[line.type] ?? ' '}${line.text}`);
70
+ if (line.noNl) parts.push('\');
92
71
  }
93
72
  return `${parts.join('\n')}\n`;
94
73
  };