reslop 0.2.5 → 0.2.7

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 (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
package/lib/git.js CHANGED
@@ -3,47 +3,18 @@
3
3
  const capabilities = require('./capabilities.js');
4
4
  const { capabilitiesFor, attachCapabilities } = capabilities;
5
5
  const worktree = require('./git-worktree.js');
6
- const { readSnapshot, readSnapshotAsync, toplevel, resolveRev } = worktree;
7
- const { listScopeFiles, fileText, editItem, writeFile, stagePath } = worktree;
8
- const { addItem, unstageItem, revertItem, currentBranch } = worktree;
9
6
  const branches = require('./git-branches.js');
10
7
  const gitDeps = require('./git-deps.js');
11
8
 
12
9
  const load = (cwd, paths = [], options = {}) => {
13
- const snapshot = readSnapshot(cwd, paths, options);
10
+ const snapshot = worktree.readSnapshot(cwd, paths, options);
14
11
  return gitDeps.finishLoad(snapshot, { ...options, paths });
15
12
  };
16
13
 
17
14
  const loadAsync = async (cwd, paths = [], options = {}) => {
18
- const snapshot = await readSnapshotAsync(cwd, paths, options);
19
- return gitDeps.finishLoad(snapshot, {
20
- ...options,
21
- paths,
22
- deferExtras: options.deferExtras ?? true,
23
- });
24
- };
25
-
26
- const addGitItem = (top, item) => {
27
- if (item.dep) return gitDeps.addItem(top, item);
28
- return addItem(top, item);
29
- };
30
-
31
- const addGitItemAsync = async (top, item) => {
32
- if (item.dep) return gitDeps.addItemAsync(top, item);
33
- return addItem(top, item);
34
- };
35
-
36
- const unstageGitItem = (top, item) => {
37
- if (item.dep) return gitDeps.unstageItem(top, item);
38
- return unstageItem(top, item);
39
- };
40
-
41
- const revertGitItem = (top, item) => {
42
- if (item.origin === 'commit') return;
43
- if (item.dep) {
44
- return void gitDeps.revertDepItem(top, item);
45
- }
46
- revertItem(top, item);
15
+ const snapshot = await worktree.readSnapshotAsync(cwd, paths, options);
16
+ const deferExtras = options.deferExtras ?? true;
17
+ return gitDeps.finishLoad(snapshot, { ...options, paths, deferExtras });
47
18
  };
48
19
 
49
20
  const createGitRepo = () =>
@@ -52,18 +23,18 @@ const createGitRepo = () =>
52
23
  load,
53
24
  loadAsync,
54
25
  loadExtras: gitDeps.loadExtras,
55
- toplevel,
56
- resolveRev,
57
- listFiles: listScopeFiles,
58
- fileText,
59
- add: addGitItem,
60
- addAsync: addGitItemAsync,
61
- unstage: unstageGitItem,
62
- revert: revertGitItem,
26
+ toplevel: worktree.toplevel,
27
+ resolveRev: worktree.resolveRev,
28
+ listFiles: worktree.listScopeFiles,
29
+ fileText: worktree.fileText,
30
+ add: gitDeps.addItem,
31
+ addAsync: gitDeps.addItemAsync,
32
+ unstage: gitDeps.unstageItem,
33
+ revert: gitDeps.revertItem,
63
34
  revertFile: gitDeps.revertFile,
64
- edit: editItem,
65
- writeFile,
66
- stagePath,
35
+ edit: worktree.editItem,
36
+ writeFile: worktree.writeFile,
37
+ stagePath: worktree.stagePath,
67
38
  commit: branches.commitChanges,
68
39
  commitAsync: branches.commitChangesAsync,
69
40
  hasStaged: branches.hasStaged,
@@ -71,7 +42,7 @@ const createGitRepo = () =>
71
42
  commitMessage: branches.commitMessage,
72
43
  listBranches: branches.listBranches,
73
44
  listCommits: branches.listCommits,
74
- currentBranch,
45
+ currentBranch: worktree.currentBranch,
75
46
  checkout: branches.checkoutBranch,
76
47
  checkoutAsync: branches.checkoutBranchAsync,
77
48
  createBranch: branches.createBranch,
@@ -95,13 +66,14 @@ const createGitRepo = () =>
95
66
  );
96
67
 
97
68
  module.exports = {
98
- ...worktree,
99
69
  ...branches,
70
+ currentBranch: worktree.currentBranch,
71
+ editItem: worktree.editItem,
100
72
  load,
101
73
  loadAsync,
102
74
  loadExtras: gitDeps.loadExtras,
103
- addItem: addGitItem,
104
- unstageItem: unstageGitItem,
105
- revertItem: revertGitItem,
75
+ addItem: gitDeps.addItem,
76
+ unstageItem: gitDeps.unstageItem,
77
+ revertItem: gitDeps.revertItem,
106
78
  createGitRepo,
107
79
  };
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
  };