reslop 0.2.4 → 0.2.6

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 (109) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE +1 -1
  3. package/README.md +12 -13
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -12
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +48 -18
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +196 -404
  14. package/lib/deps.js +240 -399
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +138 -141
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +90 -206
  25. package/lib/editor.js +98 -55
  26. package/lib/files.js +86 -103
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +182 -344
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +157 -132
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +16 -26
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +22 -35
  44. package/lib/highlight/txt.js +9 -22
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +62 -85
  47. package/lib/manifest.js +67 -130
  48. package/lib/npm-commands.js +119 -189
  49. package/lib/remote.js +141 -197
  50. package/lib/render/branches.js +66 -73
  51. package/lib/render/chrome.js +264 -323
  52. package/lib/render/commits.js +187 -500
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -228
  58. package/lib/render/files.js +45 -99
  59. package/lib/render/frame.js +129 -183
  60. package/lib/render/notes.js +144 -160
  61. package/lib/render/npm.js +234 -112
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +131 -170
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -28
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -38
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -78
  72. package/lib/report-test.js +256 -141
  73. package/lib/report-tools.js +96 -161
  74. package/lib/review-merge.js +114 -152
  75. package/lib/review.js +115 -189
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +89 -75
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +82 -63
  80. package/lib/session/branches.js +193 -247
  81. package/lib/session/change-actions.js +171 -214
  82. package/lib/session/commit.js +280 -342
  83. package/lib/session/compose.js +559 -674
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +164 -157
  86. package/lib/session/edit-click.js +57 -0
  87. package/lib/session/files-pane.js +85 -53
  88. package/lib/session/items.js +73 -104
  89. package/lib/session/lifecycle.js +403 -541
  90. package/lib/session/load.js +48 -68
  91. package/lib/session/navigation.js +96 -87
  92. package/lib/session/npm.js +409 -345
  93. package/lib/session/ops.js +196 -63
  94. package/lib/session/pointer.js +23 -15
  95. package/lib/session/progress.js +33 -21
  96. package/lib/session/review.js +33 -54
  97. package/lib/session/tasks.js +333 -0
  98. package/lib/session/templates.js +51 -59
  99. package/lib/session/terminal.js +85 -136
  100. package/lib/session/unit.js +3 -4
  101. package/lib/session/update.js +76 -97
  102. package/lib/session/view.js +52 -83
  103. package/lib/session/watch.js +208 -204
  104. package/lib/session.js +112 -237
  105. package/lib/update.js +45 -116
  106. package/lib/utilities.js +67 -89
  107. package/lib/wrap.js +33 -44
  108. package/package.json +1 -1
  109. package/lib/session/todos.js +0 -293
@@ -6,6 +6,7 @@ const { graphemes } = ansi;
6
6
  const LCS_CELL_LIMIT = 250000;
7
7
  const NOISY_TINY_SPANS = 6;
8
8
  const WORD_REFINE_MIN = 0.6;
9
+ const WHOLE_MIN_SHARED = 0.25;
9
10
  const PIECE_RE = /[A-Za-z_$][\w$]*|\d+|\s+|./g;
10
11
  const CAMEL_RE = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+|./g;
11
12
 
@@ -35,19 +36,24 @@ const wholeChanged = (a, b) => ({
35
36
  newSpans: b.length ? [{ text: b.join(''), changed: true }] : [],
36
37
  });
