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.
- package/CHANGELOG.md +22 -1
- package/LICENSE +1 -1
- package/README.md +11 -11
- package/bin/reslop.js +3 -8
- package/lib/ansi.js +72 -19
- package/lib/api.js +1 -1
- package/lib/capabilities.js +8 -8
- package/lib/cli.js +61 -75
- package/lib/clipboard.js +39 -47
- package/lib/dashboard/git.js +128 -0
- package/lib/dashboard/model.js +357 -0
- package/lib/dashboard/npm.js +290 -0
- package/lib/dashboard/tree.js +278 -0
- package/lib/deps-diff.js +197 -402
- package/lib/deps.js +113 -240
- package/lib/detect.js +8 -18
- package/lib/diff/align.js +41 -113
- package/lib/diff/diff.js +12 -43
- package/lib/diff/display.js +25 -36
- package/lib/diff/edit.js +6 -20
- package/lib/diff/inline.js +76 -144
- package/lib/diff/items.js +33 -47
- package/lib/diff/parse.js +28 -56
- package/lib/diff/patch.js +12 -33
- package/lib/diff/unit.js +79 -178
- package/lib/editor.js +52 -69
- package/lib/files.js +79 -91
- package/lib/find.js +61 -0
- package/lib/git-branches.js +254 -416
- package/lib/git-deps.js +148 -272
- package/lib/git-reword-todo.js +5 -13
- package/lib/git-worktree.js +161 -214
- package/lib/git.js +21 -49
- package/lib/github.js +67 -181
- package/lib/gitlab.js +40 -90
- package/lib/highlight/bash.js +55 -177
- package/lib/highlight/core.js +155 -141
- package/lib/highlight/css.js +25 -58
- package/lib/highlight/csv.js +19 -37
- package/lib/highlight/dart.js +72 -213
- package/lib/highlight/dot.js +14 -14
- package/lib/highlight/html.js +42 -63
- package/lib/highlight/js.js +177 -506
- package/lib/highlight/json.js +17 -37
- package/lib/highlight/log.js +19 -25
- package/lib/highlight/txt.js +7 -10
- package/lib/highlight.js +17 -46
- package/lib/keys.js +75 -83
- package/lib/manifest.js +67 -126
- package/lib/npm-commands.js +120 -169
- package/lib/remote.js +140 -190
- package/lib/render/branches.js +63 -61
- package/lib/render/chrome.js +412 -347
- package/lib/render/commits.js +160 -361
- package/lib/render/dash-activity.js +212 -0
- package/lib/render/dash-blocks.js +496 -0
- package/lib/render/dash-table.js +295 -0
- package/lib/render/dashboard.js +62 -0
- package/lib/render/diff.js +123 -258
- package/lib/render/files.js +60 -86
- package/lib/render/frame.js +82 -117
- package/lib/render/notes.js +150 -210
- package/lib/render/npm.js +213 -177
- package/lib/render/packages.js +93 -0
- package/lib/render/present.js +4 -11
- package/lib/render/primitives.js +77 -172
- package/lib/render/render.js +5 -14
- package/lib/render/report.js +43 -0
- package/lib/render/repos.js +226 -0
- package/lib/render/search.js +25 -0
- package/lib/render/tiles.js +355 -0
- package/lib/render/unit.js +15 -31
- package/lib/report-model.js +110 -33
- package/lib/report-parse.js +56 -37
- package/lib/report-render.js +102 -59
- package/lib/report-run.js +87 -77
- package/lib/report-test.js +221 -99
- package/lib/report-tools.js +96 -154
- package/lib/review-merge.js +33 -41
- package/lib/review.js +69 -129
- package/lib/runs.js +225 -0
- package/lib/select.js +53 -91
- package/lib/session/accessors.js +14 -21
- package/lib/session/actions.js +189 -65
- package/lib/session/branches.js +164 -222
- package/lib/session/change-actions.js +49 -119
- package/lib/session/commit.js +298 -352
- package/lib/session/compose.js +572 -715
- package/lib/session/dashboard.js +530 -0
- package/lib/session/dispatch.js +197 -171
- package/lib/session/files-pane.js +92 -45
- package/lib/session/find.js +94 -0
- package/lib/session/items.js +6 -17
- package/lib/session/lifecycle.js +420 -466
- package/lib/session/load.js +1 -14
- package/lib/session/navigation.js +4 -2
- package/lib/session/npm.js +403 -420
- package/lib/session/ops.js +5 -13
- package/lib/session/packages.js +143 -0
- package/lib/session/pointer.js +30 -14
- package/lib/session/progress.js +39 -30
- package/lib/session/review.js +1 -3
- package/lib/session/tasks.js +277 -236
- package/lib/session/templates.js +51 -59
- package/lib/session/terminal.js +85 -132
- package/lib/session/view.js +59 -80
- package/lib/session/watch.js +201 -212
- package/lib/session/workspace.js +455 -0
- package/lib/session.js +95 -161
- package/lib/update.js +44 -111
- package/lib/utilities.js +46 -88
- package/lib/workspace.js +166 -0
- package/lib/wrap.js +29 -55
- 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
|
-
|
|
20
|
-
|
|
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:
|
|
60
|
-
addAsync:
|
|
61
|
-
unstage:
|
|
62
|
-
revert:
|
|
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:
|
|
104
|
-
unstageItem:
|
|
105
|
-
revertItem:
|
|
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 {
|
|
7
|
-
const {
|
|
8
|
-
const {
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
128
|
-
const
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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 (!
|
|
178
|
-
const 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
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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 (!
|
|
205
|
-
const body =
|
|
122
|
+
if (!isHashObject(record)) return null;
|
|
123
|
+
const body = commentBody(record.body);
|
|
206
124
|
if (!body) return null;
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
for (const comment of
|
|
286
|
-
|
|
287
|
-
|
|
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
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
const
|
|
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 {
|
|
7
|
-
const {
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
76
|
-
const
|
|
77
|
-
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 (!
|
|
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
|
|
102
|
+
return NO_LINE;
|
|
134
103
|
};
|
|
135
104
|
|
|
136
105
|
const noteToImported = (note, items, paths) => {
|
|
137
|
-
if (!
|
|
138
|
-
const 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 =
|
|
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
|
-
|
|
154
|
-
|
|
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
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
130
|
+
return splitNotes(notes);
|
|
177
131
|
};
|
|
178
132
|
|
|
179
|
-
const loadDiscussion =
|
|
180
|
-
|
|
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
|
};
|