reslop 0.2.6 → 0.3.0

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 (158) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +61 -66
  10. package/lib/{files.js → common/files.js} +51 -52
  11. package/lib/common/find.js +61 -0
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +117 -51
  18. package/lib/dashboard/npm.js +133 -32
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +41 -31
  21. package/lib/dashboard/workspace.js +151 -0
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +123 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +240 -71
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +36 -39
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/dashboard/blocks.js +490 -0
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +22 -8
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +127 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +29 -13
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +89 -0
  66. package/lib/render/primitives.js +109 -16
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +214 -0
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +19 -61
  72. package/lib/render/tiles.js +203 -47
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +46 -14
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +9 -0
  81. package/lib/review/format.js +426 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +958 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +36 -44
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +122 -56
  100. package/lib/session/compose.js +197 -146
  101. package/lib/session/dashboard.js +183 -37
  102. package/lib/session/dispatch.js +120 -22
  103. package/lib/session/files-pane.js +60 -24
  104. package/lib/session/find.js +81 -0
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +96 -66
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +8 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/npm/packages.js +150 -0
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +216 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +166 -59
  120. package/lib/session/tasks.js +321 -66
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +72 -59
  126. package/lib/session/workspace.js +416 -0
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +2 -2
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -505
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -437
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -628
  148. package/lib/render/dash-blocks.js +0 -245
  149. package/lib/render/present.js +0 -19
  150. package/lib/render/report.js +0 -43
  151. package/lib/review.js +0 -744
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -0,0 +1,416 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { REVIEW_DIR } = require('../common/files.js');
5
+ const { FileIndex, HEAT_MS } = require('../dashboard/tree.js');
6
+ const { UNKNOWN_PATH } = require('../dashboard/watch.js');
7
+ const workspace = require('../dashboard/workspace.js');
8
+ const { emptyDiff, countCommits, readDiffStat, taskStats, pickRun } = workspace;
9
+ const { readNpmSummary } = require('../dashboard/npm.js');
10
+ const { collectReport } = require('../git/deps.js');
11
+
12
+ const TICK_ID = 'repos';
13
+ const HEAT_TIMER = 'repos-heat';
14
+
15
+ const freshBag = () => ({
16
+ files: [],
17
+ git: false,
18
+ runs: false,
19
+ tasks: false,
20
+ npm: false,
21
+ activity: false,
22
+ });
23
+
24
+ const fullBag = () => ({
25
+ files: [UNKNOWN_PATH],
26
+ git: true,
27
+ runs: true,
28
+ tasks: true,
29
+ npm: true,
30
+ activity: false,
31
+ });
32
+
33
+ const mergeBag = (left, right) => {
34
+ const base = left ?? freshBag();
35
+ return {
36
+ files: [...base.files, ...right.files],
37
+ git: base.git || right.git,
38
+ runs: base.runs || right.runs,
39
+ tasks: base.tasks || right.tasks,
40
+ npm: base.npm || right.npm,
41
+ activity: base.activity || right.activity,
42
+ };
43
+ };
44
+
45
+ const blankRow = (repo) => ({
46
+ name: repo.name,
47
+ dir: repo.dir,
48
+ index: new FileIndex(repo.dir),
49
+ bytes: 0,
50
+ exts: [],
51
+ tasks: 0,
52
+ tasksDone: 0,
53
+ commits: 0,
54
+ diff: emptyDiff(),
55
+ run: null,
56
+ activityAt: 0,
57
+ filesReady: false,
58
+ commitsReady: false,
59
+ diffReady: false,
60
+ tasksReady: false,
61
+ runsReady: false,
62
+ npm: null,
63
+ npmReady: false,
64
+ pending: null,
65
+ running: false,
66
+ });
67
+
68
+ const readFiles = async (row, bag) => {
69
+ if (!bag.files.length) return;
70
+ const unknown = bag.files.includes(UNKNOWN_PATH);
71
+ if (unknown || !row.index.ready) await row.index.rescan();
72
+ else await row.index.touch(bag.files);
73
+ if (!row.index.ready) return;
74
+ const summary = row.index.summary();
75
+ row.bytes = summary.total.bytes;
76
+ row.exts = summary.exts.slice(0, 3);
77
+ row.filesReady = true;
78
+ };
79
+
80
+ const readCommits = async (row) => {
81
+ row.commits = await countCommits(row.dir);
82
+ row.commitsReady = true;
83
+ };
84
+
85
+ const readDiff = async (row) => {
86
+ row.diff = await readDiffStat(row.dir);
87
+ row.diffReady = true;
88
+ };
89
+
90
+ const readTasks = (row) => {
91
+ const stats = taskStats(row.dir);
92
+ row.tasksDone = stats.done;
93
+ row.tasks = stats.total;
94
+ row.tasksReady = true;
95
+ };
96
+
97
+ const readRuns = (row) => {
98
+ row.run = pickRun(row.dir);
99
+ row.runsReady = true;
100
+ };
101
+
102
+ const reportCount = async (dir, kind) => {
103
+ const report = await collectReport(dir, kind);
104
+ return report ? report.size : 0;
105
+ };
106
+
107
+ const readNpm = async (row) => {
108
+ const summary = await readNpmSummary(row.dir, row.npm);
109
+ const [audit, outdated] = await Promise.all([
110
+ reportCount(row.dir, 'audit'),
111
+ reportCount(row.dir, 'outdated'),
112
+ ]);
113
+ row.npm = { ...summary, audit, outdated };
114
+ row.npmReady = true;
115
+ };
116
+
117
+ const toTile = (row) => ({
118
+ id: row.name,
119
+ name: row.name,
120
+ bytes: row.bytes,
121
+ exts: row.exts,
122
+ tasks: row.tasks,
123
+ tasksDone: row.tasksDone,
124
+ commits: row.commits,
125
+ diff: row.diff,
126
+ run: row.run,
127
+ activityAt: row.activityAt,
128
+ filesReady: row.filesReady,
129
+ commitsReady: row.commitsReady,
130
+ diffReady: row.diffReady,
131
+ tasksReady: row.tasksReady,
132
+ runsReady: row.runsReady,
133
+ npm: row.npm,
134
+ npmReady: row.npmReady,
135
+ });
136
+
137
+ const rowBusy = (row) => {
138
+ if (!row.filesReady || !row.commitsReady) return true;
139
+ if (!row.diffReady || !row.tasksReady) return true;
140
+ return !row.runsReady || !row.npmReady;
141
+ };
142
+
143
+ const classifyRest = (rest) => {
144
+ const bag = freshBag();
145
+ if (!rest) {
146
+ bag.files.push(UNKNOWN_PATH);
147
+ bag.git = true;
148
+ bag.runs = true;
149
+ bag.tasks = true;
150
+ bag.npm = true;
151
+ bag.activity = true;
152
+ return bag;
153
+ }
154
+ if (
155
+ rest === 'package.json' ||
156
+ rest === 'package-lock.json' ||
157
+ rest === 'node_modules/.package-lock.json'
158
+ ) {
159
+ bag.npm = true;
160
+ }
161
+ if (rest === '.git' || rest.startsWith('.git/')) {
162
+ bag.git = true;
163
+ return bag;
164
+ }
165
+ if (rest === '.log' || rest.startsWith('.log/')) {
166
+ bag.runs = true;
167
+ return bag;
168
+ }
169
+ if (rest === REVIEW_DIR || rest.startsWith(`${REVIEW_DIR}/`)) {
170
+ bag.tasks = true;
171
+ return bag;
172
+ }
173
+ bag.files.push(rest);
174
+ bag.activity = true;
175
+ return bag;
176
+ };
177
+
178
+ class Workspace {
179
+ constructor(ui) {
180
+ this.ui = ui;
181
+ this.epoch = 0;
182
+ this.reset();
183
+ }
184
+
185
+ reset() {
186
+ this.stop();
187
+ this.rows = [];
188
+ this.byName = new Map();
189
+ this.cursor = 0;
190
+ this.cols = 1;
191
+ }
192
+
193
+ seed() {
194
+ const info = this.ui.workspaceInfo;
195
+ const repos = info ? info.repos : [];
196
+ this.rows = repos.map(blankRow);
197
+ this.byName = new Map(this.rows.map((row) => [row.name, row]));
198
+ if (this.cursor >= this.rows.length) this.cursor = 0;
199
+ }
200
+
201
+ stale(epoch) {
202
+ return epoch !== this.epoch || !this.active;
203
+ }
204
+
205
+ publish(epoch) {
206
+ if (this.stale(epoch)) return;
207
+ this.afterDraw();
208
+ if (this.ui.uiOpen && this.ui.nav.pane === 'repos') this.ui.paint();
209
+ }
210
+
211
+ async settle(epoch, read) {
212
+ await read();
213
+ this.publish(epoch);
214
+ }
215
+
216
+ async apply(row, bag, epoch) {
217
+ if (bag.activity) row.activityAt = Date.now();
218
+ const jobs = [];
219
+ if (bag.files.length) {
220
+ jobs.push(this.settle(epoch, () => readFiles(row, bag)));
221
+ }
222
+ if (bag.git) {
223
+ jobs.push(this.settle(epoch, () => readCommits(row)));
224
+ jobs.push(this.settle(epoch, () => readDiff(row)));
225
+ }
226
+ if (bag.tasks) jobs.push(this.settle(epoch, () => readTasks(row)));
227
+ if (bag.runs) jobs.push(this.settle(epoch, () => readRuns(row)));
228
+ if (bag.npm) jobs.push(this.settle(epoch, () => readNpm(row)));
229
+ await Promise.all(jobs);
230
+ }
231
+
232
+ async pump(row) {
233
+ row.running = true;
234
+ while (row.pending && this.active) {
235
+ const bag = row.pending;
236
+ const epoch = this.epoch;
237
+ row.pending = null;
238
+ try {
239
+ await this.apply(row, bag, epoch);
240
+ } catch {
241
+ // keep the previous snapshot until the next event
242
+ }
243
+ if (this.stale(epoch)) break;
244
+ this.afterDraw();
245
+ if (this.ui.uiOpen && this.ui.nav.pane === 'repos') this.ui.paint();
246
+ }
247
+ row.running = false;
248
+ if (row.pending && this.active) {
249
+ void this.pump(row);
250
+ return;
251
+ }
252
+ this.afterDraw();
253
+ if (this.ui.uiOpen && this.ui.nav.pane === 'repos') this.ui.paint();
254
+ }
255
+
256
+ kick(row, bag) {
257
+ if (!row || !this.active) return;
258
+ if (bag.activity) row.activityAt = Date.now();
259
+ row.pending = mergeBag(row.pending, bag);
260
+ if (row.running) return;
261
+ void this.pump(row);
262
+ }
263
+
264
+ requestAll() {
265
+ for (const row of this.rows) this.kick(row, fullBag());
266
+ }
267
+
268
+ notePaths(paths) {
269
+ if (!this.active) return;
270
+ for (const rel of paths) {
271
+ if (rel === UNKNOWN_PATH) return void this.requestAll();
272
+ }
273
+ const found = new Map();
274
+ for (const rel of paths) {
275
+ const slash = rel.indexOf('/');
276
+ const name = slash < 0 ? rel : rel.slice(0, slash);
277
+ const rest = slash < 0 ? '' : rel.slice(slash + 1);
278
+ const row = this.byName.get(name);
279
+ if (!row) continue;
280
+ found.set(name, mergeBag(found.get(name), classifyRest(rest)));
281
+ }
282
+ for (const [name, bag] of found) this.kick(this.byName.get(name), bag);
283
+ if (found.size) {
284
+ this.afterDraw();
285
+ if (this.ui.uiOpen) this.ui.paint();
286
+ }
287
+ }
288
+
289
+ start() {
290
+ if (this.active) return;
291
+ this.active = true;
292
+ if (!this.rows.length) this.seed();
293
+ this.requestAll();
294
+ }
295
+
296
+ stop() {
297
+ this.epoch += 1;
298
+ this.active = false;
299
+ this.cancelTimers();
300
+ }
301
+
302
+ cancelTimers() {
303
+ const { progress, term } = this.ui;
304
+ if (!progress || !term) return;
305
+ progress.stop(TICK_ID);
306
+ term.cancel(HEAT_TIMER);
307
+ }
308
+
309
+ hotUntil() {
310
+ let until = 0;
311
+ for (const row of this.rows) {
312
+ until = Math.max(until, row.activityAt + HEAT_MS);
313
+ }
314
+ return until;
315
+ }
316
+
317
+ afterDraw() {
318
+ if (!this.active) return;
319
+ const visible = this.ui.uiOpen && this.ui.nav.pane === 'repos';
320
+ if (!visible) return void this.cancelTimers();
321
+ const now = Date.now();
322
+ const until = this.hotUntil();
323
+ const hot = now < until;
324
+ const running = this.rows.some((row) => row.run?.status === 'running');
325
+ const loading = this.rows.some((row) => rowBusy(row));
326
+ const { progress, term } = this.ui;
327
+ if (running || hot || loading) progress.start(TICK_ID);
328
+ else progress.stop(TICK_ID);
329
+ if (!hot) return;
330
+ term.later(
331
+ HEAT_TIMER,
332
+ () => {
333
+ if (this.ui.nav.pane === 'repos') this.ui.paint();
334
+ },
335
+ until - now + 20,
336
+ );
337
+ }
338
+
339
+ view() {
340
+ return {
341
+ now: Date.now(),
342
+ frame: this.ui.progressFrame,
343
+ cursor: this.cursor,
344
+ tiles: this.rows.map(toTile),
345
+ };
346
+ }
347
+
348
+ move(dx, dy) {
349
+ const count = this.rows.length;
350
+ if (!count) return;
351
+ const cols = Math.max(1, this.cols || 1);
352
+ const next = this.cursor + dx + dy * cols;
353
+ if (next < 0 || next >= count) return;
354
+ this.cursor = next;
355
+ if (this.ui.uiOpen) this.ui.paint();
356
+ }
357
+
358
+ async switchTo(row) {
359
+ const ui = this.ui;
360
+ ui.flushReview();
361
+ this.stop();
362
+ ui.retarget({ cwd: row.dir, repoName: row.name, pane: 'dashboard' });
363
+ try {
364
+ await ui.ensureRepo();
365
+ } catch (error) {
366
+ ui.status = error instanceof Error ? error.message : `${error}`;
367
+ return { loading: this.show() };
368
+ }
369
+ const loading = ui.openLoad();
370
+ if (ui.uiOpen) {
371
+ ui.lifecycle.startWatch();
372
+ ui.dashboard.start();
373
+ ui.paint();
374
+ }
375
+ return { loading };
376
+ }
377
+
378
+ async enter(name) {
379
+ const row = this.byName.get(name);
380
+ const ui = this.ui;
381
+ if (!row || ui.mode !== 'review') return;
382
+ const opened = await ui.ops.hold(this.switchTo(row));
383
+ if (opened) await opened.loading;
384
+ }
385
+
386
+ openSelected() {
387
+ const row = this.rows[this.cursor];
388
+ if (!row) return Promise.resolve();
389
+ return this.enter(row.name);
390
+ }
391
+
392
+ openAt(id) {
393
+ const index = this.rows.findIndex((row) => row.name === id);
394
+ if (index < 0) return Promise.resolve();
395
+ this.cursor = index;
396
+ return this.openSelected();
397
+ }
398
+
399
+ show() {
400
+ const ui = this.ui;
401
+ if (!ui.workspaceRoot) return Promise.resolve();
402
+ ui.flushReview();
403
+ const root = ui.workspaceRoot;
404
+ const repoName = path.basename(root);
405
+ ui.retarget({ cwd: root, top: root, repoName, pane: 'repos' });
406
+ ui.loader.didLoad = true;
407
+ this.start();
408
+ if (ui.uiOpen) {
409
+ ui.lifecycle.startWatch();
410
+ ui.paint();
411
+ }
412
+ return Promise.resolve();
413
+ }
414
+ }
415
+
416
+ module.exports = { Workspace };
@@ -2,7 +2,6 @@
2
2
 