37
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
+
38
51
  const lcsLength = (a, b) => {
39
52
  const n = a.length;
40
53
  const m = b.length;
41
54
  if (n === 0 || m === 0) return 0;
42
55
  if (n * m > LCS_CELL_LIMIT) {
43
- let lo = 0;
44
- while (lo < n && lo < m && a[lo] === b[lo]) lo += 1;
45
- let hiA = n;
46
- let hiB = m;
47
- while (hiA > lo && hiB > lo && a[hiA - 1] === b[hiB - 1]) {
48
- hiA -= 1;
49
- hiB -= 1;
50
- }
56
+ const { lo, hiA } = sharedEnds(a, b);
51
57
  return lo + (n - hiA);
52
58
  }
53
59
  let prev = new Uint32Array(m + 1);
@@ -73,7 +79,7 @@ const tokenSimilarity = (a, b) => {
73
79
  return (2 * lcs) / (a.length + b.length);
74
80
  };
75
81
 
76
- const fillLcsTable = (a, b) => {
82
+ const lcsTable = (a, b) => {
77
83
  const n = a.length;
78
84
  const m = b.length;
79
85
  const dp = new Array(n + 1);
@@ -90,7 +96,8 @@ const fillLcsTable = (a, b) => {
90
96
  return dp;
91
97
  };
92
98
 
93
- const tracebackLcs = (a, b, dp) => {
99
+ const lcsOps = (a, b) => {
100
+ const dp = lcsTable(a, b);
94
101
  const rev = new Array(a.length + b.length - dp[a.length][b.length]);
95
102
  let n = 0;
96
103
  let i = a.length;
@@ -100,44 +107,29 @@ const tracebackLcs = (a, b, dp) => {
100
107
  rev[n++] = { type: 'eq', text: a[i - 1] };
101
108
  i -= 1;
102
109
  j -= 1;
103
- continue;
104
- }
105
- if (dp[i - 1][j] >= dp[i][j - 1]) {
110
+ } else if (dp[i - 1][j] >= dp[i][j - 1]) {
106
111
  rev[n++] = { type: 'del', text: a[i - 1] };
107
112
  i -= 1;
108
- continue;
113
+ } else {
114
+ rev[n++] = { type: 'add', text: b[j - 1] };
115
+ j -= 1;
109
116
  }
110
- rev[n++] = { type: 'add', text: b[j - 1] };
111
- j -= 1;
112
117
  }
113
- while (i > 0) {
114
- rev[n++] = { type: 'del', text: a[i - 1] };
115
- i -= 1;
116
- }
117
- while (j > 0) {
118
- rev[n++] = { type: 'add', text: b[j - 1] };
119
- j -= 1;
120
- }
121
- rev.reverse();
122
- 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();
123
121
  };
124
122
 
125
- const lcsOps = (a, b) => tracebackLcs(a, b, fillLcsTable(a, b));
126
-
127
- const lcsSpans = (a, b) => {
123
+ const opsSpans = (ops) => {
128
124
  const oldParts = [];
129
125
  const newParts = [];
130
- for (const op of lcsOps(a, b)) {
131
- if (op.type === 'eq') {
132
- oldParts.push({ text: op.text, changed: false });
133
- newParts.push({ text: op.text, changed: false });
134
- continue;
126
+ for (const op of ops) {
127
+ if (op.type !== 'add') {
128
+ oldParts.push({ text: op.text, changed: op.type === 'del' });
135
129
  }
136
- if (op.type === 'del') {
137
- oldParts.push({ text: op.text, changed: true });
138
- continue;
130
+ if (op.type !== 'del') {
131
+ newParts.push({ text: op.text, changed: op.type === 'add' });
139
132
  }
140
- newParts.push({ text: op.text, changed: true });
141
133
  }
142
134
  return {
143
135
  oldSpans: collapseSpans(oldParts),
@@ -145,67 +137,35 @@ const lcsSpans = (a, b) => {
145
137
  };
146
138
  };
147
139
 
148
- const trimSharedEnds = (a, b) => {
149
- let lo = 0;
150
- while (lo < a.length && lo < b.length && a[lo] === b[lo]) lo += 1;
151
- let hiA = a.length;
152
- let hiB = b.length;
153
- while (hiA > lo && hiB > lo && a[hiA - 1] === b[hiB - 1]) {
154
- hiA -= 1;
155
- hiB -= 1;
156
- }
157
- return {
158
- prefix: a.slice(0, lo),
159
- suffix: a.slice(hiA),
160
- midA: a.slice(lo, hiA),
161
- midB: b.slice(lo, hiB),
162
- };
163
- };
164
-
165
- const diffTokenMiddle = (midA, midB) => {
166
- if (midA.length * midB.length > LCS_CELL_LIMIT) {
167
- return wholeChanged(midA, midB);
168
- }
169
- if (midA.length === 0 && midB.length === 0) {
170
- return { oldSpans: [], newSpans: [] };
171
- }
172
- if (midA.length === 0 || midB.length === 0) return wholeChanged(midA, midB);
173
- return lcsSpans(midA, midB);
174
- };
175
-
176
140
  const joinAffix = (prefix, mid, suffix) => {
177
- const hasPrefix = prefix.length > 0;
178
- const hasSuffix = suffix.length > 0;
179
- const extra = (hasPrefix ? 1 : 0) + (hasSuffix ? 1 : 0);
180
- const parts = new Array(mid.length + extra);
181
- const at = hasPrefix ? 1 : 0;
182
- if (hasPrefix) {
183
- parts[0] = { text: prefix.join(''), changed: false };
184
- }
185
- for (let i = 0; i < mid.length; i++) parts[at + i] = mid[i];
186
- if (hasSuffix) {
187
- parts[at + mid.length] = { text: suffix.join(''), changed: false };
188
- }
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 });
189
145
  return collapseSpans(parts);
190
146
  };
191
147
 
192
- const joinTrimmedSpans = (prefix, mid, suffix) => ({
193
- oldSpans: joinAffix(prefix, mid.oldSpans, suffix),
194
- newSpans: joinAffix(prefix, mid.newSpans, suffix),
195
- });
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
+ };
196
162
 
197
163
  const diffTokens = (a, b) => {
198
- if (a.length === 0 && b.length === 0) {
199
- return { oldSpans: [], newSpans: [] };
200
- }
201
- if (a.length === 0 || b.length === 0) return wholeChanged(a, b);
202
- const trimmed = trimSharedEnds(a, b);
203
- const mid = diffTokenMiddle(trimmed.midA, trimmed.midB);
204
- const joined = joinTrimmedSpans(trimmed.prefix, mid, trimmed.suffix);
205
- if (isNoisy(joined.oldSpans) || isNoisy(joined.newSpans)) {
164
+ const spans = diffTrimmed(a, b, opsSpans);
165
+ if (isNoisy(spans.oldSpans) || isNoisy(spans.newSpans)) {
206
166
  return wholeChanged(a, b);
207
167
  }
208
- return joined;
168
+ return spans;
209
169
  };
210
170
 
211
171
  const splitCamel = (part) => {
@@ -213,31 +173,23 @@ const splitCamel = (part) => {
213
173
  return part.match(CAMEL_RE) ?? [part];
214
174
  };
215
175
 
216
- const splitIdentWords = (part) => {
217
- const words = [];
218
- for (const chunk of part.split(/(_+)/)) {
219
- if (chunk === '') continue;
220
- if (chunk.includes('_')) {
221
- words.push(chunk);
222
- continue;
223
- }
224
- for (const word of splitCamel(chunk)) words.push(word);
225
- }
226
- return words;
227
- };
228
-
229
176
  const splitPieces = (text) => {
230
- if (text === '') return [];
231
177
  PIECE_RE.lastIndex = 0;
232
- const coarse = text.match(PIECE_RE) ?? [];
233
178
  const pieces = [];
234
- for (const part of coarse) {
235
- 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
+ }
236
188
  }
237
189
  return pieces;
238
190
  };
239
191
 
240
- const spansFromWordOps = (ops) => {
192
+ const refineWordOps = (ops) => {
241
193
  const oldParts = [];
242
194
  const newParts = [];
243
195
  let i = 0;
@@ -256,23 +208,18 @@ const spansFromWordOps = (ops) => {
256
208
  else adds.push(ops[i].text);
257
209
  i += 1;
258
210
  }
259
- const one = dels.length === 1 && adds.length === 1;
260
- const close =
261
- one &&
262
- tokenSimilarity(graphemes(dels[0]), graphemes(adds[0])) >=
263
- WORD_REFINE_MIN;
264
- if (close) {
265
- const inner = diffTokens(graphemes(dels[0]), graphemes(adds[0]));
266
- for (const span of inner.oldSpans) oldParts.push(span);
267
- for (const span of inner.newSpans) newParts.push(span);
268
- continue;
269
- }
270
- if (dels.length) {
271
- oldParts.push({ text: dels.join(''), changed: true });
272
- }
273
- if (adds.length) {
274
- 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
+ }
275
220
  }
221
+ if (dels.length) oldParts.push({ text: dels.join(''), changed: true });
222
+ if (adds.length) newParts.push({ text: adds.join(''), changed: true });
276
223
  }
277
224
  return {
278
225
  oldSpans: collapseSpans(oldParts),
@@ -280,26 +227,76 @@ const spansFromWordOps = (ops) => {
280
227
  };
281
228
  };
282
229
 
230
+ const opRuns = (ops) => {
231
+ const runs = [];
232
+ for (const op of ops) {
233
+ const eq = op.type === 'eq';
234
+ let run = runs[runs.length - 1];
235
+ if (!run || run.eq !== eq) {
236
+ run = { eq, del: [], add: [] };
237
+ runs.push(run);
238
+ }
239
+ if (op.type !== 'add') run.del.push(op.text);
240
+ if (op.type !== 'del') run.add.push(op.text);
241
+ }
242
+ return runs;
243
+ };
244
+
245
+ const textSize = (parts) => parts.join('').length;
246
+
247
+ const editSize = (run) => Math.max(textSize(run.del), textSize(run.add));
248
+
249
+ const absorbTinyEquals = (runs) => {
250
+ for (let k = 1; k < runs.length - 1; k++) {
251
+ const before = runs[k - 1];
252
+ const eq = runs[k];
253
+ const after = runs[k + 1];
254
+ if (!eq.eq) continue;
255
+ const size = textSize(eq.del);
256
+ if (size > editSize(before) || size > editSize(after)) continue;
257
+ const del = [...before.del, ...eq.del, ...after.del];
258
+ const add = [...before.add, ...eq.add, ...after.add];
259
+ runs.splice(k - 1, 3, { eq: false, del, add });
260
+ k = 0;
261
+ }
262
+ return runs;
263
+ };
264
+
265
+ const runOps = (runs) => {
266
+ const ops = [];
267
+ for (const run of runs) {
268
+ if (run.eq) {
269
+ for (const text of run.del) ops.push({ type: 'eq', text });
270
+ continue;
271
+ }
272
+ for (const text of run.del) ops.push({ type: 'del', text });
273
+ for (const text of run.add) ops.push({ type: 'add', text });
274
+ }
275
+ return ops;
276
+ };
277
+
278
+ const wordSpans = (ops) => refineWordOps(runOps(absorbTinyEquals(opRuns(ops))));
279
+
280
+ const sharedShare = (spans) => {
281
+ let shared = 0;
282
+ let total = 0;
283
+ for (const span of spans) {
284
+ const size = span.text.replace(/\s/g, '').length;
285
+ total += size;
286
+ if (!span.changed) shared += size;
287
+ }
288
+ return total === 0 ? 1 : shared / total;
289
+ };
290
+
283
291
  const diffChars = (oldText, newText) => {
284
292
  const a = splitPieces(oldText);
285
293
  const b = splitPieces(newText);
286
- if (a.length === 0 && b.length === 0) {
287
- return { oldSpans: [], newSpans: [] };
288
- }
289
- if (a.length === 0 || b.length === 0) return wholeChanged(a, b);
290
- const { prefix, suffix, midA, midB } = trimSharedEnds(a, b);
291
- let mid;
292
- if (midA.length === 0 && midB.length === 0) {
293
- mid = { oldSpans: [], newSpans: [] };
294
- } else if (midA.length === 0 || midB.length === 0) {
295
- mid = wholeChanged(midA, midB);
296
- } else if (midA.length * midB.length > LCS_CELL_LIMIT) {
297
- mid = wholeChanged(midA, midB);
298
- } else {
299
- mid = spansFromWordOps(lcsOps(midA, midB));
294
+ const { oldSpans, newSpans } = diffTrimmed(a, b, wordSpans);
295
+ const oldShare = sharedShare(oldSpans);
296
+ const newShare = sharedShare(newSpans);
297
+ if (oldShare < WHOLE_MIN_SHARED && newShare < WHOLE_MIN_SHARED) {
298
+ return wholeChanged(a, b);
300
299
  }
301
- const oldSpans = joinAffix(prefix, mid.oldSpans, suffix);
302
- const newSpans = joinAffix(prefix, mid.newSpans, suffix);
303
300
  return { oldSpans, newSpans };
304
301
  };
305
302
 
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