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
@@ -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
  };
@@ -1,15 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const metautil = require('metautil');
4
- const { delay, jsonParse, isHashObject } = metautil;
5
- const utilities = require('./utilities.js');
6
- const { oneLine } = utilities;
7
- const files = require('./files.js');
8
- const { itemPath, isPathInScope } = files;
9
- const diff = require('./diff/diff.js');
10
- const { parseDiff, itemsFromFiles, isCtxType } = diff;
11
- const deps = require('./deps.js');
12
- const { foldDepItems } = deps;
3
+ const { delay, jsonParse, isHashObject } = require('metautil');
4
+ const { oneLine, trimText } = require('../common/utilities.js');
5
+ const { itemPath, isPathInScope, ISSUE_FILE } = require('../common/files.js');
6
+ const { parseDiff, itemsFromFiles, isCtxType } = require('../diff/diff.js');
7
+ const { foldDepItems } = require('../deps/deps.js');
13
8
 
14
9
  const RETRY_ATTEMPTS = 3;
15
10
  const RETRY_DELAY_MS = 200;
@@ -62,19 +57,18 @@ class RemoteClient {
62
57
  throw this.error('PARSE', `${this.name} API: invalid ${label}`);
63
58
  }
64
59
 
65
- httpError(status, body, token) {
60
+ httpError(status, body, token, subject = this.subject) {
66
61
  const { name, prefix } = this;
67
- const text = `${body ?? ''}`.trim();
62
+ const text = trimText(body);
68
63
  const parsed = jsonParse(text);
69
64
  const fields = this.errorFields;
70
65
  const field = fields.find((key) => typeof parsed?.[key] === 'string');
71
- const message =
72
- isHashObject(parsed) && field ? parsed[field] : oneLine(text);
66
+ const fromField = isHashObject(parsed) && field;
67
+ const message = fromField ? parsed[field] : oneLine(text);
73
68
  if (status === 404) {
74
- const hint = token
75
- ? ''
76
- : ` (set ${prefix}_TOKEN for private repositories)`;
77
- const notFound = `${name} ${this.subject} not found${hint}`;
69
+ let hint = '';
70
+ if (!token) hint = ` (set ${prefix}_TOKEN for private repositories)`;
71
+ const notFound = `${name} ${subject} not found${hint}`;
78
72
  return this.error('NOT_FOUND', notFound, { status });
79
73
  }
80
74
  if (status === 401) {
@@ -101,18 +95,22 @@ class RemoteClient {
101
95
  const init = { method: options.method || 'GET', headers };
102
96
  if (options.body !== undefined) init.body = options.body;
103
97
  if (signal) init.signal = signal;
104
- let response;
105
- try {
106
- response = await options.fetch(url, init);
107
- } catch (cause) {
108
- if (isAbortError(cause, signal)) throw cause;
109
- const detail = oneLine(cause && cause.message);
110
- const suffix = detail ? `: ${detail}` : '';
111
- const message = `${this.name} request failed${suffix}`;
112
- throw this.error('NETWORK', message, { cause });
113
- }
98
+ const load = async () => {
99
+ try {
100
+ return await options.fetch(url, init);
101
+ } catch (cause) {
102
+ if (isAbortError(cause, signal)) throw cause;
103
+ const detail = oneLine(cause && cause.message);
104
+ const suffix = detail ? `: ${detail}` : '';
105
+ const message = `${this.name} request failed${suffix}`;
106
+ throw this.error('NETWORK', message, { cause });
107
+ }
108
+ };
109
+ const response = await load();
114
110
  const text = await response.text();
115
- if (!response.ok) throw this.httpError(response.status, text, token);
111
+ if (!response.ok) {
112
+ throw this.httpError(response.status, text, token, options.subject);
113
+ }
116
114
  return { body: text, headers: response.headers };
117
115
  }
118
116
 
@@ -183,7 +181,14 @@ const listUrl = (base) => {
183
181
  return url.toString();
184
182
  };
185
183
 
186
- const commentBody = (value) => `${value ?? ''}`.trim();
184
+ const commentBody = (value) => trimText(value);
185
+
186
+ const issueText = (title, body) => {
187
+ const head = trimText(title);
188
+ const rest = commentBody(body);
189
+ if (head && rest) return `${head}\n\n${rest}`;
190
+ return head || rest;
191
+ };
187
192
 
188
193
  const sideLineNumber = (type, oldNo, newNo, side) => {
189
194
  if (side === 'LEFT') return type === 'add' ? 0 : oldNo;
@@ -228,6 +233,21 @@ const importedTodo = (file, entry, host) => ({
228
233
  done: entry.resolved === true,
229
234
  });
230
235
 
236
+ const issueTodoText = (entry, host) => {
237
+ const reviewer = entry.reviewer || 'unknown';
238
+ const body = `@${reviewer} at ${host}: ${commentBody(entry.body)}`;
239
+ const url = trimText(entry.url);
240
+ if (!url) return body;
241
+ return `${body}\nIssue: ${url}`;
242
+ };
243
+
244
+ const issueTodo = (entry, host) => ({
245
+ kind: 'todo',
246
+ file: ISSUE_FILE,
247
+ text: issueTodoText(entry, host),
248
+ done: entry.resolved === true,
249
+ });
250
+
231
251
  const importedFeedback = (item, path, entry, host) => {
232
252
  const hunk = item.hunk;
233
253
  return {
@@ -281,17 +301,100 @@ const changeFiles = (items) => {
281
301
  return [...rels];
282
302
  };
283
303
 
304
+ const requestOptions = (options) => ({
305
+ fetch: options.fetch ?? globalThis.fetch,
306
+ token: options.token ?? '',
307
+ retry: options.retry ?? {},
308
+ signal: options.signal,
309
+ });
310
+
311
+ const stateIs = (state, name) => `${state ?? ''}`.toLowerCase() === name;
312
+
313
+ const issueChange = (site, issue) => {
314
+ const number = issue.number || 0;
315
+ const tail = number ? `/${number}` : '';
316
+ return {
317
+ id: `${site.id}${tail}`,
318
+ source: 'issue',
319
+ title: issue.title || '',
320
+ author: issue.author || '',
321
+ base: '',
322
+ head: '',
323
+ url: issue.url || `${site.page}${tail}`,
324
+ repository: site.repository,
325
+ number,
326
+ };
327
+ };
328
+
329
+ const issueTasks = (source, issue, url, comments) => {
330
+ const { host, readComment } = source;
331
+ const tasks = [];
332
+ const body = issueText(issue.title, issue.body);
333
+ if (body) {
334
+ const resolved = stateIs(issue.state, 'closed');
335
+ const entry = { reviewer: issue.author, body, resolved, url };
336
+ tasks.push(issueTodo(entry, host));
337
+ }
338
+ for (const comment of comments) {
339
+ const entry = readComment(comment);
340
+ if (!entry || !commentBody(entry.body)) continue;
341
+ tasks.push(issueTodo(entry, host));
342
+ }
343
+ return tasks;
344
+ };
345
+
346
+ const loadIssue = async (ref, options, source) => {
347
+ const { client, site, issuesUrl, readIssue } = source;
348
+ const request = { ...requestOptions(options), subject: 'issue' };
349
+ const readComments = (number) => {
350
+ const url = `${issuesUrl}/${number}/${source.comments}`;
351
+ return client.list(listUrl(url), request);
352
+ };
353
+ const tasks = [];
354
+ let change = issueChange(site, {});
355
+ if (ref.list) {
356
+ const open = listUrl(`${issuesUrl}?state=${source.openState}`);
357
+ for (const data of await client.list(open, request)) {
358
+ if (!source.isOpen(data)) continue;
359
+ const issue = readIssue(data);
360
+ const quiet = issue.comments === 0;
361
+ const comments = quiet ? [] : await readComments(issue.number);
362
+ const { url } = issueChange(site, issue);
363
+ tasks.push(...issueTasks(source, issue, url, comments));
364
+ }
365
+ } else {
366
+ const [page, comments] = await Promise.all([
367
+ client.request(`${issuesUrl}/${ref.number}`, request),
368
+ readComments(ref.number),
369
+ ]);
370
+ const data = client.parseJson(page.body, 'issue payload');
371
+ const issue = readIssue(data);
372
+ issue.number ||= ref.number;
373
+ change = issueChange(site, issue);
374
+ tasks.push(...issueTasks(source, issue, change.url, comments));
375
+ }
376
+ return {
377
+ top: options.cwd,
378
+ items: [],
379
+ sourceLabel: ref.list ? site.repository : `#${change.number}`,
380
+ change,
381
+ imported: { feedback: [], tasks },
382
+ };
383
+ };
384
+
284
385
  const loadChange = async (ref, options, source) => {
285
386
  const { client, url, diffUrl = url, diffAccept } = source;
286
- const { cwd, paths = [], fetch = globalThis.fetch } = options;
287
- const { token = '', retry = {}, signal } = options;
288
- const request = { fetch, token, retry, signal };
289
- const jsonPage = await client.request(url, request);
387
+ const { cwd, paths = [], signal } = options;
388
+ const request = requestOptions(options);
389
+ const discussion = Promise.resolve().then(() =>
390
+ source.loadDiscussion(ref, request),
391
+ );
392
+ discussion.catch(() => {});
393
+ const [jsonPage, diffPage] = await Promise.all([
394
+ client.request(url, request),
395
+ client.request(diffUrl, { ...request, accept: diffAccept }),
396
+ ]);
290
397
  const data = client.parseJson(jsonPage.body, `${client.subject} payload`);
291
- const diffPage = await client.request(diffUrl, {
292
- ...request,
293
- accept: diffAccept,
294
- });
295
398
  const files = filterChangeFiles(parseDiff(diffPage.body), paths);
296
399
  const items = foldDepItems(itemsFromFiles(files, 'pr'));
297
400
  const change = {
@@ -302,8 +405,7 @@ const loadChange = async (ref, options, source) => {
302
405
  const sourceLabel = `${source.prefix}${change.number}`;
303
406
  let imported = { feedback: [], tasks: [] };
304
407
  try {
305
- const discussion = await source.loadDiscussion(ref, request);
306
- imported = source.discussionToNotes(discussion, items, paths);
408
+ imported = source.discussionToNotes(await discussion, items, paths);
307
409
  } catch (error) {
308
410
  if (isAbortError(error, signal)) throw error;
309
411
  }
@@ -320,6 +422,8 @@ module.exports = {
320
422
  importedTodo,
321
423
  noteFromLocation,
322
424
  splitNotes,
425
+ stateIs,
323
426
  filterChangeFiles,
427
+ loadIssue,
324
428
  loadChange,
325
429
  };
@@ -0,0 +1,20 @@
1
+ query ($owner: String!, $name: String!, $number: Int!, $after: String) {
2
+ repository(owner: $owner, name: $name) {
3
+ pullRequest(number: $number) {
4
+ reviewThreads(first: 100, after: $after) {
5
+ pageInfo {
6
+ hasNextPage
7
+ endCursor
8
+ }
9
+ nodes {
10
+ isResolved
11
+ comments(first: 100) {
12
+ nodes {
13
+ databaseId
14
+ }
15
+ }
16
+ }
17
+ }
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const github = require('./github.js');
4
+ const gitlab = require('./gitlab.js');
5
+ const { capabilitiesFor, attachCapabilities } = require('./capabilities.js');
6
+ const { parseGithubPrUrl, parseGithubIssueUrl } = github;
7
+ const { parseGithubIssueListUrl } = github;
8
+ const { parseGitlabMrUrl, parseGitlabIssueUrl } = gitlab;
9
+ const { parseGitlabIssueListUrl } = gitlab;
10
+
11
+ const REMOTE_KINDS = {
12
+ pr: {
13
+ option: 'loadPullRequest',
14
+ load: github.loadPullRequest,
15
+ token: github.githubToken,
16
+ target: (selected) => selected.pr,
17
+ },
18
+ mr: {
19
+ option: 'loadMergeRequest',
20
+ load: gitlab.loadMergeRequest,
21
+ token: gitlab.gitlabToken,
22
+ target: (selected) => selected.mr,
23
+ },
24
+ issue: {
25
+ option: 'loadGithubIssue',
26
+ load: github.loadGithubIssue,
27
+ token: github.githubToken,
28
+ target: (selected) => selected.issue,
29
+ },
30
+ 'gl-issue': {
31
+ option: 'loadGitlabIssue',
32
+ load: gitlab.loadGitlabIssue,
33
+ token: gitlab.gitlabToken,
34
+ target: (selected) => selected.issue,
35
+ },
36
+ };
37
+
38
+ const remoteKind = (selected) => REMOTE_KINDS[selected.kind] ?? null;
39
+
40
+ const selectChangeSource = (paths) => {
41
+ if (!paths.length) return { kind: 'local', paths };
42
+ const first = paths[0];
43
+ const rest = paths.slice(1);
44
+ const pr = parseGithubPrUrl(first);
45
+ if (pr) return { kind: 'pr', pr, paths: rest };
46
+ const githubIssue = parseGithubIssueUrl(first);
47
+ if (githubIssue) return { kind: 'issue', issue: githubIssue, paths: rest };
48
+ const githubIssues = parseGithubIssueListUrl(first);
49
+ if (githubIssues) return { kind: 'issue', issue: githubIssues, paths: rest };
50
+ const mr = parseGitlabMrUrl(first);
51
+ if (mr) return { kind: 'mr', mr, paths: rest };
52
+ const gitlabIssue = parseGitlabIssueUrl(first);
53
+ if (gitlabIssue) return { kind: 'gl-issue', issue: gitlabIssue, paths: rest };
54
+ const gitlabIssues = parseGitlabIssueListUrl(first);
55
+ if (gitlabIssues) {
56
+ return { kind: 'gl-issue', issue: gitlabIssues, paths: rest };
57
+ }
58
+ return { kind: 'local', paths };
59
+ };
60
+
61
+ const createLoadedSource = (loaded, kind = 'pr', extra = {}) => {
62
+ const load = async () => {
63
+ const items = loaded.items ?? [];
64
+ return { ...loaded, items: [...items] };
65
+ };
66
+ const impl = { load: extra.load ?? load, resolveRev: () => null };
67
+ return attachCapabilities(impl, capabilitiesFor(kind));
68
+ };
69
+
70
+ module.exports = {
71
+ remoteKind,
72
+ selectChangeSource,
73
+ createLoadedSource,
74
+ };