3
3
  const READ_METHODS = [
4
4
  'load',
5
- 'loadAsync',
6
5
  'loadExtras',
7
6
  'toplevel',
8
7
  'resolveRev',
@@ -12,7 +11,6 @@ const READ_METHODS = [
12
11
 
13
12
  const CHANGE_METHODS = [
14
13
  'add',
15
- 'addAsync',
16
14
  'unstage',
17
15
  'revert',
18
16
  'revertFile',
@@ -23,7 +21,7 @@ const CHANGE_METHODS = [
23
21
 
24
22
  const BRANCH_METHODS = [
25
23
  'commit',
26
- 'commitAsync',
24
+ 'updateCommit',
27
25
  'hasStaged',
28
26
  'lastMessage',
29
27
  'commitMessage',
@@ -31,23 +29,14 @@ const BRANCH_METHODS = [
31
29
  'listCommits',
32
30
  'currentBranch',
33
31
  'checkout',
34
- 'checkoutAsync',
35
32
  'createBranch',
36
- 'createBranchAsync',
37
33
  'rebase',
38
- 'rebaseAsync',
39
34
  'drop',
40
- 'dropAsync',
41
35
  'dropCommit',
42
- 'dropCommitAsync',
43
36
  'applyFixup',
44
- 'applyFixupAsync',
45
37
  'reword',
46
- 'rewordAsync',
47
38
  'pull',
48
- 'pullAsync',
49
39
  'push',
50
- 'pushAsync',
51
40
  ];
52
41
 
53
42
  const VIEW_CAPS = { read: true, changes: false, branches: false, review: true };
@@ -57,6 +46,8 @@ const SOURCE_CAPS = {
57
46
  commit: VIEW_CAPS,
58
47
  pr: VIEW_CAPS,
59
48
  mr: VIEW_CAPS,
49
+ issue: VIEW_CAPS,
50
+ 'gl-issue': VIEW_CAPS,
60
51
  readonly: VIEW_CAPS,
61
52
  };
62
53
 
@@ -1,13 +1,12 @@
1
1
  'use strict';
2
2
 
3
- const metautil = require('metautil');
4
- const { isHashObject } = metautil;
5
- const files = require('./files.js');
6
- const { isPathInScope } = files;
3
+ const { isHashObject } = require('metautil');
4
+ const { isPathInScope } = require('../common/files.js');
5
+ const { resource } = require('../common/utilities.js');
7
6
  const remote = require('./remote.js');
8
7
  const { RemoteClient, isAbortError, listUrl, parseLinkNext } = remote;
9
8
  const { commentBody, importedTodo, noteFromLocation } = remote;
10
- const { splitNotes, loadChange } = remote;
9
+ const { splitNotes, loadChange, stateIs, loadIssue } = remote;
11
10
 
12
11
  const GITHUB_API = 'https://api.github.com';
13
12
  const API_VERSION = '2022-11-28';
@@ -17,14 +16,9 @@ const DIFF_ACCEPT = 'application/vnd.github.diff';
17
16
  const PR_TODO_FILE = 'pull request';
18
17
  const IMPORT_HOST = 'github';
19
18
  const GRAPHQL_URL = `${GITHUB_API}/graphql`;
20
- const REVIEW_THREADS_QUERY =
21
- 'query($owner: String!, $name: String!, $number: Int!, $after: String) {' +
22
- ' repository(owner: $owner, name: $name) {' +
23
- ' pullRequest(number: $number) {' +
24
- ' reviewThreads(first: 100, after: $after) {' +
25
- ' pageInfo { hasNextPage endCursor }' +
26
- ' nodes { isResolved comments(first: 100) { nodes { databaseId } } }' +
27
- ' } } } }';
19
+ const REVIEW_THREADS_QUERY = resource(__dirname, 'review-threads.graphql')
20
+ .replace(/\s+/g, ' ')
21
+ .trim();
28
22
 
29
23
  const GITHUB_PR_RE = new RegExp(
30
24
  String.raw`^(?:https?://)?(?:www\.)?github\.com/` +
@@ -35,18 +29,41 @@ const GITHUB_PR_RE = new RegExp(
35
29
  'i',
36
30
  );
37
31
 
38
- const parseGithubPrUrl = (text) => {
32
+ const GITHUB_ISSUE_RE = new RegExp(
33
+ String.raw`^(?:https?://)?(?:www\.)?github\.com/` +
34
+ String.raw`([^/?#]+)/([^/?#]+)/issues/(\d+)` +
35
+ String.raw`/?(?:[?#].*)?$`,
36
+ 'i',
37
+ );
38
+
39
+ const GITHUB_ISSUE_LIST_RE = new RegExp(
40
+ String.raw`^(?:https?://)?(?:www\.)?github\.com/` +
41
+ String.raw`([^/?#]+)/([^/?#]+?)(?:\.git)?` +
42
+ String.raw`(?:/issues)?` +
43
+ String.raw`/?(?:[?#].*)?$`,
44
+ 'i',
45
+ );
46
+
47
+ const parseGithubUrl = (text, pattern) => {
39
48
  if (typeof text !== 'string' || !text) return null;
40
- const match = GITHUB_PR_RE.exec(text.trim());
49
+ const match = pattern.exec(text.trim());
41
50
  if (!match) return null;
42
51
  const owner = match[1];
43
52
  const repo = match[2].replace(/\.git$/i, '');
53
+ if (!owner || !repo) return null;
54
+ if (!match[3]) return { owner, repo, list: true };
44
55
  const number = parseInt(match[3], 10);
45
- const hasNumber = Number.isFinite(number) && number >= 1;
46
- if (!owner || !repo || !hasNumber) return null;
56
+ if (!Number.isFinite(number) || number < 1) return null;
47
57
  return { owner, repo, number };
48
58
  };
49
59
 
60
+ const parseGithubPrUrl = (text) => parseGithubUrl(text, GITHUB_PR_RE);
61
+
62
+ const parseGithubIssueUrl = (text) => parseGithubUrl(text, GITHUB_ISSUE_RE);
63
+
64
+ const parseGithubIssueListUrl = (text) =>
65
+ parseGithubUrl(text, GITHUB_ISSUE_LIST_RE);
66
+
50
67
  const githubToken = (env) => {
51
68
  const from = env ?? process.env;
52
69
  return from.GITHUB_TOKEN || from.GH_TOKEN || '';
@@ -69,12 +86,13 @@ const client = new RemoteClient({
69
86
  nextPage: parseLinkNext,
70
87
  });
71
88
 
72
- const prApiUrl = (pr) => {
73
- const owner = encodeURIComponent(pr.owner);
74
- const repo = encodeURIComponent(pr.repo);
75
- return `${GITHUB_API}/repos/${owner}/${repo}/pulls/${pr.number}`;
89
+ const repositoryApiUrl = ({ owner, repo }) => {
90
+ const name = `${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
91
+ return `${GITHUB_API}/repos/${name}`;
76
92
  };
77
93
 
94
+ const prApiUrl = (pr) => `${repositoryApiUrl(pr)}/pulls/${pr.number}`;
95
+
78
96
  const loginOf = (user) => user?.login || '';
79
97
 
80
98
  const toChange = (pr, data) => {
@@ -131,12 +149,15 @@ const discussionToNotes = (discussion, items, paths = []) => {
131
149
  const comments = discussion.reviewComments ?? [];
132
150
  const reviews = discussion.reviews ?? [];
133
151
  const issueComments = discussion.issueComments ?? [];
134
- return splitNotes([
135
- ...comments.map((comment) =>
136
- reviewCommentToNote(comment, items, resolvedById, paths),
137
- ),
138
- ...[...reviews, ...issueComments].map((record) => summaryToTodo(record)),
139
- ]);
152
+ const reviewNote = (comment) => {
153
+ const note = reviewCommentToNote(comment, items, resolvedById, paths);
154
+ return note;
155
+ };
156
+ const reviewNotes = comments.map(reviewNote);
157
+ const summaries = [...reviews, ...issueComments];
158
+ const summaryNotes = summaries.map(summaryToTodo);
159
+ const notes = [...reviewNotes, ...summaryNotes];
160
+ return splitNotes(notes);
140
161
  };
141
162
 
142
163
  const requestThreads = async (variables, options) => {
@@ -184,12 +205,15 @@ const loadResolvedById = async (pr, options) => {
184
205
 
185
206
  const loadDiscussion = async (pr, options) => {
186
207
  const pull = prApiUrl(pr);
187
- const issue = pull.replace('/pulls/', '/issues/');
208
+ const issue = `${repositoryApiUrl(pr)}/issues/${pr.number}`;
188
209
  const list = (url) => client.list(listUrl(url), options);
189
- const reviewComments = await list(`${pull}/comments`);
190
- const reviews = await list(`${pull}/reviews`);
191
- const issueComments = await list(`${issue}/comments`);
192
- const resolvedById = await loadResolvedById(pr, options);
210
+ const [reviewComments, reviews, issueComments, resolvedById] =
211
+ await Promise.all([
212
+ list(`${pull}/comments`),
213
+ list(`${pull}/reviews`),
214
+ list(`${issue}/comments`),
215
+ loadResolvedById(pr, options),
216
+ ]);
193
217
  return { reviewComments, reviews, issueComments, resolvedById };
194
218
  };
195
219
 
@@ -204,10 +228,56 @@ const loadPullRequest = (pr, options = {}) =>
204
228
  discussionToNotes,
205
229
  });
206
230
 
231
+ const issueSite = (issue) => {
232
+ const repository = `${issue.owner}/${issue.repo}`;
233
+ return {
234
+ repository,
235
+ id: `github.com/${repository}/issues`,
236
+ page: `https://github.com/${repository}/issues`,
237
+ };
238
+ };
239
+
240
+ const readIssue = (data) => ({
241
+ number: data.number,
242
+ title: data.title,
243
+ body: data.body,
244
+ author: loginOf(data.user),
245
+ state: data.state,
246
+ url: data['html_url'],
247
+ comments: data.comments,
248
+ });
249
+
250
+ const readIssueComment = (comment) => ({
251
+ reviewer: loginOf(comment.user),
252
+ body: comment.body,
253
+ });
254
+
255
+ const isOpenIssue = (data) => {
256
+ if (!isHashObject(data)) return false;
257
+ if (isHashObject(data['pull_request'])) return false;
258
+ return stateIs(data.state, 'open');
259
+ };
260
+
261
+ const loadGithubIssue = async (issue, options = {}) =>
262
+ loadIssue(issue, options, {
263
+ client,
264
+ host: IMPORT_HOST,
265
+ site: issueSite(issue),
266
+ issuesUrl: `${repositoryApiUrl(issue)}/issues`,
267
+ openState: 'open',
268
+ comments: 'comments',
269
+ isOpen: isOpenIssue,
270
+ readIssue,
271
+ readComment: readIssueComment,
272
+ });
273
+
207
274
  module.exports = {
208
275
  parseGithubPrUrl,
276
+ parseGithubIssueUrl,
277
+ parseGithubIssueListUrl,
209
278
  githubToken,
210
279
  prApiUrl,
211
280
  discussionToNotes,
212
281
  loadPullRequest,
282
+ loadGithubIssue,
213
283
  };