@osdlc/typed-gh 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -1
- package/CONTRIBUTING.md +4 -3
- package/README.md +247 -56
- package/dist/async.d.ts +70 -28
- package/dist/async.js +127 -44
- package/dist/doors.d.ts +82 -0
- package/dist/doors.js +183 -0
- package/dist/failure.d.ts +59 -0
- package/dist/failure.js +93 -0
- package/dist/index.d.ts +12 -8
- package/dist/index.js +9 -4
- package/dist/parse.d.ts +5 -2
- package/dist/parse.js +48 -0
- package/dist/repo.d.ts +42 -11
- package/dist/repo.js +76 -12
- package/dist/sync.d.ts +104 -28
- package/dist/sync.js +155 -46
- package/dist/types.d.ts +37 -1
- package/dist/verbs.d.ts +213 -45
- package/dist/verbs.js +504 -86
- package/package.json +13 -17
- package/async.ts +0 -285
- package/dist/async.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/parse.d.ts.map +0 -1
- package/dist/repo.d.ts.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/verbs.d.ts.map +0 -1
- package/index.ts +0 -97
- package/parse.ts +0 -252
- package/repo.ts +0 -59
- package/sync.ts +0 -326
- package/types.ts +0 -186
- package/verbs.ts +0 -799
package/dist/verbs.js
CHANGED
|
@@ -7,12 +7,66 @@
|
|
|
7
7
|
* asynchronous fan-out drive the same definitions without either duplicating
|
|
8
8
|
* the other's parsing.
|
|
9
9
|
*
|
|
10
|
-
* `checked` marks a verb
|
|
11
|
-
* and `prDiff`, which is a read. Other queries are
|
|
12
|
-
* into
|
|
10
|
+
* `checked` marks a verb the runner throws for on a non-zero exit, with
|
|
11
|
+
* stderr: every mutation, and `prDiff`, which is a read. Other queries are run
|
|
12
|
+
* unchecked and `parse` folds a failure into `null`, which is the verb's
|
|
13
|
+
* *soft* reading. The clients' plain door reads them through `settle` (see
|
|
14
|
+
* `doors.ts`) instead: a failure throws, except where the verb declares
|
|
15
|
+
* `absent` or is a `probe`.
|
|
13
16
|
*/
|
|
14
17
|
import { stderrOrError } from '@osdlc/command-seam';
|
|
15
|
-
import { parseLabels, parsePullRequest } from "./parse.js";
|
|
18
|
+
import { parseLabels, parsePrSearchHit, parsePullRequest, parseRepository } from "./parse.js";
|
|
19
|
+
import { isAbsent } from "./failure.js";
|
|
20
|
+
import { isRepoSlug, parsePrUrl } from "./repo.js";
|
|
21
|
+
/** Whether a failed run says GitHub has no such `thing`: read from the same text the classifier reads. */
|
|
22
|
+
function noSuch(thing) {
|
|
23
|
+
return (r) => isAbsent(thing, `${r.stderr}\n${r.stdout}`);
|
|
24
|
+
}
|
|
25
|
+
const HOSTNAME = /^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::\d+)?$/;
|
|
26
|
+
/**
|
|
27
|
+
* Throw `RangeError` unless `target.repo` is `owner/name` and
|
|
28
|
+
* `target.hostname` is a bare host. Run by every verb that uses a target, so
|
|
29
|
+
* no text can reach gh as a flag or change the endpoint a call hits.
|
|
30
|
+
*/
|
|
31
|
+
export function checkTarget(target) {
|
|
32
|
+
if (target?.repo !== undefined && !isRepoSlug(target.repo)) {
|
|
33
|
+
throw new RangeError(`repo must be owner/name, got ${JSON.stringify(target.repo)}`);
|
|
34
|
+
}
|
|
35
|
+
if (target?.hostname !== undefined && !HOSTNAME.test(target.hostname)) {
|
|
36
|
+
throw new RangeError(`hostname must be a bare host, got ${JSON.stringify(target.hostname)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** `--repo [host/]owner/name` for `pr`, `issue` and `label` verbs; `repo` overrides the target's. */
|
|
40
|
+
function repoArgs(target, repo) {
|
|
41
|
+
checkTarget(target);
|
|
42
|
+
const slug = repo ?? target?.repo;
|
|
43
|
+
if (slug === undefined)
|
|
44
|
+
return [];
|
|
45
|
+
if (repo !== undefined)
|
|
46
|
+
checkTarget({ repo });
|
|
47
|
+
return ['--repo', target?.hostname === undefined ? slug : `${target.hostname}/${slug}`];
|
|
48
|
+
}
|
|
49
|
+
/** `--hostname <host>` for `api` and `auth` verbs. */
|
|
50
|
+
function hostArgs(target) {
|
|
51
|
+
checkTarget(target);
|
|
52
|
+
return target?.hostname === undefined ? [] : ['--hostname', target.hostname];
|
|
53
|
+
}
|
|
54
|
+
/** `GH_HOST` for the verbs gh gives no host flag. */
|
|
55
|
+
function hostEnv(target) {
|
|
56
|
+
checkTarget(target);
|
|
57
|
+
return target?.hostname === undefined ? undefined : { GH_HOST: target.hostname };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `endpoint` with `{owner}` and `{repo}` filled from the target. With no
|
|
61
|
+
* target repo they are left for gh to fill from the clone.
|
|
62
|
+
*/
|
|
63
|
+
function fillRepo(endpoint, target) {
|
|
64
|
+
checkTarget(target);
|
|
65
|
+
if (target?.repo === undefined)
|
|
66
|
+
return endpoint;
|
|
67
|
+
const [owner, name] = target.repo.split('/');
|
|
68
|
+
return endpoint.replaceAll('{owner}', owner).replaceAll('{repo}', name);
|
|
69
|
+
}
|
|
16
70
|
/**
|
|
17
71
|
* The cheapest useful set: enough to identify a PR and nothing more.
|
|
18
72
|
*
|
|
@@ -94,6 +148,18 @@ function manyOrNull(r) {
|
|
|
94
148
|
}
|
|
95
149
|
return rows;
|
|
96
150
|
}
|
|
151
|
+
/** Throw `RangeError` unless `limit` is a positive integer. */
|
|
152
|
+
function checkLimit(limit) {
|
|
153
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
154
|
+
throw new RangeError(`limit must be a positive integer, got ${limit}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** `rows` cut to `limit`, with whether there were more. Null stays null. */
|
|
158
|
+
function limited(rows, limit) {
|
|
159
|
+
if (rows === null)
|
|
160
|
+
return null;
|
|
161
|
+
return { rows: rows.slice(0, limit), truncated: rows.length > limit };
|
|
162
|
+
}
|
|
97
163
|
/** A mutation's result carries no value. */
|
|
98
164
|
function nothing() { }
|
|
99
165
|
/**
|
|
@@ -172,43 +238,89 @@ function manyIssuesOrNull(r) {
|
|
|
172
238
|
}
|
|
173
239
|
return rows;
|
|
174
240
|
}
|
|
175
|
-
/** `gh pr list …` — open PRs by default. */
|
|
176
|
-
export function prList(opts = {}) {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
241
|
+
/** `gh pr list …` — open PRs by default. `opts.repo` overrides the target's repo. */
|
|
242
|
+
export function prList(opts = {}, target) {
|
|
243
|
+
const limit = opts.limit ?? 100;
|
|
244
|
+
checkLimit(limit);
|
|
245
|
+
const argv = ['pr', 'list', '--state', opts.state ?? 'open', ...repoArgs(target, opts.repo)];
|
|
180
246
|
if (opts.base !== undefined)
|
|
181
247
|
argv.push('--base', opts.base);
|
|
182
248
|
if (opts.head !== undefined)
|
|
183
|
-
argv.push('--
|
|
249
|
+
argv.push('--head', opts.head);
|
|
184
250
|
if (opts.search !== undefined)
|
|
185
251
|
argv.push('--search', opts.search);
|
|
186
252
|
if (opts.author !== undefined)
|
|
187
253
|
argv.push('--author', opts.author);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
254
|
+
// The exact-head filter below reads `headRefName`, so ask for it.
|
|
255
|
+
const fields = opts.fields ?? PR_LIST_FIELDS;
|
|
256
|
+
const asked = opts.head === undefined ? fields : withFields(fields, ['headRefName']);
|
|
257
|
+
argv.push('--json', asked.join(','));
|
|
258
|
+
argv.push('--limit', String(limit + 1));
|
|
259
|
+
const head = opts.head;
|
|
260
|
+
return {
|
|
261
|
+
argv,
|
|
262
|
+
checked: false,
|
|
263
|
+
readOnly: true,
|
|
264
|
+
parse(r) {
|
|
265
|
+
const result = limited(manyOrNull(r), limit);
|
|
266
|
+
if (result === null || head === undefined)
|
|
267
|
+
return result;
|
|
268
|
+
return { ...result, rows: result.rows.filter((pr) => pr.headRefName === head) };
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/** `fields` with each of `required` added once, ahead of the rest. */
|
|
273
|
+
function withFields(fields, required) {
|
|
274
|
+
return [...new Set([...required.filter((f) => !fields.includes(f)), ...fields])];
|
|
191
275
|
}
|
|
192
|
-
/**
|
|
193
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Throw `RangeError` when a string bound for gh's argv as a positional
|
|
278
|
+
* argument would read as a flag.
|
|
279
|
+
*/
|
|
280
|
+
function notAFlag(what, value) {
|
|
281
|
+
if (value.startsWith('-')) {
|
|
282
|
+
throw new RangeError(`${what} must not start with "-", got ${JSON.stringify(value)}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* `gh pr view <number|branch|url> --json …` — one PR, with the heavy fields
|
|
287
|
+
* by default.
|
|
288
|
+
*
|
|
289
|
+
* A branch is the PR whose head is that branch, as gh picks it. A URL names
|
|
290
|
+
* its own repository and host, so the target is not passed with one. A
|
|
291
|
+
* string that starts with `-` throws `RangeError`.
|
|
292
|
+
*/
|
|
293
|
+
export function prView(ref, fields = PR_VIEW_FIELDS, target) {
|
|
294
|
+
if (typeof ref === 'string')
|
|
295
|
+
notAFlag('prView ref', ref);
|
|
194
296
|
// `parsePullRequest` yields null without a `number`, so a field subset that
|
|
195
297
|
// omits it would read as "no such PR". Always ask for it, and each field once.
|
|
196
|
-
const asked =
|
|
298
|
+
const asked = withFields(fields, ['number']);
|
|
299
|
+
const isUrl = typeof ref === 'string' && parsePrUrl(ref) !== null;
|
|
197
300
|
return {
|
|
198
|
-
argv: [
|
|
301
|
+
argv: [
|
|
302
|
+
'pr',
|
|
303
|
+
'view',
|
|
304
|
+
String(ref),
|
|
305
|
+
...(isUrl ? [] : repoArgs(target)),
|
|
306
|
+
'--json',
|
|
307
|
+
asked.join(','),
|
|
308
|
+
],
|
|
199
309
|
checked: false,
|
|
310
|
+
readOnly: true,
|
|
311
|
+
absent: noSuch('pr'),
|
|
200
312
|
parse: oneOrNull,
|
|
201
313
|
};
|
|
202
314
|
}
|
|
203
315
|
/**
|
|
204
|
-
* Open PRs whose head
|
|
316
|
+
* Open PRs whose head is exactly `branch`.
|
|
205
317
|
*
|
|
206
318
|
* The common shape behind "which PR is this branch?", bundling the cheap
|
|
207
|
-
* field set with a small limit.
|
|
208
|
-
*
|
|
319
|
+
* field set with a small limit. More than one row comes back only when forks
|
|
320
|
+
* share the branch name.
|
|
209
321
|
*/
|
|
210
|
-
export function openPrsForBranch(branch, limit = 5) {
|
|
211
|
-
return prList({ head: branch, limit, fields: PR_REF_FIELDS });
|
|
322
|
+
export function openPrsForBranch(branch, limit = 5, target) {
|
|
323
|
+
return prList({ head: branch, limit, fields: PR_REF_FIELDS }, target);
|
|
212
324
|
}
|
|
213
325
|
/**
|
|
214
326
|
* `gh pr view <n> --json body -q .body` — just the PR body. Null when gh
|
|
@@ -216,29 +328,159 @@ export function openPrsForBranch(branch, limit = 5) {
|
|
|
216
328
|
* so every trailing `\r` and `\n` is trimmed (including any that were part of
|
|
217
329
|
* the body, such as the `\r\n` of a body saved with CRLF line endings).
|
|
218
330
|
*/
|
|
219
|
-
export function prBody(number) {
|
|
331
|
+
export function prBody(number, target) {
|
|
220
332
|
return {
|
|
221
|
-
argv: ['pr', 'view', String(number), '--json', 'body', '-q', '.body'],
|
|
333
|
+
argv: ['pr', 'view', String(number), ...repoArgs(target), '--json', 'body', '-q', '.body'],
|
|
222
334
|
checked: false,
|
|
335
|
+
readOnly: true,
|
|
223
336
|
parse: (r) => (r.exitCode === 0 ? r.stdout.replace(/[\r\n]+$/, '') : null),
|
|
224
337
|
};
|
|
225
338
|
}
|
|
226
339
|
/**
|
|
227
340
|
* `gh api [-i] [-H If-None-Match: …] <endpoint>` — a GET against the REST API. `{owner}` and
|
|
228
|
-
* `{repo}` placeholders in the endpoint are filled
|
|
341
|
+
* `{repo}` placeholders in the endpoint are filled from the target's repo, or by gh from the
|
|
342
|
+
* cwd's repo when the target names none.
|
|
229
343
|
* Unchecked: failure is a value (`ok: false`), so a caller can tell a 304
|
|
230
344
|
* from an outage.
|
|
231
345
|
*/
|
|
232
|
-
export function api(endpoint, opts = {}) {
|
|
346
|
+
export function api(endpoint, opts = {}, target) {
|
|
347
|
+
notAFlag('api endpoint', endpoint);
|
|
233
348
|
const conditional = opts.etag !== undefined && opts.etag !== null && opts.etag !== '';
|
|
234
349
|
const withHeaders = conditional || opts.includeHeaders === true;
|
|
235
|
-
const
|
|
350
|
+
const paginate = opts.paginate === true;
|
|
351
|
+
if (paginate && withHeaders) {
|
|
352
|
+
throw new RangeError('api: paginate cannot be combined with etag or includeHeaders');
|
|
353
|
+
}
|
|
354
|
+
const argv = ['api', ...hostArgs(target)];
|
|
236
355
|
if (withHeaders)
|
|
237
356
|
argv.push('-i');
|
|
238
357
|
if (conditional)
|
|
239
358
|
argv.push('-H', `If-None-Match: ${opts.etag}`);
|
|
240
|
-
|
|
241
|
-
|
|
359
|
+
if (paginate)
|
|
360
|
+
argv.push('--paginate', '--slurp');
|
|
361
|
+
argv.push(fillRepo(endpoint, target));
|
|
362
|
+
return {
|
|
363
|
+
argv,
|
|
364
|
+
checked: false,
|
|
365
|
+
readOnly: true,
|
|
366
|
+
probe: true,
|
|
367
|
+
parse(r) {
|
|
368
|
+
const response = parseApi(r, withHeaders);
|
|
369
|
+
return paginate ? { ...response, body: flattenPages(response.body) } : response;
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/** `--slurp` pages joined into one array when every page is an array. */
|
|
374
|
+
function flattenPages(body) {
|
|
375
|
+
if (!Array.isArray(body) || !body.every(Array.isArray))
|
|
376
|
+
return body;
|
|
377
|
+
return body.flat();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* `gh api graphql -f query=… [--paginate --slurp]` — a GraphQL query.
|
|
381
|
+
*
|
|
382
|
+
* Returns each page's `data`, one entry per page (one without `paginate`),
|
|
383
|
+
* or null when the call failed, the output did not parse, or any page
|
|
384
|
+
* carries `errors` or lacks `data`: a partial answer is no answer. Read one
|
|
385
|
+
* connection across the pages with {@link nodesAt}.
|
|
386
|
+
*
|
|
387
|
+
* A string variable of exactly `{owner}` or `{repo}` is filled from the
|
|
388
|
+
* target's repo, or by gh from the clone. A mutation document throws
|
|
389
|
+
* `RangeError`: this is a read, and changes go through `raw`.
|
|
390
|
+
*
|
|
391
|
+
* @experimental
|
|
392
|
+
*/
|
|
393
|
+
export function graphql(query, variables = {}, opts = {}, target) {
|
|
394
|
+
checkTarget(target);
|
|
395
|
+
const body = query.replace(/^(?:\s|#[^\n]*\n?)*/, '');
|
|
396
|
+
if (/^mutation\b/.test(body) || /^subscription\b/.test(body)) {
|
|
397
|
+
throw new RangeError('graphql: only queries are allowed; send a mutation through raw');
|
|
398
|
+
}
|
|
399
|
+
const paginate = opts.paginate === true;
|
|
400
|
+
if (paginate && !query.includes('$endCursor')) {
|
|
401
|
+
throw new RangeError('graphql: paginate needs a query that takes $endCursor');
|
|
402
|
+
}
|
|
403
|
+
const argv = ['api', 'graphql', ...hostArgs(target)];
|
|
404
|
+
if (paginate)
|
|
405
|
+
argv.push('--paginate', '--slurp');
|
|
406
|
+
const [owner, name] = target?.repo?.split('/') ?? [];
|
|
407
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
408
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
409
|
+
throw new RangeError(`graphql: bad variable name ${JSON.stringify(key)}`);
|
|
410
|
+
}
|
|
411
|
+
if (typeof value === 'string') {
|
|
412
|
+
const filled = value === '{owner}' ? owner : value === '{repo}' ? name : undefined;
|
|
413
|
+
if (filled !== undefined)
|
|
414
|
+
argv.push('-f', `${key}=${filled}`);
|
|
415
|
+
else if (value === '{owner}' || value === '{repo}')
|
|
416
|
+
argv.push('-F', `${key}=${value}`);
|
|
417
|
+
else
|
|
418
|
+
argv.push('-f', `${key}=${value}`);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
argv.push('-F', `${key}=${value === null ? 'null' : String(value)}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
argv.push('-f', `query=${query}`);
|
|
425
|
+
return {
|
|
426
|
+
argv,
|
|
427
|
+
checked: false,
|
|
428
|
+
readOnly: true,
|
|
429
|
+
parse(r) {
|
|
430
|
+
if (r.exitCode !== 0)
|
|
431
|
+
return null;
|
|
432
|
+
let data;
|
|
433
|
+
try {
|
|
434
|
+
data = JSON.parse(r.stdout);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
const pages = paginate ? data : [data];
|
|
440
|
+
if (!Array.isArray(pages))
|
|
441
|
+
return null;
|
|
442
|
+
const out = [];
|
|
443
|
+
for (const page of pages) {
|
|
444
|
+
if (page === null || typeof page !== 'object')
|
|
445
|
+
return null;
|
|
446
|
+
const rec = page;
|
|
447
|
+
if (Array.isArray(rec['errors']) && rec['errors'].length > 0)
|
|
448
|
+
return null;
|
|
449
|
+
if (rec['data'] === null || typeof rec['data'] !== 'object')
|
|
450
|
+
return null;
|
|
451
|
+
out.push(rec['data']);
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* The `nodes` of one connection, joined across {@link graphql} pages: walk
|
|
459
|
+
* `path` into each page and take its `nodes` array. Null when `pages` is
|
|
460
|
+
* null, or any page lacks the path or a `nodes` array, so a page that came
|
|
461
|
+
* back short is never read as fewer rows.
|
|
462
|
+
*
|
|
463
|
+
* @experimental
|
|
464
|
+
*/
|
|
465
|
+
export function nodesAt(pages, path) {
|
|
466
|
+
if (pages === null)
|
|
467
|
+
return null;
|
|
468
|
+
const out = [];
|
|
469
|
+
for (const page of pages) {
|
|
470
|
+
let at = page;
|
|
471
|
+
for (const key of path) {
|
|
472
|
+
if (at === null || typeof at !== 'object')
|
|
473
|
+
return null;
|
|
474
|
+
at = at[key];
|
|
475
|
+
}
|
|
476
|
+
if (at === null || typeof at !== 'object')
|
|
477
|
+
return null;
|
|
478
|
+
const nodes = at['nodes'];
|
|
479
|
+
if (!Array.isArray(nodes))
|
|
480
|
+
return null;
|
|
481
|
+
out.push(...nodes);
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
242
484
|
}
|
|
243
485
|
/** One header block off the front of `text`: the block, and what follows its blank line. */
|
|
244
486
|
function takeHeaderBlock(text) {
|
|
@@ -295,64 +537,208 @@ function parseApi(r, withHeaders) {
|
|
|
295
537
|
}
|
|
296
538
|
/** `gh --version` — the install probe. True when `gh` runs. */
|
|
297
539
|
export function installed() {
|
|
298
|
-
return {
|
|
540
|
+
return {
|
|
541
|
+
argv: ['--version'],
|
|
542
|
+
checked: false,
|
|
543
|
+
readOnly: true,
|
|
544
|
+
probe: true,
|
|
545
|
+
parse: (r) => r.exitCode === 0,
|
|
546
|
+
};
|
|
299
547
|
}
|
|
300
|
-
/** `gh auth status` — the credential probe. */
|
|
301
|
-
export function authStatus() {
|
|
548
|
+
/** `gh auth status [--hostname <host>]` — the credential probe, for one host when the target names it. */
|
|
549
|
+
export function authStatus(target) {
|
|
302
550
|
return {
|
|
303
|
-
argv: ['auth', 'status'],
|
|
551
|
+
argv: ['auth', 'status', ...hostArgs(target)],
|
|
304
552
|
checked: false,
|
|
553
|
+
readOnly: true,
|
|
554
|
+
probe: true,
|
|
305
555
|
parse: (r) => ({ ok: r.exitCode === 0, detail: stderrOrError(r) || r.stdout.trim() }),
|
|
306
556
|
};
|
|
307
557
|
}
|
|
308
558
|
/**
|
|
309
|
-
* `gh label list --json name,color,description` — every label on the repo
|
|
310
|
-
* `limit`
|
|
311
|
-
* repository with more labels than that without saying so.
|
|
559
|
+
* `gh label list --json name,color,description` — every label on the repo,
|
|
560
|
+
* up to `limit` (default 1000; `gh`'s own default is 30).
|
|
312
561
|
*/
|
|
313
|
-
export function labelList(opts = {}) {
|
|
562
|
+
export function labelList(opts = {}, target) {
|
|
563
|
+
const limit = opts.limit ?? 1000;
|
|
564
|
+
checkLimit(limit);
|
|
314
565
|
return {
|
|
315
566
|
argv: [
|
|
316
567
|
'label',
|
|
317
568
|
'list',
|
|
569
|
+
...repoArgs(target),
|
|
318
570
|
'--json',
|
|
319
571
|
'name,color,description',
|
|
320
572
|
'--limit',
|
|
321
|
-
String(
|
|
573
|
+
String(limit + 1),
|
|
322
574
|
],
|
|
323
575
|
checked: false,
|
|
324
|
-
|
|
576
|
+
readOnly: true,
|
|
577
|
+
parse: (r) => limited(manyLabelsOrNull(r), limit),
|
|
325
578
|
};
|
|
326
579
|
}
|
|
327
580
|
/** `gh issue list --json …` — open issues by default, optionally by label. */
|
|
328
|
-
export function issueList(opts = {}) {
|
|
329
|
-
const
|
|
581
|
+
export function issueList(opts = {}, target) {
|
|
582
|
+
const limit = opts.limit ?? 200;
|
|
583
|
+
checkLimit(limit);
|
|
584
|
+
const argv = ['issue', 'list', ...repoArgs(target)];
|
|
330
585
|
if (opts.label !== undefined)
|
|
331
586
|
argv.push('--label', opts.label);
|
|
332
|
-
argv.push('--json', ISSUE_FIELDS, '--limit', String(
|
|
333
|
-
return { argv, checked: false, parse: manyIssuesOrNull };
|
|
587
|
+
argv.push('--json', ISSUE_FIELDS, '--limit', String(limit + 1));
|
|
588
|
+
return { argv, checked: false, readOnly: true, parse: (r) => limited(manyIssuesOrNull(r), limit) };
|
|
334
589
|
}
|
|
335
590
|
/** `gh issue view <n> --json …` — one issue, same field set as {@link issueList}. */
|
|
336
|
-
export function issueView(number) {
|
|
591
|
+
export function issueView(number, target) {
|
|
337
592
|
return {
|
|
338
|
-
argv: ['issue', 'view', String(number), '--json', ISSUE_FIELDS],
|
|
593
|
+
argv: ['issue', 'view', String(number), ...repoArgs(target), '--json', ISSUE_FIELDS],
|
|
339
594
|
checked: false,
|
|
595
|
+
readOnly: true,
|
|
596
|
+
absent: noSuch('issue'),
|
|
340
597
|
parse: oneIssueOrNull,
|
|
341
598
|
};
|
|
342
599
|
}
|
|
600
|
+
/** The `gh repo list`/`view --json` fields a {@link Repository} reads. */
|
|
601
|
+
export const REPO_FIELDS = [
|
|
602
|
+
'nameWithOwner',
|
|
603
|
+
'description',
|
|
604
|
+
'defaultBranchRef',
|
|
605
|
+
'visibility',
|
|
606
|
+
'isArchived',
|
|
607
|
+
'url',
|
|
608
|
+
];
|
|
609
|
+
/** The `gh search prs --json` fields a {@link PrSearchHit} reads. */
|
|
610
|
+
export const SEARCH_PR_FIELDS = [
|
|
611
|
+
'number',
|
|
612
|
+
'title',
|
|
613
|
+
'state',
|
|
614
|
+
'url',
|
|
615
|
+
'isDraft',
|
|
616
|
+
'repository',
|
|
617
|
+
'author',
|
|
618
|
+
'labels',
|
|
619
|
+
'createdAt',
|
|
620
|
+
'updatedAt',
|
|
621
|
+
'closedAt',
|
|
622
|
+
];
|
|
623
|
+
/** Parse a `--json` array payload with `parse`, dropping rows it rejects. Null on failure. */
|
|
624
|
+
function rowsOrNull(r, parse) {
|
|
625
|
+
if (r.exitCode !== 0)
|
|
626
|
+
return null;
|
|
627
|
+
let data;
|
|
628
|
+
try {
|
|
629
|
+
data = JSON.parse(r.stdout || '[]');
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
if (!Array.isArray(data))
|
|
635
|
+
return null;
|
|
636
|
+
return data.map(parse).filter((row) => row !== null);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* `gh repo list <owner> --json …` — an owner's repositories. The host comes
|
|
640
|
+
* from the target (`GH_HOST`); the target's repo is not used.
|
|
641
|
+
*/
|
|
642
|
+
export function repoList(owner, opts = {}, target) {
|
|
643
|
+
notAFlag('repoList owner', owner);
|
|
644
|
+
const limit = opts.limit ?? 100;
|
|
645
|
+
checkLimit(limit);
|
|
646
|
+
const argv = ['repo', 'list', owner];
|
|
647
|
+
if (opts.archived === true)
|
|
648
|
+
argv.push('--archived');
|
|
649
|
+
if (opts.archived === false)
|
|
650
|
+
argv.push('--no-archived');
|
|
651
|
+
if (opts.visibility !== undefined)
|
|
652
|
+
argv.push('--visibility', opts.visibility);
|
|
653
|
+
argv.push('--json', withFields(opts.fields ?? REPO_FIELDS, ['nameWithOwner']).join(','));
|
|
654
|
+
argv.push('--limit', String(limit + 1));
|
|
655
|
+
return {
|
|
656
|
+
argv,
|
|
657
|
+
checked: false,
|
|
658
|
+
readOnly: true,
|
|
659
|
+
env: hostEnv(target),
|
|
660
|
+
parse: (r) => limited(rowsOrNull(r, parseRepository), limit),
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* `gh repo view [<[host/]owner/name>] --json …` — one repository: `repo`,
|
|
665
|
+
* else the target's repo, else the clone at the client's cwd.
|
|
666
|
+
*/
|
|
667
|
+
export function repoView(repo, fields = REPO_FIELDS, target) {
|
|
668
|
+
checkTarget(target);
|
|
669
|
+
if (repo !== undefined)
|
|
670
|
+
checkTarget({ repo });
|
|
671
|
+
const slug = repo ?? target?.repo;
|
|
672
|
+
const argv = ['repo', 'view'];
|
|
673
|
+
if (slug !== undefined) {
|
|
674
|
+
argv.push(target?.hostname === undefined ? slug : `${target.hostname}/${slug}`);
|
|
675
|
+
}
|
|
676
|
+
argv.push('--json', withFields(fields, ['nameWithOwner']).join(','));
|
|
677
|
+
return {
|
|
678
|
+
argv,
|
|
679
|
+
checked: false,
|
|
680
|
+
readOnly: true,
|
|
681
|
+
absent: noSuch('repo'),
|
|
682
|
+
parse(r) {
|
|
683
|
+
if (r.exitCode !== 0)
|
|
684
|
+
return null;
|
|
685
|
+
try {
|
|
686
|
+
return parseRepository(JSON.parse(r.stdout));
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* `gh search prs [flags] -- <query>` — pull requests across repositories,
|
|
696
|
+
* with GitHub's search syntax in `query` (it may start with `-`, as in
|
|
697
|
+
* `-label:bug`). The host comes from the target (`GH_HOST`); the target's
|
|
698
|
+
* repo is not used, so pass `opts.repo` or `opts.owner` to narrow.
|
|
699
|
+
*/
|
|
700
|
+
export function searchPrs(query, opts = {}, target) {
|
|
701
|
+
const limit = opts.limit ?? 100;
|
|
702
|
+
checkLimit(limit);
|
|
703
|
+
const argv = ['search', 'prs'];
|
|
704
|
+
if (opts.owner !== undefined) {
|
|
705
|
+
notAFlag('searchPrs owner', opts.owner);
|
|
706
|
+
argv.push('--owner', opts.owner);
|
|
707
|
+
}
|
|
708
|
+
if (opts.repo !== undefined) {
|
|
709
|
+
checkTarget({ repo: opts.repo });
|
|
710
|
+
argv.push('--repo', opts.repo);
|
|
711
|
+
}
|
|
712
|
+
if (opts.state === 'merged')
|
|
713
|
+
argv.push('--merged');
|
|
714
|
+
else if (opts.state !== undefined)
|
|
715
|
+
argv.push('--state', opts.state);
|
|
716
|
+
argv.push('--json', withFields(opts.fields ?? SEARCH_PR_FIELDS, ['number']).join(','));
|
|
717
|
+
argv.push('--limit', String(limit + 1));
|
|
718
|
+
if (query !== '')
|
|
719
|
+
argv.push('--', query);
|
|
720
|
+
return {
|
|
721
|
+
argv,
|
|
722
|
+
checked: false,
|
|
723
|
+
readOnly: true,
|
|
724
|
+
env: hostEnv(target),
|
|
725
|
+
parse: (r) => limited(rowsOrNull(r, parsePrSearchHit), limit),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
343
728
|
/**
|
|
344
729
|
* `gh pr create …`, returning the new PR's URL: the last line gh printed that
|
|
345
730
|
* starts with `http://` or `https://`. `''` when it printed none, as
|
|
346
731
|
* {@link prComment} does. A line after the URL, such as a tip, is skipped.
|
|
347
732
|
*/
|
|
348
|
-
export function createPr(opts) {
|
|
349
|
-
const argv = ['pr', 'create', '--head', opts.head, '--base', opts.base];
|
|
733
|
+
export function createPr(opts, target) {
|
|
734
|
+
const argv = ['pr', 'create', ...repoArgs(target), '--head', opts.head, '--base', opts.base];
|
|
350
735
|
if (opts.draft)
|
|
351
736
|
argv.push('--draft');
|
|
352
737
|
argv.push('--title', opts.title, '--body', opts.body);
|
|
353
738
|
return {
|
|
354
739
|
argv,
|
|
355
740
|
checked: true,
|
|
741
|
+
readOnly: false,
|
|
356
742
|
parse(r) {
|
|
357
743
|
const urls = r.stdout
|
|
358
744
|
.split('\n')
|
|
@@ -363,31 +749,31 @@ export function createPr(opts) {
|
|
|
363
749
|
};
|
|
364
750
|
}
|
|
365
751
|
/** `gh pr edit <n>` — retitle or rewrite the body. */
|
|
366
|
-
export function editPr(number, opts) {
|
|
367
|
-
const argv = ['pr', 'edit', String(number)];
|
|
752
|
+
export function editPr(number, opts, target) {
|
|
753
|
+
const argv = ['pr', 'edit', String(number), ...repoArgs(target)];
|
|
368
754
|
if (opts.title !== undefined)
|
|
369
755
|
argv.push('--title', opts.title);
|
|
370
756
|
if (opts.body !== undefined)
|
|
371
757
|
argv.push('--body', opts.body);
|
|
372
|
-
return { argv, checked: true, parse: nothing };
|
|
758
|
+
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
373
759
|
}
|
|
374
760
|
/** `gh pr edit <n> --body` — rewrite just the body. */
|
|
375
|
-
export function editPrBody(number, body) {
|
|
376
|
-
return editPr(number, { body });
|
|
761
|
+
export function editPrBody(number, body, target) {
|
|
762
|
+
return editPr(number, { body }, target);
|
|
377
763
|
}
|
|
378
764
|
/** `gh pr update-branch <n> [--rebase]` — bring the head up to date with its base. */
|
|
379
|
-
export function prUpdateBranch(number, style = 'merge') {
|
|
380
|
-
const argv = ['pr', 'update-branch', String(number)];
|
|
765
|
+
export function prUpdateBranch(number, style = 'merge', target) {
|
|
766
|
+
const argv = ['pr', 'update-branch', String(number), ...repoArgs(target)];
|
|
381
767
|
if (style === 'rebase')
|
|
382
768
|
argv.push('--rebase');
|
|
383
|
-
return { argv, checked: true, parse: nothing };
|
|
769
|
+
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
384
770
|
}
|
|
385
771
|
/** `gh pr ready <n> [--undo]` — mark a draft PR ready for review, or (with `draft`) convert it back to a draft. */
|
|
386
|
-
export function prSetDraft(number, draft) {
|
|
387
|
-
const argv = ['pr', 'ready', String(number)];
|
|
772
|
+
export function prSetDraft(number, draft, target) {
|
|
773
|
+
const argv = ['pr', 'ready', String(number), ...repoArgs(target)];
|
|
388
774
|
if (draft)
|
|
389
775
|
argv.push('--undo');
|
|
390
|
-
return { argv, checked: true, parse: nothing };
|
|
776
|
+
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
391
777
|
}
|
|
392
778
|
/**
|
|
393
779
|
* `gh api -X PATCH repos/{owner}/{repo}/issues/comments/<id> -f body=…` — rewrite one
|
|
@@ -403,20 +789,22 @@ export function prSetDraft(number, draft) {
|
|
|
403
789
|
*
|
|
404
790
|
* @experimental
|
|
405
791
|
*/
|
|
406
|
-
export function issueCommentEdit(commentId, body) {
|
|
792
|
+
export function issueCommentEdit(commentId, body, target) {
|
|
407
793
|
if (!/^[0-9]+$/.test(commentId)) {
|
|
408
794
|
throw new RangeError(`issueCommentEdit: commentId must be a numeric REST id, got ${JSON.stringify(commentId)}`);
|
|
409
795
|
}
|
|
410
796
|
return {
|
|
411
797
|
argv: [
|
|
412
798
|
'api',
|
|
799
|
+
...hostArgs(target),
|
|
413
800
|
'-X',
|
|
414
801
|
'PATCH',
|
|
415
|
-
`repos/{owner}/{repo}/issues/comments/${commentId}`,
|
|
802
|
+
fillRepo(`repos/{owner}/{repo}/issues/comments/${commentId}`, target),
|
|
416
803
|
'-f',
|
|
417
804
|
`body=${body}`,
|
|
418
805
|
],
|
|
419
806
|
checked: true,
|
|
807
|
+
readOnly: false,
|
|
420
808
|
parse: nothing,
|
|
421
809
|
};
|
|
422
810
|
}
|
|
@@ -427,16 +815,17 @@ export function issueCommentEdit(commentId, body) {
|
|
|
427
815
|
* merge can never ride in unreviewed. The branch is left alone (no
|
|
428
816
|
* `--delete-branch`); deleting it is the caller's job.
|
|
429
817
|
*/
|
|
430
|
-
export function prMerge(number, opts) {
|
|
818
|
+
export function prMerge(number, opts, target) {
|
|
431
819
|
const argv = [
|
|
432
820
|
'pr',
|
|
433
821
|
'merge',
|
|
434
822
|
String(number),
|
|
823
|
+
...repoArgs(target),
|
|
435
824
|
`--${opts.method}`,
|
|
436
825
|
'--match-head-commit',
|
|
437
826
|
opts.headSha,
|
|
438
827
|
];
|
|
439
|
-
return { argv, checked: true, parse: nothing };
|
|
828
|
+
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
440
829
|
}
|
|
441
830
|
const REVIEW_THREADS_QUERY = 'query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){reviewThreads(first:100){pageInfo{hasNextPage} nodes{isResolved isOutdated comments(last:1){nodes{author{login}}}}}}}}';
|
|
442
831
|
/**
|
|
@@ -446,21 +835,26 @@ const REVIEW_THREADS_QUERY = 'query($owner:String!,$name:String!,$number:Int!){r
|
|
|
446
835
|
* PR is clear to merge must read "could not tell" differently from "no
|
|
447
836
|
* threads".
|
|
448
837
|
*/
|
|
449
|
-
export function prReviewThreads(number) {
|
|
838
|
+
export function prReviewThreads(number, target) {
|
|
839
|
+
checkTarget(target);
|
|
840
|
+
// `-F` lets gh fill the placeholders from the clone; real values go as plain strings.
|
|
841
|
+
const [owner, name] = target?.repo?.split('/') ?? [];
|
|
842
|
+
const repo = owner === undefined || name === undefined
|
|
843
|
+
? ['-F', 'owner={owner}', '-F', 'name={repo}']
|
|
844
|
+
: ['-f', `owner=${owner}`, '-f', `name=${name}`];
|
|
450
845
|
return {
|
|
451
846
|
argv: [
|
|
452
847
|
'api',
|
|
453
848
|
'graphql',
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
'-F',
|
|
457
|
-
'name={repo}',
|
|
849
|
+
...hostArgs(target),
|
|
850
|
+
...repo,
|
|
458
851
|
'-F',
|
|
459
852
|
`number=${number}`,
|
|
460
853
|
'-f',
|
|
461
854
|
`query=${REVIEW_THREADS_QUERY}`,
|
|
462
855
|
],
|
|
463
856
|
checked: false,
|
|
857
|
+
readOnly: true,
|
|
464
858
|
parse(r) {
|
|
465
859
|
if (r.exitCode !== 0)
|
|
466
860
|
return null;
|
|
@@ -492,27 +886,28 @@ export function prReviewThreads(number) {
|
|
|
492
886
|
* `--force` makes it safe to repeat: when the label already exists, its color
|
|
493
887
|
* and description are set to these instead of failing.
|
|
494
888
|
*/
|
|
495
|
-
export function createLabel(opts) {
|
|
889
|
+
export function createLabel(opts, target) {
|
|
496
890
|
const argv = [
|
|
497
891
|
'label',
|
|
498
892
|
'create',
|
|
499
893
|
opts.name,
|
|
894
|
+
...repoArgs(target),
|
|
500
895
|
'--color',
|
|
501
896
|
opts.color,
|
|
502
897
|
'--description',
|
|
503
898
|
opts.description,
|
|
504
899
|
'--force',
|
|
505
900
|
];
|
|
506
|
-
return { argv, checked: true, parse: nothing };
|
|
901
|
+
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
507
902
|
}
|
|
508
903
|
/** `gh label edit <name>` — recolor or redescribe; omits a flag not given. */
|
|
509
|
-
export function editLabel(name, opts) {
|
|
510
|
-
const argv = ['label', 'edit', name];
|
|
904
|
+
export function editLabel(name, opts, target) {
|
|
905
|
+
const argv = ['label', 'edit', name, ...repoArgs(target)];
|
|
511
906
|
if (opts.color !== undefined)
|
|
512
907
|
argv.push('--color', opts.color);
|
|
513
908
|
if (opts.description !== undefined)
|
|
514
909
|
argv.push('--description', opts.description);
|
|
515
|
-
return { argv, checked: true, parse: nothing };
|
|
910
|
+
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
516
911
|
}
|
|
517
912
|
/**
|
|
518
913
|
* `gh issue edit <n> [--remove-label <current>] --add-label <new>` — swap
|
|
@@ -531,22 +926,28 @@ export function editLabel(name, opts) {
|
|
|
531
926
|
* issue carries no prior label to remove — the `--remove-label` flag is then
|
|
532
927
|
* omitted rather than passed empty.
|
|
533
928
|
*/
|
|
534
|
-
export function setIssueLabel(number, currentLabel, newLabel) {
|
|
535
|
-
const argv = ['issue', 'edit', String(number)];
|
|
929
|
+
export function setIssueLabel(number, currentLabel, newLabel, target) {
|
|
930
|
+
const argv = ['issue', 'edit', String(number), ...repoArgs(target)];
|
|
536
931
|
if (currentLabel !== null)
|
|
537
932
|
argv.push('--remove-label', currentLabel);
|
|
538
933
|
argv.push('--add-label', newLabel);
|
|
539
|
-
return { argv, checked: true, parse: nothing };
|
|
934
|
+
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
540
935
|
}
|
|
541
936
|
/** `gh pr diff <n>` — the PR's unified diff, as text. Checked: an empty diff is a value, a failed call is not. */
|
|
542
|
-
export function prDiff(number) {
|
|
543
|
-
return {
|
|
937
|
+
export function prDiff(number, target) {
|
|
938
|
+
return {
|
|
939
|
+
argv: ['pr', 'diff', String(number), ...repoArgs(target)],
|
|
940
|
+
checked: true,
|
|
941
|
+
readOnly: true,
|
|
942
|
+
parse: (r) => r.stdout,
|
|
943
|
+
};
|
|
544
944
|
}
|
|
545
945
|
/** `gh pr comment <n> --body <body>` — post a conversation comment, returning the URL gh prints ('' if none). */
|
|
546
|
-
export function prComment(number, body) {
|
|
946
|
+
export function prComment(number, body, target) {
|
|
547
947
|
return {
|
|
548
|
-
argv: ['pr', 'comment', String(number), '--body', body],
|
|
948
|
+
argv: ['pr', 'comment', String(number), ...repoArgs(target), '--body', body],
|
|
549
949
|
checked: true,
|
|
950
|
+
readOnly: false,
|
|
550
951
|
parse(r) {
|
|
551
952
|
const lines = r.stdout
|
|
552
953
|
.split('\n')
|
|
@@ -562,12 +963,12 @@ export function prComment(number, body) {
|
|
|
562
963
|
* when GitHub's reply carried none). The event is always `COMMENT`: this
|
|
563
964
|
* library does not offer a way to approve or request changes. Goes through
|
|
564
965
|
* `gh api` with the payload on stdin, because `gh pr review` cannot attach
|
|
565
|
-
* line-anchored comments; `{owner}`/`{repo}`
|
|
566
|
-
* client's working directory.
|
|
966
|
+
* line-anchored comments; `{owner}`/`{repo}` come from the target's repo, or
|
|
967
|
+
* are filled in by `gh` from the client's working directory.
|
|
567
968
|
*
|
|
568
969
|
* @experimental
|
|
569
970
|
*/
|
|
570
|
-
export function createReview(number, opts) {
|
|
971
|
+
export function createReview(number, opts, target) {
|
|
571
972
|
const payload = {
|
|
572
973
|
commit_id: opts.commitId,
|
|
573
974
|
body: opts.body,
|
|
@@ -582,13 +983,15 @@ export function createReview(number, opts) {
|
|
|
582
983
|
return {
|
|
583
984
|
argv: [
|
|
584
985
|
'api',
|
|
986
|
+
...hostArgs(target),
|
|
585
987
|
'--method',
|
|
586
988
|
'POST',
|
|
587
|
-
`repos/{owner}/{repo}/pulls/${number}/reviews`,
|
|
989
|
+
fillRepo(`repos/{owner}/{repo}/pulls/${number}/reviews`, target),
|
|
588
990
|
'--input',
|
|
589
991
|
'-',
|
|
590
992
|
],
|
|
591
993
|
checked: true,
|
|
994
|
+
readOnly: false,
|
|
592
995
|
input: JSON.stringify(payload),
|
|
593
996
|
parse(r) {
|
|
594
997
|
try {
|
|
@@ -603,16 +1006,31 @@ export function createReview(number, opts) {
|
|
|
603
1006
|
}
|
|
604
1007
|
// ── helpers over the parsed rows ────────────────────────────────────────────
|
|
605
1008
|
/**
|
|
606
|
-
* The row whose `headRefName` exactly equals `branch`, or null.
|
|
1009
|
+
* The first row whose `headRefName` exactly equals `branch`, or null.
|
|
607
1010
|
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
1011
|
+
* `prList({ head })` already filters exactly. This is for rows from anywhere
|
|
1012
|
+
* else, such as a `search` query with gh's `head:` qualifier, which matches
|
|
1013
|
+
* `task/foo-2` for `task/foo`.
|
|
610
1014
|
*/
|
|
611
1015
|
export function pickPrForBranch(rows, branch) {
|
|
612
1016
|
if (rows === null)
|
|
613
1017
|
return null;
|
|
614
1018
|
return rows.find((r) => r.headRefName === branch) ?? null;
|
|
615
1019
|
}
|
|
1020
|
+
/**
|
|
1021
|
+
* The PR a branch names, from `prList({ head, state: 'all' })` rows: the open
|
|
1022
|
+
* one, or else the first row (gh lists newest first). Null when there are no
|
|
1023
|
+
* rows or the call failed.
|
|
1024
|
+
*/
|
|
1025
|
+
export function branchPr(result) {
|
|
1026
|
+
if (result === null)
|
|
1027
|
+
return null;
|
|
1028
|
+
return result.rows.find((r) => r.state === 'OPEN') ?? result.rows[0] ?? null;
|
|
1029
|
+
}
|
|
1030
|
+
/** The field set {@link branchPr} needs, ahead of `fields`. */
|
|
1031
|
+
export function branchPrFields(fields) {
|
|
1032
|
+
return withFields(fields, ['number', 'state', 'headRefName']);
|
|
1033
|
+
}
|
|
616
1034
|
/**
|
|
617
1035
|
* True when GitHub has not finished computing this PR's mergeability.
|
|
618
1036
|
*
|