reslop 0.2.7 → 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 +17 -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 +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  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 +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  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 -96
  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 +111 -48
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +15 -43
  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/{dash-blocks.js → dashboard/blocks.js} +131 -137
  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 +9 -14
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +123 -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 +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  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} +13 -60
  72. package/lib/render/tiles.js +109 -48
  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 +33 -39
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +181 -145
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +85 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +75 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -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} +156 -63
  120. package/lib/session/tasks.js +320 -65
  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 +65 -65
  126. package/lib/session/workspace.js +39 -78
  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 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  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
@@ -1,17 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('node:path');
4
-
5
- const tree = require('../dashboard/tree.js');
6
- const { FileIndex, HEAT_MS } = tree;
7
- const watch = require('./watch.js');
8
- const { UNKNOWN_PATH } = watch;
9
- const workspace = require('../workspace.js');
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');
10
8
  const { emptyDiff, countCommits, readDiffStat, taskStats, pickRun } = workspace;
11
- const npmInfo = require('../dashboard/npm.js');
12
- const { readNpmSummary } = npmInfo;
13
- const gitDeps = require('../git-deps.js');
14
- const { collectReportAsync } = gitDeps;
9
+ const { readNpmSummary } = require('../dashboard/npm.js');
10
+ const { collectReport } = require('../git/deps.js');
15
11
 
16
12
  const TICK_ID = 'repos';
17
13
  const HEAT_TIMER = 'repos-heat';
@@ -104,7 +100,7 @@ const readRuns = (row) => {
104
100
  };
105
101
 
106
102
  const reportCount = async (dir, kind) => {
107
- const report = await collectReportAsync(dir, kind);
103
+ const report = await collectReport(dir, kind);
108
104
  return report ? report.size : 0;
109
105
  };
110
106
 
@@ -138,13 +134,11 @@ const toTile = (row) => ({
138
134
  npmReady: row.npmReady,
139
135
  });
140
136
 
141
- const rowBusy = (row) =>
142
- !row.filesReady ||
143
- !row.commitsReady ||
144
- !row.diffReady ||
145
- !row.tasksReady ||
146
- !row.runsReady ||
147
- !row.npmReady;
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
+ };
148
142
 
