reslop 0.2.5 → 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 (105) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -8
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +39 -47
  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 +197 -402
  14. package/lib/deps.js +113 -240
  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 +76 -144
  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 +79 -178
  25. package/lib/editor.js +52 -69
  26. package/lib/files.js +71 -89
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +147 -272
  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 +155 -141
  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 +14 -14
  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 +19 -25
  44. package/lib/highlight/txt.js +7 -10
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +49 -82
  47. package/lib/manifest.js +67 -126
  48. package/lib/npm-commands.js +120 -169
  49. package/lib/remote.js +140 -190
  50. package/lib/render/branches.js +63 -61
  51. package/lib/render/chrome.js +220 -333
  52. package/lib/render/commits.js +160 -361
  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 -258
  58. package/lib/render/files.js +40 -85
  59. package/lib/render/frame.js +78 -117
  60. package/lib/render/notes.js +139 -210
  61. package/lib/render/npm.js +213 -177
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +72 -174
  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 -31
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -37
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -77
  72. package/lib/report-test.js +221 -99
  73. package/lib/report-tools.js +96 -154
  74. package/lib/review-merge.js +33 -41
  75. package/lib/review.js +68 -129
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +53 -91
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +61 -43
  80. package/lib/session/branches.js +166 -222
  81. package/lib/session/change-actions.js +49 -119
  82. package/lib/session/commit.js +274 -355
  83. package/lib/session/compose.js +556 -714
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +162 -170
  86. package/lib/session/files-pane.js +76 -44
  87. package/lib/session/items.js +6 -17
  88. package/lib/session/lifecycle.js +393 -466
  89. package/lib/session/load.js +1 -14
  90. package/lib/session/navigation.js +2 -2
  91. package/lib/session/npm.js +403 -420
  92. package/lib/session/ops.js +5 -13
  93. package/lib/session/pointer.js +16 -14
  94. package/lib/session/progress.js +39 -30
  95. package/lib/session/review.js +1 -3
  96. package/lib/session/tasks.js +276 -235
  97. package/lib/session/templates.js +51 -59
  98. package/lib/session/terminal.js +85 -132
  99. package/lib/session/view.js +47 -81
  100. package/lib/session/watch.js +196 -212
  101. package/lib/session.js +81 -161
  102. package/lib/update.js +44 -111
  103. package/lib/utilities.js +46 -88
  104. package/lib/wrap.js +29 -55
  105. package/package.json +1 -1
package/lib/github.js CHANGED
@@ -1,12 +1,13 @@
1
1
  'use strict';
2
2
 
3
+ const metautil = require('metautil');
4
+ const { isHashObject } = metautil;
3
5
  const files = require('./files.js');
4
6
  const { isPathInScope } = files;
5
7
  const remote = require('./remote.js');
6
- const { isRecord, isAbortError } = remote;
7
- const { createClient, listUrl, parseLinkNext } = remote;
8
- const { filterChangeFiles, changeFiles, loadChange } = remote;
9
- const { importedText, importedTodo, noteFromLocation } = remote;
8
+ const { RemoteClient, isAbortError, listUrl, parseLinkNext } = remote;
9
+ const { commentBody, importedTodo, noteFromLocation } = remote;
10
+ const { splitNotes, loadChange } = remote;
10
11
 
11
12
  const GITHUB_API = 'https://api.github.com';
12
13
  const API_VERSION = '2022-11-28';
@@ -34,15 +35,12 @@ const GITHUB_PR_RE = new RegExp(
34
35
  'i',
35
36
  );
36
37
 
