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.
- package/CHANGELOG.md +12 -1
- package/LICENSE +1 -1
- package/README.md +11 -11
- package/bin/reslop.js +3 -8
- package/lib/ansi.js +32 -19
- package/lib/capabilities.js +8 -8
- package/lib/cli.js +40 -74
- package/lib/clipboard.js +39 -47
- package/lib/dashboard/git.js +128 -0
- package/lib/dashboard/model.js +332 -0
- package/lib/dashboard/npm.js +193 -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 +71 -89
- package/lib/git-branches.js +254 -416
- package/lib/git-deps.js +147 -272
- package/lib/git-reword-todo.js +5 -13
- package/lib/git-worktree.js +141 -212
- 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 +49 -82
- 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 +220 -333
- package/lib/render/commits.js +160 -361
- package/lib/render/dash-activity.js +212 -0
- package/lib/render/dash-blocks.js +245 -0
- package/lib/render/dash-table.js +290 -0
- package/lib/render/dashboard.js +62 -0
- package/lib/render/diff.js +123 -258
- package/lib/render/files.js +40 -85
- package/lib/render/frame.js +78 -117
- package/lib/render/notes.js +139 -210
- package/lib/render/npm.js +213 -177
- package/lib/render/present.js +4 -11
- package/lib/render/primitives.js +72 -174
- package/lib/render/render.js +5 -14
- package/lib/render/report.js +43 -0
- package/lib/render/tiles.js +260 -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 +68 -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 +61 -43
- package/lib/session/branches.js +166 -222
- package/lib/session/change-actions.js +49 -119
- package/lib/session/commit.js +274 -355
- package/lib/session/compose.js +556 -714
- package/lib/session/dashboard.js +385 -0
- package/lib/session/dispatch.js +162 -170
- package/lib/session/files-pane.js +76 -44
- package/lib/session/items.js +6 -17
- package/lib/session/lifecycle.js +393 -466
- package/lib/session/load.js +1 -14
- package/lib/session/navigation.js +2 -2
- package/lib/session/npm.js +403 -420
- package/lib/session/ops.js +5 -13
- package/lib/session/pointer.js +16 -14
- package/lib/session/progress.js +39 -30
- package/lib/session/review.js +1 -3
- package/lib/session/tasks.js +276 -235
- package/lib/session/templates.js +51 -59
- package/lib/session/terminal.js +85 -132
- package/lib/session/view.js +47 -81
- package/lib/session/watch.js +196 -212
- package/lib/session.js +81 -161
- package/lib/update.js +44 -111
- package/lib/utilities.js +46 -88
- package/lib/wrap.js +29 -55
- package/package.json +1 -1
package/lib/remote.js
CHANGED
|
@@ -15,8 +15,6 @@ const RETRY_ATTEMPTS = 3;
|
|
|
15
15
|
const RETRY_DELAY_MS = 200;
|
|
16
16
|
const TRANSIENT_STATUS = [429, 502, 503, 504];
|
|
17
17
|
|
|
18
|
-
const isRecord = isHashObject;
|
|
19
|
-
|
|
20
18
|
const abortError = (signal) => {
|
|
21
19
|
if (signal && signal.reason !== undefined) return signal.reason;
|
|
22
20
|
const error = new Error('This operation was aborted');
|
|
@@ -26,110 +24,142 @@ const abortError = (signal) => {
|
|
|
26
24
|
|
|
27
25
|
const isAbortError = (error, signal) => {
|
|
28
26
|
if (signal && signal.aborted) return true;
|
|
29
|
-
|
|
30
|
-
if (error.name === 'AbortError') return true;
|
|
31
|
-
if (error.code === 'ABORT') return true;
|
|
32
|
-
return false;
|
|
27
|
+
return error?.name === 'AbortError' || error?.code === 'ABORT';
|
|
33
28
|
};
|
|
34
29
|
|
|
35
30
|
const throwIfAborted = (signal) => {
|
|
36
31
|
if (signal && signal.aborted) throw abortError(signal);
|
|
37
32
|
};
|
|
38
33
|
|
|
39
|
-
const wait = async (ms,
|
|
34
|
+
const wait = async (ms, signal) => {
|
|
40
35
|
try {
|
|
41
|
-
await delay(ms,
|
|
36
|
+
await delay(ms, signal);
|
|
42
37
|
} catch (error) {
|
|
43
|
-
throwIfAborted(
|
|
38
|
+
throwIfAborted(signal);
|
|
44
39
|
throw error;
|
|
45
40
|
}
|
|
46
41
|
};
|
|
47
42
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
class RemoteClient {
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.name = options.name;
|
|
46
|
+
this.subject = options.subject;
|
|
47
|
+
this.prefix = options.name.toUpperCase();
|
|
48
|
+
this.headers = options.headers;
|
|
49
|
+
this.nextPage = options.nextPage;
|
|
50
|
+
this.errorFields = options.errorFields ?? ['message'];
|
|
51
|
+
this.limitStatus = options.limitStatus ?? 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
error(code, message, extra = {}) {
|
|
55
|
+
const fields = { code: `${this.prefix}_${code}`, ...extra };
|
|
56
|
+
return Object.assign(new Error(message), fields);
|
|
57
|
+
}
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
parseJson(text, label, validate = isHashObject) {
|
|
60
|
+
const data = jsonParse(text ?? '');
|
|
61
|
+
if (validate(data)) return data;
|
|
62
|
+
throw this.error('PARSE', `${this.name} API: invalid ${label}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
httpError(status, body, token) {
|
|
66
|
+
const { name, prefix } = this;
|
|
67
|
+
const text = `${body ?? ''}`.trim();
|
|
68
|
+
const parsed = jsonParse(text);
|
|
69
|
+
const fields = this.errorFields;
|
|
70
|
+
const field = fields.find((key) => typeof parsed?.[key] === 'string');
|
|
71
|
+
const message =
|
|
72
|
+
isHashObject(parsed) && field ? parsed[field] : oneLine(text);
|
|
73
|
+
if (status === 404) {
|
|
74
|
+
const hint = token
|
|
75
|
+
? ''
|
|
76
|
+
: ` (set ${prefix}_TOKEN for private repositories)`;
|
|
77
|
+
const notFound = `${name} ${this.subject} not found${hint}`;
|
|
78
|
+
return this.error('NOT_FOUND', notFound, { status });
|
|
79
|
+
}
|
|
80
|
+
if (status === 401) {
|
|
81
|
+
return this.error('AUTH', `${name} authentication failed`, { status });
|
|
82
|
+
}
|
|
83
|
+
const rateLimit = status === 403 && /rate limit/i.test(message);
|
|
84
|
+
if (rateLimit || status === this.limitStatus) {
|
|
85
|
+
const limited = `${name} API rate limit exceeded`;
|
|
86
|
+
return this.error('RATE_LIMIT', limited, { status });
|
|
87
|
+
}
|
|
88
|
+
const detail = message ? `: ${oneLine(message)}` : ` HTTP ${status}`;
|
|
89
|
+
return this.error('HTTP', `${name} API${detail}`, { status });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
isRetryable(error) {
|
|
93
|
+
if (error.code === `${this.prefix}_NETWORK`) return true;
|
|
94
|
+
return TRANSIENT_STATUS.includes(error.status);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async fetchPage(url, options) {
|
|
98
|
+
const { signal, token = '' } = options;
|
|
99
|
+
const headers = this.headers(token, options.accept);
|
|
100
|
+
if (options.contentType) headers['Content-Type'] = options.contentType;
|
|
101
|
+
const init = { method: options.method || 'GET', headers };
|
|
102
|
+
if (options.body !== undefined) init.body = options.body;
|
|
103
|
+
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
|
+
}
|
|
114
|
+
const text = await response.text();
|
|
115
|
+
if (!response.ok) throw this.httpError(response.status, text, token);
|
|
116
|
+
return { body: text, headers: response.headers };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async request(url, options) {
|
|
120
|
+
const { signal, retry = {} } = options;
|
|
121
|
+
throwIfAborted(signal);
|
|
122
|
+
if (typeof options.fetch !== 'function') {
|
|
123
|
+
throw this.error('FETCH', 'fetch is not available');
|
|
124
|
+
}
|
|
125
|
+
const attempts = retry.attempts ?? RETRY_ATTEMPTS;
|
|
126
|
+
const delayMs = retry.delayMs ?? RETRY_DELAY_MS;
|
|
127
|
+
for (let attempt = 1; ; attempt++) {
|
|
57
128
|
try {
|
|
58
|
-
return await
|
|
129
|
+
return await this.fetchPage(url, options);
|
|
59
130
|
} catch (error) {
|
|
60
131
|
if (isAbortError(error, signal)) throw error;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throwIfAborted(signal);
|
|
64
|
-
await wait(delayMs * (i + 1), signal);
|
|
132
|
+
if (attempt >= attempts || !this.isRetryable(error)) throw error;
|
|
133
|
+
await wait(delayMs * attempt, signal);
|
|
65
134
|
}
|
|
66
135
|
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const request = async (url, options = {}) => {
|
|
70
|
-
throwIfAborted(signal);
|
|
71
|
-
if (typeof fetchImpl !== 'function') {
|
|
72
|
-
throw options.missingFetch();
|
|
73
|
-
}
|
|
74
|
-
const headers = options.headers;
|
|
75
|
-
const method = options.method || 'GET';
|
|
76
|
-
const body = options.body;
|
|
77
|
-
const wrapNetwork = options.wrapNetwork;
|
|
78
|
-
const toHttpError = options.toHttpError;
|
|
79
|
-
const retryOpts = options.retry ?? {};
|
|
80
|
-
return retry(
|
|
81
|
-
async () => {
|
|
82
|
-
let response;
|
|
83
|
-
try {
|
|
84
|
-
const init = { method, headers };
|
|
85
|
-
if (body !== undefined) init.body = body;
|
|
86
|
-
if (signal) init.signal = signal;
|
|
87
|
-
response = await fetchImpl(url, init);
|
|
88
|
-
} catch (error) {
|
|
89
|
-
if (isAbortError(error, signal)) throw error;
|
|
90
|
-
throw wrapNetwork(error);
|
|
91
|
-
}
|
|
92
|
-
const text = await response.text();
|
|
93
|
-
if (!response.ok) throw toHttpError(response.status, text);
|
|
94
|
-
return { body: text, headers: response.headers };
|
|
95
|
-
},
|
|
96
|
-
{ ...retryOpts, isRetryable: options.isRetryable },
|
|
97
|
-
);
|
|
98
|
-
};
|
|
136
|
+
}
|
|
99
137
|
|
|
100
|
-
|
|
138
|
+
async list(url, options) {
|
|
101
139
|
const items = [];
|
|
102
140
|
let next = url;
|
|
103
|
-
const parsePage = options.parsePage;
|
|
104
|
-
const nextPage = options.nextPage;
|
|
105
141
|
while (next) {
|
|
106
|
-
const page = await request(next, options);
|
|
107
|
-
const entries =
|
|
108
|
-
|
|
109
|
-
next = nextPage(page.headers, next);
|
|
142
|
+
const page = await this.request(next, { ...options, method: 'GET' });
|
|
143
|
+
const entries = this.parseJson(page.body, 'list payload', Array.isArray);
|
|
144
|
+
items.push(...entries);
|
|
145
|
+
next = this.nextPage(page.headers, next);
|
|
110
146
|
}
|
|
111
147
|
return items;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return { retry, request, listPages };
|
|
115
|
-
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
116
150
|
|
|
117
151
|
const headerValue = (headers, name) => {
|
|
118
152
|
if (!headers) return '';
|
|
119
|
-
if (typeof headers.get === 'function')
|
|
120
|
-
return headers.get(name) || '';
|
|
121
|
-
}
|
|
153
|
+
if (typeof headers.get === 'function') return headers.get(name) || '';
|
|
122
154
|
const wanted = name.toLowerCase();
|
|
123
155
|
for (const key of Object.keys(headers)) {
|
|
124
|
-
|
|
125
|
-
if (key.toLowerCase() === wanted) return `${value}`;
|
|
156
|
+
if (key.toLowerCase() === wanted) return `${headers[key]}`;
|
|
126
157
|
}
|
|
127
158
|
return '';
|
|
128
159
|
};
|
|
129
160
|
|
|
130
161
|
const parseLinkNext = (headers) => {
|
|
131
162
|
const link = headerValue(headers, 'link');
|
|
132
|
-
if (!link) return '';
|
|
133
163
|
for (const part of link.split(',')) {
|
|
134
164
|
const match = /<([^>]+)>\s*;\s*rel="?next"?/i.exec(part.trim());
|
|
135
165
|
if (match) return match[1];
|
|
@@ -153,106 +183,24 @@ const listUrl = (base) => {
|
|
|
153
183
|
return url.toString();
|
|
154
184
|
};
|
|
155
185
|
|
|
156
|
-
const
|
|
157
|
-
const prefix = name.toUpperCase();
|
|
158
|
-
const error = (code, message, extra = {}) =>
|
|
159
|
-
Object.assign(new Error(message), { code: `${prefix}_${code}`, ...extra });
|
|
160
|
-
|
|
161
|
-
const parseJson = (text, label, validate = isHashObject) => {
|
|
162
|
-
const data = jsonParse(text ?? '');
|
|
163
|
-
if (!validate(data)) throw error('PARSE', `${name} API: invalid ${label}`);
|
|
164
|
-
return data;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const httpError = (status, body, token) => {
|
|
168
|
-
const text = `${body ?? ''}`.trim();
|
|
169
|
-
const parsed = jsonParse(text);
|
|
170
|
-
const fields = name === 'GitLab' ? ['message', 'error'] : ['message'];
|
|
171
|
-
const field = fields.find((key) => typeof parsed?.[key] === 'string');
|
|
172
|
-
const message =
|
|
173
|
-
isHashObject(parsed) && field ? parsed[field] : oneLine(text);
|
|
174
|
-
if (status === 404) {
|
|
175
|
-
const hint = token
|
|
176
|
-
? ''
|
|
177
|
-
: ` (set ${prefix}_TOKEN for private repositories)`;
|
|
178
|
-
return error('NOT_FOUND', `${name} ${subject} not found${hint}`, {
|
|
179
|
-
status,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
if (status === 401) {
|
|
183
|
-
return error('AUTH', `${name} authentication failed`, { status });
|
|
184
|
-
}
|
|
185
|
-
const rateLimit = status === 403 && /rate limit/i.test(message);
|
|
186
|
-
if (rateLimit || (name === 'GitLab' && status === 429)) {
|
|
187
|
-
return error('RATE_LIMIT', `${name} API rate limit exceeded`, { status });
|
|
188
|
-
}
|
|
189
|
-
const detail = message ? `: ${oneLine(message)}` : ` HTTP ${status}`;
|
|
190
|
-
return error('HTTP', `${name} API${detail}`, { status });
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const policy = (options) => ({
|
|
194
|
-
retry: options.retry ?? {},
|
|
195
|
-
isRetryable: (err) =>
|
|
196
|
-
err.code === `${prefix}_NETWORK` || TRANSIENT_STATUS.includes(err.status),
|
|
197
|
-
missingFetch: () => error('FETCH', 'fetch is not available'),
|
|
198
|
-
toHttpError: (status, body) => httpError(status, body, options.token),
|
|
199
|
-
wrapNetwork: (cause) => {
|
|
200
|
-
const detail = oneLine(cause && cause.message);
|
|
201
|
-
const suffix = detail ? `: ${detail}` : '';
|
|
202
|
-
return error('NETWORK', `${name} request failed${suffix}`, { cause });
|
|
203
|
-
},
|
|
204
|
-
method: options.method || 'GET',
|
|
205
|
-
headers: headers(options.token || '', options.accept),
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const request = (url, options) => {
|
|
209
|
-
const init = policy(options);
|
|
210
|
-
if (options.contentType) init.headers['Content-Type'] = options.contentType;
|
|
211
|
-
return createTransport(options).request(url, {
|
|
212
|
-
...init,
|
|
213
|
-
body: options.body,
|
|
214
|
-
});
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const list = (url, options) =>
|
|
218
|
-
createTransport(options).listPages(url, {
|
|
219
|
-
...policy(options),
|
|
220
|
-
method: 'GET',
|
|
221
|
-
parsePage: (text) => parseJson(text, 'list payload', Array.isArray),
|
|
222
|
-
nextPage,
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
return { parseJson, request, list, subject };
|
|
226
|
-
};
|
|
186
|
+
const commentBody = (value) => `${value ?? ''}`.trim();
|
|
227
187
|
|
|
228
188
|
const sideLineNumber = (type, oldNo, newNo, side) => {
|
|
229
|
-
if (side === 'LEFT')
|
|
230
|
-
|
|
231
|
-
return oldNo;
|
|
232
|
-
}
|
|
233
|
-
if (type === 'del') return 0;
|
|
234
|
-
return newNo;
|
|
189
|
+
if (side === 'LEFT') return type === 'add' ? 0 : oldNo;
|
|
190
|
+
return type === 'del' ? 0 : newNo;
|
|
235
191
|
};
|
|
236
192
|
|
|
237
193
|
const locateLineInItem = (item, rel, line, side) => {
|
|
238
|
-
const path = itemPath(item);
|
|
239
|
-
if (path !== rel) return null;
|
|
240
194
|
const hunk = item.hunk;
|
|
241
|
-
if (!hunk || !line) return null;
|
|
195
|
+
if (itemPath(item) !== rel || !hunk || !line) return null;
|
|
242
196
|
let oldNo = hunk.oldStart;
|
|
243
197
|
let newNo = hunk.newStart;
|
|
244
198
|
for (const entry of hunk.lines) {
|
|
245
199
|
const at = sideLineNumber(entry.type, oldNo, newNo, side);
|
|
246
|
-
if (at === line) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (isCtxType(entry.type) || entry.type === 'del') {
|
|
251
|
-
oldNo += 1;
|
|
252
|
-
}
|
|
253
|
-
if (isCtxType(entry.type) || entry.type === 'add') {
|
|
254
|
-
newNo += 1;
|
|
255
|
-
}
|
|
200
|
+
if (at === line) return { inBlock: entry.blockId === item.blockId };
|
|
201
|
+
const isCtx = isCtxType(entry.type);
|
|
202
|
+
if (isCtx || entry.type === 'del') oldNo += 1;
|
|
203
|
+
if (isCtx || entry.type === 'add') newNo += 1;
|
|
256
204
|
}
|
|
257
205
|
return null;
|
|
258
206
|
};
|
|
@@ -270,8 +218,7 @@ const findItemForLine = (items, rel, line, side) => {
|
|
|
270
218
|
|
|
271
219
|
const importedText = (entry, host) => {
|
|
272
220
|
const reviewer = entry.reviewer || 'unknown';
|
|
273
|
-
|
|
274
|
-
return `@${reviewer} review at ${host}: ${body}`;
|
|
221
|
+
return `@${reviewer} review at ${host}: ${commentBody(entry.body)}`;
|
|
275
222
|
};
|
|
276
223
|
|
|
277
224
|
const importedTodo = (file, entry, host) => ({
|
|
@@ -298,44 +245,46 @@ const importedFeedback = (item, path, entry, host) => {
|
|
|
298
245
|
|
|
299
246
|
const noteFromLocation = (items, loc, entry, host, fallbackFile) => {
|
|
300
247
|
const path = loc.path || '';
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (line && path) {
|
|
304
|
-
const item = findItemForLine(items, path, line, side);
|
|
248
|
+
if (loc.line && path) {
|
|
249
|
+
const item = findItemForLine(items, path, loc.line, loc.side);
|
|
305
250
|
if (item) return importedFeedback(item, path, entry, host);
|
|
306
251
|
}
|
|
307
|
-
|
|
308
|
-
return importedTodo(fallbackFile, entry, host);
|
|
252
|
+
return importedTodo(path || fallbackFile, entry, host);
|
|
309
253
|
};
|
|
310
254
|
|
|
311
|
-
const
|
|
255
|
+
const splitNotes = (notes) => {
|
|
256
|
+
const feedback = [];
|
|
257
|
+
const tasks = [];
|
|
258
|
+
for (const note of notes) {
|
|
259
|
+
if (!note) continue;
|
|
260
|
+
if (note.kind === 'feedback') feedback.push(note);
|
|
261
|
+
else tasks.push(note);
|
|
262
|
+
}
|
|
263
|
+
return { feedback, tasks };
|
|
264
|
+
};
|
|
312
265
|
|
|
313
266
|
const filterChangeFiles = (files, paths) => {
|
|
314
267
|
if (!paths.length) return files;
|
|
315
|
-
return files.filter((file) =>
|
|
268
|
+
return files.filter((file) =>
|
|
269
|
+
isPathInScope(file.newPath || file.oldPath || '', paths),
|
|
270
|
+
);
|
|
316
271
|
};
|
|
317
272
|
|
|
318
273
|
const changeFiles = (items) => {
|
|
319
|
-
const
|
|
274
|
+
const rels = new Set();
|
|
320
275
|
for (const item of items) {
|
|
321
276
|
const paths = item.dep ? item.dep.files : [itemPath(item)];
|
|
322
277
|
for (const rel of paths ?? []) {
|
|
323
|
-
if (rel)
|
|
278
|
+
if (rel) rels.add(rel);
|
|
324
279
|
}
|
|
325
280
|
}
|
|
326
|
-
return [...
|
|
281
|
+
return [...rels];
|
|
327
282
|
};
|
|
328
283
|
|
|
329
284
|
const loadChange = async (ref, options, source) => {
|
|
330
285
|
const { client, url, diffUrl = url, diffAccept } = source;
|
|
331
|
-
const {
|
|
332
|
-
|
|
333
|
-
paths = [],
|
|
334
|
-
fetch = globalThis.fetch,
|
|
335
|
-
token = '',
|
|
336
|
-
retry = {},
|
|
337
|
-
signal,
|
|
338
|
-
} = options;
|
|
286
|
+
const { cwd, paths = [], fetch = globalThis.fetch } = options;
|
|
287
|
+
const { token = '', retry = {}, signal } = options;
|
|
339
288
|
const request = { fetch, token, retry, signal };
|
|
340
289
|
const jsonPage = await client.request(url, request);
|
|
341
290
|
const data = client.parseJson(jsonPage.body, `${client.subject} payload`);
|
|
@@ -345,31 +294,32 @@ const loadChange = async (ref, options, source) => {
|
|
|
345
294
|
});
|
|
346
295
|
const files = filterChangeFiles(parseDiff(diffPage.body), paths);
|
|
347
296
|
const items = foldDepItems(itemsFromFiles(files, 'pr'));
|
|
348
|
-
const change =
|
|
297
|
+
const change = {
|
|
298
|
+
...source.toChange(ref, data),
|
|
299
|
+
files: changeFiles(items),
|
|
300
|
+
status: 'reviewing',
|
|
301
|
+
};
|
|
349
302
|
const sourceLabel = `${source.prefix}${change.number}`;
|
|
350
303
|
let imported = { feedback: [], tasks: [] };
|
|
351
304
|
try {
|
|
352
305
|
const discussion = await source.loadDiscussion(ref, request);
|
|
353
306
|
imported = source.discussionToNotes(discussion, items, paths);
|
|
354
307
|
} catch (error) {
|
|
355
|
-
if (isAbortError(error,
|
|
356
|
-
// Discussion import is optional.
|
|
308
|
+
if (isAbortError(error, signal)) throw error;
|
|
357
309
|
}
|
|
358
310
|
return { top: cwd, items, sourceLabel, change, imported };
|
|
359
311
|
};
|
|
360
312
|
|
|
361
313
|
module.exports = {
|
|
362
|
-
|
|
314
|
+
RemoteClient,
|
|
363
315
|
isAbortError,
|
|
364
|
-
createTransport,
|
|
365
316
|
parseLinkNext,
|
|
366
317
|
parseNextPage,
|
|
367
|
-
createClient,
|
|
368
318
|
listUrl,
|
|
369
|
-
|
|
319
|
+
commentBody,
|
|
370
320
|
importedTodo,
|
|
371
321
|
noteFromLocation,
|
|
322
|
+
splitNotes,
|
|
372
323
|
filterChangeFiles,
|
|
373
|
-
changeFiles,
|
|
374
324
|
loadChange,
|
|
375
325
|
};
|
package/lib/render/branches.js
CHANGED
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
const ansi = require('../ansi.js');
|
|
4
4
|
const files = require('../files.js');
|
|
5
5
|
const primitives = require('./primitives.js');
|
|
6
|
+
|
|
6
7
|
const { shortAge } = files;
|
|
8
|
+
const { THEME, paint, visibleWidth, trimVisible, seq, EL } = ansi;
|
|
9
|
+
const { MARK_W, markPrefix, branchLabel, editSpan } = primitives;
|
|
10
|
+
const { paintCursorList, fieldWindow } = primitives;
|
|
7
11
|
const { measureColumns, padColumns, joinColumns, paintColumns } = primitives;
|
|
8
12
|
|
|
9
|
-
const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
|
|
10
|
-
const { FILE_MARK } = primitives;
|
|
11
|
-
const { paintCursorList, paintListEdit } = primitives;
|
|
12
|
-
const { branchLabel } = primitives;
|
|
13
|
-
|
|
14
13
|
const BRANCH_COL_ALIGN = {
|
|
15
14
|
sha: 'end',
|
|
16
15
|
ahead: 'start',
|
|
@@ -28,88 +27,91 @@ const BRANCH_COL_FG = {
|
|
|
28
27
|
};
|
|
29
28
|
|
|
30
29
|
const BRANCH_CURRENT_COL_FG = {
|
|
30
|
+
...BRANCH_COL_FG,
|
|
31
31
|
sha: THEME.shaDarkFg,
|
|
32
32
|
date: THEME.headerFg,
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
const BRANCH_NAME_MAX = 24;
|
|
36
36
|
const BRANCH_TAIL = 1;
|
|
37
|
+
|
|
37
38
|
const branchNameChip = (entry, rowBg) => {
|
|
38
39
|
if (entry.current) {
|
|
39
|
-
return {
|
|
40
|
-
nameFg: THEME.mutedFg,
|
|
41
|
-
nameBg: THEME.currentBg,
|
|
42
|
-
nameBold: false,
|
|
43
|
-
};
|
|
40
|
+
return { nameFg: THEME.mutedFg, nameBg: THEME.currentBg, nameBold: false };
|
|
44
41
|
}
|
|
45
42
|
if (entry.isDefault) {
|
|
46
|
-
return {
|
|
47
|
-
nameFg: THEME.warnFg,
|
|
48
|
-
nameBg: rowBg,
|
|
49
|
-
nameBold: true,
|
|
50
|
-
};
|
|
43
|
+
return { nameFg: THEME.warnFg, nameBg: rowBg, nameBold: true };
|
|
51
44
|
}
|
|
52
|
-
return {
|
|
53
|
-
nameFg: THEME.buttonHotFg,
|
|
54
|
-
nameBg: rowBg,
|
|
55
|
-
nameBold: false,
|
|
56
|
-
};
|
|
45
|
+
return { nameFg: THEME.buttonHotFg, nameBg: rowBg, nameBold: false };
|
|
57
46
|
};
|
|
58
47
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
48
|
+
const branchRowFields = (entry) => ({
|
|
49
|
+
sha: entry.sha ?? '',
|
|
50
|
+
ahead: entry.ahead ? `⇡${entry.ahead}` : '',
|
|
51
|
+
behind: entry.behind ? `⇣${entry.behind}` : '',
|
|
52
|
+
gone: entry.gone ? 'gone' : '',
|
|
53
|
+
date: shortAge(entry.date),
|
|
54
|
+
});
|
|
63
55
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const gone = entry.gone ? 'gone' : '';
|
|
68
|
-
const ahead = entry.ahead ? `⇡${entry.ahead}` : '';
|
|
69
|
-
const behind = entry.behind ? `⇣${entry.behind}` : '';
|
|
70
|
-
return { sha, ahead, behind, gone, date };
|
|
56
|
+
const branchRowFg = (entry, selected) => {
|
|
57
|
+
if (entry.current) return THEME.headerFg;
|
|
58
|
+
return selected ? THEME.chromeFg : THEME.mutedFg;
|
|
71
59
|
};
|
|
72
60
|
|
|
73
61
|
const branchRowBg = (entry, selected) => {
|
|
74
62
|
if (entry.current) return THEME.currentBg;
|
|
75
|
-
|
|
76
|
-
return THEME.ctxBg;
|
|
63
|
+
return selected ? THEME.buttonBg : THEME.ctxBg;
|
|
77
64
|
};
|
|
78
65
|
|
|
79
66
|
const paintBranchRow = (entry, width, color, selected, cols) => {
|
|
80
67
|
const inner = Math.max(1, width - BRANCH_TAIL);
|
|
81
|
-
const
|
|
82
|
-
const prefix = ` ${mark} `;
|
|
68
|
+
const prefix = markPrefix(selected);
|
|
83
69
|
const parts = padColumns(branchRowFields(entry), cols, BRANCH_COL_ALIGN);
|
|
84
70
|
const suffix = joinColumns(parts, cols);
|
|
85
71
|
const rest = Math.max(1, inner - visibleWidth(prefix + suffix));
|
|
86
72
|
const nameW = Math.min(cols.name, rest);
|
|
87
|
-
const label =
|
|
88
|
-
const namePad = Math.max(0, nameW - visibleWidth(label));
|
|
73
|
+
const label = trimVisible(branchLabel(entry.name), nameW);
|
|
74
|
+
const namePad = ' '.repeat(Math.max(0, nameW - visibleWidth(label)));
|
|
89
75
|
const canSubject = rest >= nameW + 3;
|
|
90
76
|
const gap = canSubject ? ' ' : '';
|
|
91
|
-
const subjectW = canSubject ? rest - nameW -
|
|
92
|
-
const subject =
|
|
93
|
-
const mid = `${label}${
|
|
94
|
-
const pad = Math.max(0, rest - visibleWidth(mid));
|
|
95
|
-
const after = `${
|
|
96
|
-
const left = `${prefix}${label}${after}`;
|
|
97
|
-
const fgRgb = branchRowFg(entry, selected);
|
|
98
|
-
const rowBg = branchRowBg(entry, selected);
|
|
99
|
-
const chip = branchNameChip(entry, rowBg);
|
|
100
|
-
const toneFg = entry.current ? THEME.headerFg : fgRgb;
|
|
77
|
+
const subjectW = canSubject ? rest - nameW - gap.length : 0;
|
|
78
|
+
const subject = trimVisible(entry.subject ?? '', subjectW);
|
|
79
|
+
const mid = `${label}${namePad}${gap}${subject}`;
|
|
80
|
+
const pad = ' '.repeat(Math.max(0, rest - visibleWidth(mid)));
|
|
81
|
+
const after = `${namePad}${gap}${subject}${pad}`;
|
|
101
82
|
const tail = ' '.repeat(BRANCH_TAIL);
|
|
102
|
-
if (!color) return `${
|
|
103
|
-
|
|
104
|
-
|
|
83
|
+
if (!color) return `${prefix}${label}${after}${suffix}${tail}`;
|
|
84
|
+
const fgRgb = branchRowFg(entry, selected);
|
|
85
|
+
const bgRgb = branchRowBg(entry, selected);
|
|
86
|
+
const chip = branchNameChip(entry, bgRgb);
|
|
87
|
+
const tones = entry.current ? BRANCH_CURRENT_COL_FG : BRANCH_COL_FG;
|
|
88
|
+
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
89
|
+
out += paint(prefix, fgRgb, bgRgb, color);
|
|
105
90
|
out += paint(label, chip.nameFg, chip.nameBg, color, chip.nameBold);
|
|
106
|
-
out += paint(after,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
91
|
+
out += paint(after, fgRgb, bgRgb, color);
|
|
92
|
+
out += paintColumns(parts, cols, fgRgb, bgRgb, color, tones);
|
|
93
|
+
return out + paint(tail, fgRgb, bgRgb, color);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const paintBranchEdit = (compose, width, color) => {
|
|
97
|
+
const prefix = markPrefix(true);
|
|
98
|
+
const rest = Math.max(1, width - MARK_W);
|
|
99
|
+
const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
|
|
100
|
+
const win = fieldWindow(text, compose.cursor, compose.scrollCol, rest);
|
|
101
|
+
const gap = ' '.repeat(Math.max(0, rest - visibleWidth(win.text)));
|
|
102
|
+
const fgRgb = THEME.chromeFg;
|
|
103
|
+
const bgRgb = THEME.buttonBg;
|
|
104
|
+
let row = `${prefix}${win.text}${gap}`;
|
|
105
|
+
if (color) {
|
|
106
|
+
row = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
107
|
+
row += paint(prefix, fgRgb, bgRgb, color);
|
|
108
|
+
row += paint(win.text, THEME.buttonHotFg, bgRgb, color);
|
|
109
|
+
row += paint(gap, fgRgb, bgRgb, color);
|
|
110
|
+
}
|
|
111
|
+
const textX = MARK_W + 1;
|
|
112
|
+
const edit = editSpan(1, textX, textX + rest, 0, text);
|
|
113
|
+
const cursor = { x: textX + win.col, scroll: win.scroll };
|
|
114
|
+
return { row, cursor, edits: [{ ...edit, scroll: win.scroll }] };
|
|
113
115
|
};
|
|
114
116
|
|
|
115
117
|
const paintBodyBranches = (view, width, color, bodyH, headerLines) => {
|
|
@@ -121,13 +123,13 @@ const paintBodyBranches = (view, width, color, bodyH, headerLines) => {
|
|
|
121
123
|
}
|
|
122
124
|
cols.name = Math.min(BRANCH_NAME_MAX, cols.name);
|
|
123
125
|
const cursor = view.branchCursor ?? 0;
|
|
124
|
-
const compose = view
|
|
125
|
-
const
|
|
126
|
-
|
|
126
|
+
const { compose } = view;
|
|
127
|
+
const edit =
|
|
128
|
+
compose?.kind === 'branch' ? paintBranchEdit(compose, width, color) : null;
|
|
127
129
|
const pane = { width, color, bodyH, headerLines, offset: view.listScroll };
|
|
128
130
|
const paintRow = (entry, selected) =>
|
|
129
131
|
paintBranchRow(entry, width, color, selected, cols);
|
|
130
|
-
return paintCursorList(branches, cursor, pane, paintRow,
|
|
132
|
+
return paintCursorList(branches, cursor, pane, paintRow, edit);
|
|
131
133
|
};
|
|
132
134
|
|
|
133
135
|
module.exports = { paintBodyBranches };
|