149
143
  const classifyRest = (rest) => {
150
144
  const bag = freshBag();
@@ -172,7 +166,7 @@ const classifyRest = (rest) => {
172
166
  bag.runs = true;
173
167
  return bag;
174
168
  }
175
- if (rest === '.review' || rest.startsWith('.review/')) {
169
+ if (rest === REVIEW_DIR || rest.startsWith(`${REVIEW_DIR}/`)) {
176
170
  bag.tasks = true;
177
171
  return bag;
178
172
  }
@@ -193,13 +187,7 @@ class Workspace {
193
187
  this.rows = [];
194
188
  this.byName = new Map();
195
189
  this.cursor = 0;
196
- this.screen = {
197
- now: 0,
198
- frame: 0,
199
- cursor: 0,
200
- cols: 1,
201
- tiles: [],
202
- };
190
+ this.cols = 1;
203
191
  }
204
192
 
205
193
  seed() {
@@ -349,61 +337,50 @@ class Workspace {
349
337
  }
350
338
 
351
339
  view() {
352
- const screen = this.screen;
353
- screen.now = Date.now();
354
- screen.frame = this.ui.progressFrame;
355
- screen.cursor = this.cursor;
356
- screen.tiles = this.rows.map(toTile);
357
- return screen;
340
+ return {
341
+ now: Date.now(),
342
+ frame: this.ui.progressFrame,
343
+ cursor: this.cursor,
344
+ tiles: this.rows.map(toTile),
345
+ };
358
346
  }
359
347
 
360
348
  move(dx, dy) {
361
349
  const count = this.rows.length;
362
350
  if (!count) return;
363
- const cols = Math.max(1, this.screen.cols || 1);
351
+ const cols = Math.max(1, this.cols || 1);
364
352
  const next = this.cursor + dx + dy * cols;
365
353
  if (next < 0 || next >= count) return;
366
354
  this.cursor = next;
367
355
  if (this.ui.uiOpen) this.ui.paint();
368
356
  }
369
357
 
370
- enter(name) {
371
- const row = this.byName.get(name);
358
+ async switchTo(row) {
372
359
  const ui = this.ui;
373
- if (!row || ui.mode !== 'review') return Promise.resolve();
374
360
  ui.flushReview();
375
361
  this.stop();
376
- ui.lifecycle.stopWatch();
377
- ui.dashboard.stop();
378
- ui.loader.reset();
379
- ui.cwd = row.dir;
380
- ui.paths = [];
381
- ui.repoName = row.name;
382
- ui.branch = '';
383
- ui.rev = null;
384
- ui.revShort = '';
385
- ui.change = null;
386
- ui.dashboardHome = true;
387
- ui.collection.reset();
388
- ui.review.reset();
389
- ui.nav.pane = 'dashboard';
390
- ui.nav.tasksOpen = false;
391
- ui.nav.scroll = 0;
392
- ui.nav.clearSelection();
393
- ui.status = '';
362
+ ui.retarget({ cwd: row.dir, repoName: row.name, pane: 'dashboard' });
394
363
  try {
395
- ui.ensureRepo();
364
+ await ui.ensureRepo();
396
365
  } catch (error) {
397
366
  ui.status = error instanceof Error ? error.message : `${error}`;
398
- return this.show();
367
+ return { loading: this.show() };
399
368
  }
400
- const pending = ui.openLoad();
369
+ const loading = ui.openLoad();
401
370
  if (ui.uiOpen) {
402
371
  ui.lifecycle.startWatch();
403
372
  ui.dashboard.start();
404
373
  ui.paint();
405
374
  }
406
- return pending ?? Promise.resolve();
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;
407
384
  }
408
385
 
409
386
  openSelected() {
@@ -423,25 +400,9 @@ class Workspace {
423
400
  const ui = this.ui;
424
401
  if (!ui.workspaceRoot) return Promise.resolve();
425
402
  ui.flushReview();
426
- ui.dashboard.stop();
427
- ui.lifecycle.stopWatch();
428
- ui.loader.reset();
429
- ui.cwd = ui.workspaceRoot;
430
- ui.top = ui.workspaceRoot;
431
- ui.paths = [];
432
- ui.repoName = path.basename(ui.workspaceRoot);
433
- ui.branch = '';
434
- ui.rev = null;
435
- ui.revShort = '';
436
- ui.change = null;
437
- ui.dashboardHome = false;
438
- ui.collection.reset();
439
- ui.review.reset();
440
- ui.nav.pane = 'repos';
441
- ui.nav.tasksOpen = false;
442
- ui.nav.scroll = 0;
443
- ui.nav.clearSelection();
444
- ui.status = '';
403
+ const root = ui.workspaceRoot;
404
+ const repoName = path.basename(root);
405
+ ui.retarget({ cwd: root, top: root, repoName, pane: 'repos' });
445
406
  ui.loader.didLoad = true;
446
407
  this.start();
447
408
  if (ui.uiOpen) {
@@ -452,4 +413,4 @@ class Workspace {
452
413
  }
453
414
  }
454
415
 
455
- module.exports = { Workspace, classifyRest };
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
  };
@@ -1,12 +1,11 @@
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');
7
5
  const remote = require('./remote.js');
8
6
  const { RemoteClient, listUrl, parseNextPage, commentBody } = remote;
9
7
  const { importedTodo, noteFromLocation, splitNotes, loadChange } = remote;
8
+ const { stateIs, loadIssue } = remote;
10
9
 
11
10
  const USER_AGENT = 'reslop';
12
11
  const JSON_ACCEPT = 'application/json';
@@ -24,9 +23,16 @@ const GITLAB_MR_RE = new RegExp(
24
23
  'i',
25
24
  );
26
25
 
27
- const parseGitlabMrUrl = (text) => {
26
+ const GITLAB_ISSUE_RE = new RegExp(
27
+ String.raw`^(?:(https?)://)?(?:www\.)?([^/?#]+)/` +
28
+ String.raw`(.+?)(?:/-)?/issues/(\d+)` +
29
+ String.raw`/?(?:[?#].*)?$`,
30
+ 'i',
31
+ );
32
+
33
+ const parseGitlabUrl = (text, pattern) => {
28
34
  if (typeof text !== 'string' || !text) return null;
29
- const match = GITLAB_MR_RE.exec(text.trim());
35
+ const match = pattern.exec(text.trim());
30
36
  if (!match) return null;
31
37
  const scheme = (match[1] || 'https').toLowerCase();
32
38
  const host = match[2];
@@ -37,6 +43,35 @@ const parseGitlabMrUrl = (text) => {
37
43
  return { host, project, number, origin: `${scheme}://${host}` };
38
44
  };
39
45
 
46
+ const GITLAB_HOST_RE = new RegExp(
47
+ String.raw`^(?:(https?)://)?(?:www\.)?([^/?#]+\.[^/?#]+)/([^?#]+)`,
48
+ 'i',
49
+ );
50
+
51
+ const parseGitlabMrUrl = (text) => parseGitlabUrl(text, GITLAB_MR_RE);
52
+
53
+ const parseGitlabIssueUrl = (text) => {
54
+ const issue = parseGitlabUrl(text, GITLAB_ISSUE_RE);
55
+ return issue?.host.toLowerCase() === 'github.com' ? null : issue;
56
+ };
57
+
58
+ const parseGitlabIssueListUrl = (text) => {
59
+ if (typeof text !== 'string' || !text) return null;
60
+ const match = GITLAB_HOST_RE.exec(text.trim());
61
+ if (!match) return null;
62
+ const scheme = (match[1] || 'https').toLowerCase();
63
+ const host = match[2];
64
+ if (!host || host.toLowerCase() === 'github.com') return null;
65
+ let path = match[3].replace(/\/+$/, '');
66
+ if (/\/issues\/\d+/.test(path)) return null;
67
+ const listed = /(?:\/-)?\/issues$/.test(path);
68
+ if (listed) path = path.replace(/(?:\/-)?\/issues$/, '');
69
+ else if (path.includes('/-/')) return null;
70
+ path = path.replace(/\.git$/i, '');
71
+ if (!path.includes('/')) return null;
72
+ return { host, project: path, origin: `${scheme}://${host}`, list: true };
73
+ };
74
+
40
75
  const gitlabToken = (env) => {
41
76
  const from = env ?? process.env;
42
77
  return from.GITLAB_TOKEN || from.GL_TOKEN || '';
@@ -57,10 +92,10 @@ const client = new RemoteClient({
57
92
  limitStatus: 429,
58
93
  });
59
94
 
60
- const mrApiUrl = (mr) => {
61
- const project = encodeURIComponent(mr.project);
62
- return `${mr.origin}/api/v4/projects/${project}/merge_requests/${mr.number}`;
63
- };
95
+ const projectApiUrl = (ref) =>
96
+ `${ref.origin}/api/v4/projects/${encodeURIComponent(ref.project)}`;
97
+
98
+ const mrApiUrl = (mr) => `${projectApiUrl(mr)}/merge_requests/${mr.number}`;
64
99
 
65
100
  const usernameOf = (user) => user?.username || '';
66
101
 
@@ -133,6 +168,45 @@ const discussionToNotes = (discussions, items, paths = []) => {
133
168
  const loadDiscussion = (mr, options) =>
134
169
  client.list(listUrl(`${mrApiUrl(mr)}/discussions`), options);
135
170
 
171
+ const issueSite = (issue) => ({
172
+ repository: issue.project,
173
+ id: `${issue.host}/${issue.project}/issues`,
174
+ page: `${issue.origin}/${issue.project}/-/issues`,
175
+ });
176
+
177
+ const readIssue = (data) => ({
178
+ number: data.iid,
179
+ title: data.title,
180
+ body: data.description,
181
+ author: usernameOf(data.author),
182
+ state: data.state,
183
+ url: data.web_url,
184
+ comments: data.user_notes_count,
185
+ });
186
+
187
+ const readIssueNote = (note) => {
188
+ if (!isHashObject(note) || note.system === true) return null;
189
+ return { reviewer: usernameOf(note.author), body: note.body };
190
+ };
191
+
192
+ const isOpenIssue = (data) => {
193
+ if (!isHashObject(data)) return false;
194
+ return stateIs(data.state, 'opened');
195
+ };
196
+
197
+ const loadGitlabIssue = async (issue, options = {}) =>
198
+ loadIssue(issue, options, {
199
+ client,
200
+ host: IMPORT_HOST,
201
+ site: issueSite(issue),
202
+ issuesUrl: `${projectApiUrl(issue)}/issues`,
203
+ openState: 'opened',
204
+ comments: 'notes',
205
+ isOpen: isOpenIssue,
206
+ readIssue,
207
+ readComment: readIssueNote,
208
+ });
209
+
136
210
  const loadMergeRequest = (mr, options = {}) => {
137
211
  const url = mrApiUrl(mr);
138
212
  return loadChange(mr, options, {
@@ -149,8 +223,11 @@ const loadMergeRequest = (mr, options = {}) => {
149
223
 
150
224
  module.exports = {
151
225
  parseGitlabMrUrl,
226
+ parseGitlabIssueUrl,
227
+ parseGitlabIssueListUrl,
152
228
  gitlabToken,
153
229
  mrApiUrl,
154
230
  discussionToNotes,
231
+ loadGitlabIssue,
155
232
  loadMergeRequest,
156
233
  };