37
- const asRecord = (value) => (isRecord(value) ? value : {});
38
-
39
38
  const parseGithubPrUrl = (text) => {
40
39
  if (typeof text !== 'string' || !text) return null;
41
40
  const match = GITHUB_PR_RE.exec(text.trim());
42
41
  if (!match) return null;
43
42
  const owner = match[1];
44
- const rawRepo = match[2];
45
- const repo = rawRepo.replace(/\.git$/i, '');
43
+ const repo = match[2].replace(/\.git$/i, '');
46
44
  const number = parseInt(match[3], 10);
47
45
  const hasNumber = Number.isFinite(number) && number >= 1;
48
46
  if (!owner || !repo || !hasNumber) return null;
@@ -64,29 +62,12 @@ const githubHeaders = (token, accept = JSON_ACCEPT) => {
64
62
  return { ...headers, Authorization: `Bearer ${token}` };
65
63
  };
66
64
 
67
- const client = createClient({
65
+ const client = new RemoteClient({
68
66
  name: 'GitHub',
69
67
  subject: 'pull request',
70
68
  headers: githubHeaders,
71
69
  nextPage: parseLinkNext,
72
70
  });
73
- const {
74
- request: requestGithub,
75
- list: requestGithubList,
76
- parseJson: parseGithubJson,
77
- } = client;
78
-
79
- const requestGraphql = async (query, variables, options) => {
80
- const payload = JSON.stringify({ query, variables });
81
- const page = await requestGithub(GRAPHQL_URL, {
82
- ...options,
83
- method: 'POST',
84
- accept: JSON_ACCEPT,
85
- contentType: 'application/json',
86
- body: payload,
87
- });
88
- return parseGithubJson(page.body, 'GraphQL payload');
89
- };
90
71
 
91
72
  const prApiUrl = (pr) => {
92
73
  const owner = encodeURIComponent(pr.owner);
@@ -94,213 +75,120 @@ const prApiUrl = (pr) => {
94
75
  return `${GITHUB_API}/repos/${owner}/${repo}/pulls/${pr.number}`;
95
76
  };
96
77
 
97
- const discussionUrls = (pr) => {
98
- const pull = prApiUrl(pr);
99
- const issue = pull.replace('/pulls/', '/issues/');
100
- return {
101
- reviewComments: listUrl(`${pull}/comments`),
102
- reviews: listUrl(`${pull}/reviews`),
103
- issueComments: listUrl(`${issue}/comments`),
104
- };
105
- };
106
-
107
- const readLogin = (user) => {
108
- if (!isRecord(user)) return '';
109
- return user.login || '';
110
- };
111
-
112
- const readRef = (side) => {
113
- if (!isRecord(side)) return '';
114
- return side.ref || '';
115
- };
78
+ const loginOf = (user) => user?.login || '';
116
79
 
117
- const readRepoName = (data, pr) => {
118
- const base = isRecord(data.base) ? data.base : null;
119
- const repo = base && isRecord(base.repo) ? base.repo : null;
120
- const fromBase = repo && repo['full_name'];
121
- if (fromBase) return fromBase;
122
- return `${pr.owner}/${pr.repo}`;
123
- };
124
-
125
- const toChange = (pr, data, items) => {
80
+ const toChange = (pr, data) => {
126
81
  const number = data.number || pr.number;
127
- const repository = readRepoName(data, pr);
128
- const title = data.title || '';
129
- const author = readLogin(data.user);
130
- const files = changeFiles(items);
131
- const base = readRef(data.base);
132
- const head = readRef(data.head);
82
+ const fullName = data.base?.repo?.['full_name'];
83
+ const repository = fullName || `${pr.owner}/${pr.repo}`;
133
84
  const fallbackUrl = `https://github.com/${repository}/pull/${number}`;
134
- const url = data['html_url'] || fallbackUrl;
135
- const id = `github.com/${repository}/pull/${number}`;
136
85
  return {
137
- id,
86
+ id: `github.com/${repository}/pull/${number}`,
138
87
  source: 'pr',
139
- title,
140
- author,
141
- files,
142
- status: 'reviewing',
143
- base,
144
- head,
145
- url,
88
+ title: data.title || '',
89
+ author: loginOf(data.user),
90
+ base: data.base?.ref || '',
91
+ head: data.head?.ref || '',
92
+ url: data['html_url'] || fallbackUrl,
146
93
  repository,
147
94
  number,
148
95
  };
149
96
  };
150
97
 
151
- const readCommentBody = (value) => `${value ?? ''}`.trim();
152
-
153
- const reviewerOf = (record) => {
154
- const login = readLogin(record && record.user);
155
- return login || 'unknown';
156
- };
157
-
158
- const formatImportedText = (entry) => importedText(entry, IMPORT_HOST);
159
-
160
98
  const commentLine = (comment) => {
161
99
  const side = comment.side === 'LEFT' ? 'LEFT' : 'RIGHT';
162
- const raw = comment.line ?? comment['original_line'];
163
- const line = Number(raw);
100
+ const line = Number(comment.line ?? comment['original_line']);
164
101
  if (!Number.isFinite(line) || line < 1) return { side, line: 0 };
165
102
  return { side, line };
166
103
  };
167
104
 
168
- const resolvedOf = (resolvedById, id) => {
169
- const commentId = Number(id);
170
- if (!Number.isFinite(commentId) || !resolvedById.has(commentId)) {
171
- return null;
172
- }
173
- return resolvedById.get(commentId);
174
- };
175
-
176
105
  const reviewCommentToNote = (comment, items, resolvedById, paths) => {
177
- if (!isRecord(comment)) return null;
178
- const body = readCommentBody(comment.body);
106
+ if (!isHashObject(comment)) return null;
107
+ const body = commentBody(comment.body);
179
108
  if (!body) return null;
180
109
  const path = comment.path || '';
181
110
  if (path && !isPathInScope(path, paths)) return null;
182
- const { side, line } = commentLine(comment);
183
- const resolved = resolvedOf(resolvedById, comment.id);
184
- const entry = {
185
- reviewer: reviewerOf(comment),
186
- body,
187
- resolved,
188
- };
189
- const subject = comment['subject_type'] || 'line';
190
- if (subject === 'file') {
191
- if (path) return importedTodo(path, entry, IMPORT_HOST);
192
- return importedTodo(PR_TODO_FILE, entry, IMPORT_HOST);
111
+ const resolved = resolvedById.get(Number(comment.id)) ?? null;
112
+ const entry = { reviewer: loginOf(comment.user), body, resolved };
113
+ if (comment['subject_type'] === 'file') {
114
+ return importedTodo(path || PR_TODO_FILE, entry, IMPORT_HOST);
193
115
  }
194
- return noteFromLocation(
195
- items,
196
- { path, line, side },
197
- entry,
198
- IMPORT_HOST,
199
- PR_TODO_FILE,
200
- );
116
+ const { side, line } = commentLine(comment);
117
+ const loc = { path, line, side };
118
+ return noteFromLocation(items, loc, entry, IMPORT_HOST, PR_TODO_FILE);
201
119
  };
202
120
 
203
121
  const summaryToTodo = (record) => {
204
- if (!isRecord(record)) return null;
205
- const body = readCommentBody(record.body);
122
+ if (!isHashObject(record)) return null;
123
+ const body = commentBody(record.body);
206
124
  if (!body) return null;
207
- return importedTodo(
208
- PR_TODO_FILE,
209
- {
210
- reviewer: reviewerOf(record),
211
- body,
212
- },
213
- IMPORT_HOST,
214
- );
125
+ const entry = { reviewer: loginOf(record.user), body };
126
+ return importedTodo(PR_TODO_FILE, entry, IMPORT_HOST);
215
127
  };
216
128
 
217
129
  const discussionToNotes = (discussion, items, paths = []) => {
218
- const feedback = [];
219
- const tasks = [];
220
130
  const resolvedById = discussion.resolvedById ?? new Map();
221
- const reviewComments = discussion.reviewComments ?? [];
222
- for (const comment of reviewComments) {
223
- const note = reviewCommentToNote(comment, items, resolvedById, paths);
224
- if (!note) continue;
225
- if (note.kind === 'feedback') feedback.push(note);
226
- else tasks.push(note);
227
- }
131
+ const comments = discussion.reviewComments ?? [];
228
132
  const reviews = discussion.reviews ?? [];
229
- for (const review of reviews) {
230
- const task = summaryToTodo(review);
231
- if (task) tasks.push(task);
232
- }
233
133
  const issueComments = discussion.issueComments ?? [];
234
- for (const comment of issueComments) {
235
- const task = summaryToTodo(comment);
236
- if (task) tasks.push(task);
237
- }
238
- return { feedback, tasks };
134
+ return splitNotes([
135
+ ...comments.map((comment) =>
136
+ reviewCommentToNote(comment, items, resolvedById, paths),
137
+ ),
138
+ ...[...reviews, ...issueComments].map((record) => summaryToTodo(record)),
139
+ ]);
140
+ };
141
+
142
+ const requestThreads = async (variables, options) => {
143
+ const page = await client.request(GRAPHQL_URL, {
144
+ ...options,
145
+ method: 'POST',
146
+ accept: JSON_ACCEPT,
147
+ contentType: 'application/json',
148
+ body: JSON.stringify({ query: REVIEW_THREADS_QUERY, variables }),
149
+ });
150
+ const payload = client.parseJson(page.body, 'GraphQL payload');
151
+ return payload.data?.repository?.pullRequest?.reviewThreads;
239
152
  };
240
153
 
241
154
  const loadReviewThreads = async (pr, options) => {
242
155
  const threads = [];
243
156
  let after = null;
244
157
  for (;;) {
245
- const payload = await requestGraphql(
246
- REVIEW_THREADS_QUERY,
247
- {
248
- owner: pr.owner,
249
- name: pr.repo,
250
- number: pr.number,
251
- after,
252
- },
253
- options,
254
- );
255
- if (!isRecord(payload.data)) return threads;
256
- const data = payload.data;
257
- const repository = asRecord(data.repository);
258
- const pullRequest = asRecord(repository.pullRequest);
259
- const reviewThreads = asRecord(pullRequest.reviewThreads);
260
- const nodes = Array.isArray(reviewThreads.nodes) ? reviewThreads.nodes : [];
261
- for (const thread of nodes) threads.push(thread);
262
- const pageInfo = asRecord(reviewThreads.pageInfo);
263
- if (!pageInfo.hasNextPage) return threads;
264
- const cursor = pageInfo.endCursor || null;
158
+ const variables = { owner: pr.owner, name: pr.repo, number: pr.number };
159
+ const page = await requestThreads({ ...variables, after }, options);
160
+ if (Array.isArray(page?.nodes)) threads.push(...page.nodes);
161
+ const pageInfo = page?.pageInfo;
162
+ const cursor = pageInfo?.hasNextPage ? pageInfo.endCursor : null;
265
163
  if (!cursor || cursor === after) return threads;
266
164
  after = cursor;
267
165
  }
268
166
  };
269
167
 
270
- const threadComments = (thread) => {
271
- if (!isRecord(thread)) return [];
272
- const comments = thread.comments;
273
- if (isRecord(comments) && Array.isArray(comments.nodes)) {
274
- return comments.nodes;
275
- }
276
- return [];
277
- };
278
-
279
168
  const loadResolvedById = async (pr, options) => {
280
169
  const resolved = new Map();
281
170
  try {
282
- const threads = await loadReviewThreads(pr, options);
283
- for (const thread of threads) {
284
- const isResolved = thread.isResolved === true;
285
- for (const comment of threadComments(thread)) {
286
- if (!isRecord(comment)) continue;
287
- const id = Number(comment.databaseId);
288
- if (!Number.isFinite(id)) continue;
289
- resolved.set(id, isResolved);
171
+ for (const thread of await loadReviewThreads(pr, options)) {
172
+ const comments = thread?.comments?.nodes;
173
+ if (!Array.isArray(comments)) continue;
174
+ for (const comment of comments) {
175
+ const id = Number(comment?.databaseId);
176
+ if (Number.isFinite(id)) resolved.set(id, thread.isResolved === true);
290
177
  }
291
178
  }
292
179
  } catch (error) {
293
180
  if (isAbortError(error, options.signal)) throw error;
294
- // ignore: resolved state is optional
295
181
  }
296
182
  return resolved;
297
183
  };
298
184
 
299
185
  const loadDiscussion = async (pr, options) => {
300
- const urls = discussionUrls(pr);
301
- const reviewComments = await requestGithubList(urls.reviewComments, options);
302
- const reviews = await requestGithubList(urls.reviews, options);
303
- const issueComments = await requestGithubList(urls.issueComments, options);
186
+ const pull = prApiUrl(pr);
187
+ const issue = pull.replace('/pulls/', '/issues/');
188
+ 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`);
304
192
  const resolvedById = await loadResolvedById(pr, options);
305
193
  return { reviewComments, reviews, issueComments, resolvedById };
306
194
  };
@@ -319,9 +207,7 @@ const loadPullRequest = (pr, options = {}) =>
319
207
  module.exports = {
320
208
  parseGithubPrUrl,
321
209
  githubToken,
322
- filterChangeFiles,
323
210
  prApiUrl,
324
- formatImportedText,
325
211
  discussionToNotes,
326
212
  loadPullRequest,
327
213
  };
package/lib/gitlab.js CHANGED
@@ -1,18 +1,19 @@
1
1
  'use strict';
2
2
 
3
+ const metautil = require('metautil');
4
+ const { isHashObject } = metautil;
3
5
  const files = require('./files.js');
4
6
  const { isPathInScope } = files;
5
7
  const remote = require('./remote.js');
6
- const { isRecord } = remote;
7
- const { createClient, listUrl, parseNextPage } = remote;
8
- const { filterChangeFiles, changeFiles, loadChange } = remote;
9
- const { importedText, importedTodo, noteFromLocation } = remote;
8
+ const { RemoteClient, listUrl, parseNextPage, commentBody } = remote;
9
+ const { importedTodo, noteFromLocation, splitNotes, loadChange } = remote;
10
10
 
11
11
  const USER_AGENT = 'reslop';
12
12
  const JSON_ACCEPT = 'application/json';
13
13
  const DIFF_ACCEPT = 'text/plain';
14
14
  const MR_TODO_FILE = 'merge request';
15
15
  const IMPORT_HOST = 'gitlab';
16
+ const NO_LINE = { side: 'RIGHT', line: 0 };
16
17
 
17
18
  const GITLAB_MR_RE = new RegExp(
18
19
  String.raw`^(?:(https?)://)?(?:www\.)?([^/?#]+)/` +
@@ -29,13 +30,11 @@ const parseGitlabMrUrl = (text) => {
29
30
  if (!match) return null;
30
31
  const scheme = (match[1] || 'https').toLowerCase();
31
32
  const host = match[2];
32
- const rawProject = match[3];
33
- const project = rawProject.replace(/\.git$/i, '');
33
+ const project = match[3].replace(/\.git$/i, '');
34
34
  const number = parseInt(match[4], 10);
35
35
  const hasNumber = Number.isFinite(number) && number >= 1;
36
36
  if (!host || !project || !hasNumber) return null;
37
- const origin = `${scheme}://${host}`;
38
- return { host, project, number, origin };
37
+ return { host, project, number, origin: `${scheme}://${host}` };
39
38
  };
40
39
 
41
40
  const gitlabToken = (env) => {
@@ -44,84 +43,54 @@ const gitlabToken = (env) => {
44
43
  };
45
44
 
46
45
  const gitlabHeaders = (token, accept = JSON_ACCEPT) => {
47
- const headers = {
48
- Accept: accept,
49
- 'User-Agent': USER_AGENT,
50
- };
46
+ const headers = { Accept: accept, 'User-Agent': USER_AGENT };
51
47
  if (!token) return headers;
52
48
  return { ...headers, 'PRIVATE-TOKEN': token };
53
49
  };
54
50
 
55
- const client = createClient({
51
+ const client = new RemoteClient({
56
52
  name: 'GitLab',
57
53
  subject: 'merge request',
58
54
  headers: gitlabHeaders,
59
55
  nextPage: parseNextPage,
56
+ errorFields: ['message', 'error'],
57
+ limitStatus: 429,
60
58
  });
61
- const { list: requestGitlabList } = client;
62
59
 
63
60
  const mrApiUrl = (mr) => {
64
61
  const project = encodeURIComponent(mr.project);
65
- const base = `${mr.origin}/api/v4/projects/${project}`;
66
- return `${base}/merge_requests/${mr.number}`;
62
+ return `${mr.origin}/api/v4/projects/${project}/merge_requests/${mr.number}`;
67
63
  };
68
64
 
69
- const readUsername = (user) => {
70
- if (!isRecord(user)) return '';
71
- return user.username || '';
72
- };
65
+ const usernameOf = (user) => user?.username || '';
73
66
 
74
67
  const readProject = (data, mr) => {
75
- const refs = isRecord(data.references) ? data.references : null;
76
- const full = refs && refs.full;
77
- if (typeof full === 'string') {
78
- const cut = full.lastIndexOf('!');
79
- if (cut > 0) return full.slice(0, cut);
80
- }
81
- return mr.project;
68
+ const full = data.references?.full;
69
+ const cut = typeof full === 'string' ? full.lastIndexOf('!') : -1;
70
+ return cut > 0 ? full.slice(0, cut) : mr.project;
82
71
  };
83
72
 
84
- const toChange = (mr, data, items) => {
73
+ const toChange = (mr, data) => {
85
74
  const number = data.iid || mr.number;
86
75
  const repository = readProject(data, mr);
87
- const title = data.title || '';
88
- const author = readUsername(data.author);
89
- const files = changeFiles(items);
90
- const base = data.target_branch || '';
91
- const head = data.source_branch || '';
92
76
  const fallbackUrl = `${mr.origin}/${repository}/-/merge_requests/${number}`;
93
- const url = data.web_url || fallbackUrl;
94
- const id = `${mr.host}/${repository}/merge_requests/${number}`;
95
77
  return {
96
- id,
78
+ id: `${mr.host}/${repository}/merge_requests/${number}`,
97
79
  source: 'mr',
98
- title,
99
- author,
100
- files,
101
- status: 'reviewing',
102
- base,
103
- head,
104
- url,
80
+ title: data.title || '',
81
+ author: usernameOf(data.author),
82
+ base: data.target_branch || '',
83
+ head: data.source_branch || '',
84
+ url: data.web_url || fallbackUrl,
105
85
  repository,
106
86
  number,
107
87
  };
108
88
  };
109
89
 
110
- const readCommentBody = (value) => `${value ?? ''}`.trim();
111
-
112
- const reviewerOf = (record) => {
113
- const login = readUsername(record && record.author);
114
- return login || 'unknown';
115
- };
116
-
117
- const formatImportedText = (entry) => importedText(entry, IMPORT_HOST);
118
-
119
90
  const noteLine = (position) => {
120
- if (!isRecord(position)) return { side: 'RIGHT', line: 0 };
91
+ if (!position) return NO_LINE;
121
92
  const kind = position.position_type || 'text';
122
- if (kind === 'file' || kind === 'image') {
123
- return { side: 'RIGHT', line: 0 };
124
- }
93
+ if (kind === 'file' || kind === 'image') return NO_LINE;
125
94
  const newLine = Number(position.new_line);
126
95
  if (Number.isFinite(newLine) && newLine >= 1) {
127
96
  return { side: 'RIGHT', line: newLine };
@@ -130,56 +99,39 @@ const noteLine = (position) => {
130
99
  if (Number.isFinite(oldLine) && oldLine >= 1) {
131
100
  return { side: 'LEFT', line: oldLine };
132
101
  }
133
- return { side: 'RIGHT', line: 0 };
102
+ return NO_LINE;
134
103
  };
135
104
 
136
105
  const noteToImported = (note, items, paths) => {
137
- if (!isRecord(note) || note.system === true) return null;
138
- const body = readCommentBody(note.body);
106
+ if (!isHashObject(note) || note.system === true) return null;
107
+ const body = commentBody(note.body);
139
108
  if (!body) return null;
140
109
  const resolved = note.resolved === true;
141
- const entry = {
142
- reviewer: reviewerOf(note),
143
- body,
144
- resolved,
145
- };
110
+ const entry = { reviewer: usernameOf(note.author), body, resolved };
146
111
  if (note.type !== 'DiffNote') {
147
112
  return importedTodo(MR_TODO_FILE, entry, IMPORT_HOST);
148
113
  }
149
- const position = isRecord(note.position) ? note.position : null;
114
+ const position = isHashObject(note.position) ? note.position : null;
150
115
  const path = position ? position.new_path || position.old_path || '' : '';
151
116
  if (path && !isPathInScope(path, paths)) return null;
152
117
  const { side, line } = noteLine(position);
153
- return noteFromLocation(
154
- items,
155
- { path, line, side },
156
- entry,
157
- IMPORT_HOST,
158
- MR_TODO_FILE,
159
- );
118
+ const loc = { path, line, side };
119
+ return noteFromLocation(items, loc, entry, IMPORT_HOST, MR_TODO_FILE);
160
120
  };
161
121
 
162
122
  const discussionToNotes = (discussions, items, paths = []) => {
163
- const feedback = [];
164
- const tasks = [];
165
- const threads = discussions ?? [];
166
- for (const discussion of threads) {
167
- if (!isRecord(discussion)) continue;
168
- const notes = Array.isArray(discussion.notes) ? discussion.notes : [];
169
- for (const note of notes) {
170
- const mapped = noteToImported(note, items, paths);
171
- if (!mapped) continue;
172
- if (mapped.kind === 'feedback') feedback.push(mapped);
173
- else tasks.push(mapped);
123
+ const notes = [];
124
+ for (const discussion of discussions ?? []) {
125
+ if (!Array.isArray(discussion?.notes)) continue;
126
+ for (const note of discussion.notes) {
127
+ notes.push(noteToImported(note, items, paths));
174
128
  }
175
129
  }
176
- return { feedback, tasks };
130
+ return splitNotes(notes);
177
131
  };
178
132
 
179
- const loadDiscussion = async (mr, options) => {
180
- const url = listUrl(`${mrApiUrl(mr)}/discussions`);
181
- return requestGitlabList(url, options);
182
- };
133
+ const loadDiscussion = (mr, options) =>
134
+ client.list(listUrl(`${mrApiUrl(mr)}/discussions`), options);
183
135
 
184
136
  const loadMergeRequest = (mr, options = {}) => {
185
137
  const url = mrApiUrl(mr);
@@ -198,9 +150,7 @@ const loadMergeRequest = (mr, options = {}) => {
198
150
  module.exports = {
199
151
  parseGitlabMrUrl,
200
152
  gitlabToken,
201
- filterChangeFiles,
202
153
  mrApiUrl,
203
- formatImportedText,
204
154
  discussionToNotes,
205
155
  loadMergeRequest,
206
156
  };