@osdlc/typed-gh 0.9.0 → 0.9.2
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 +14 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +98 -96
- package/SECURITY.md +4 -4
- package/dist/async.d.ts +28 -27
- package/dist/async.js +29 -28
- package/dist/doors.d.ts +25 -23
- package/dist/doors.js +29 -27
- package/dist/failure.d.ts +8 -8
- package/dist/failure.js +6 -6
- package/dist/index.d.ts +13 -14
- package/dist/index.js +14 -15
- package/dist/parse.d.ts +21 -21
- package/dist/parse.js +34 -36
- package/dist/repo.d.ts +13 -13
- package/dist/repo.js +13 -13
- package/dist/run.d.ts +12 -13
- package/dist/run.js +11 -12
- package/dist/sync.d.ts +66 -64
- package/dist/sync.js +67 -65
- package/dist/types.d.ts +31 -32
- package/dist/types.js +8 -8
- package/dist/verbs.d.ts +120 -122
- package/dist/verbs.js +119 -110
- package/package.json +3 -3
package/dist/verbs.js
CHANGED
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The verb table
|
|
2
|
+
* The verb table: every `gh` call this library knows, as data.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the other's parsing.
|
|
4
|
+
* It has the same shape as `@osdlc/typed-git`'s. A {@link GhVerb} pairs the
|
|
5
|
+
* argv to run with the function that reads the result, and nothing here
|
|
6
|
+
* starts a process. So the sync client, the async client and callers with
|
|
7
|
+
* their own runner all use the same definitions and the same parsing.
|
|
9
8
|
*
|
|
10
9
|
* `checked` marks a verb the runner throws for on a non-zero exit, with
|
|
11
|
-
* stderr
|
|
12
|
-
* unchecked and `parse`
|
|
13
|
-
*
|
|
14
|
-
* `doors.ts`) instead
|
|
15
|
-
* `absent` or is a `probe`.
|
|
10
|
+
* stderr. That is every mutation, and `prDiff`, which is a read. Other
|
|
11
|
+
* queries run unchecked, and `parse` turns a failure into `null`, which is
|
|
12
|
+
* what `soft` returns. The clients' methods that throw read them through
|
|
13
|
+
* `settle` (see `doors.ts`) instead. There a failure throws, except where the
|
|
14
|
+
* verb declares `absent` or is a `probe`.
|
|
16
15
|
*/
|
|
17
16
|
import { stderrOrError } from '@osdlc/command-seam';
|
|
18
17
|
import { parseLabels, parsePrSearchHit, parsePullRequest, parseRepository } from "./parse.js";
|
|
19
18
|
import { isAbsent } from "./failure.js";
|
|
20
19
|
import { isRepoSlug, parsePrUrl } from "./repo.js";
|
|
21
|
-
/** Whether a failed run says GitHub has no such `thing
|
|
20
|
+
/** Whether a failed run says GitHub has no such `thing`. Reads the same text the classifier reads. */
|
|
22
21
|
function noSuch(thing) {
|
|
23
22
|
return (r) => isAbsent(thing, `${r.stderr}\n${r.stdout}`);
|
|
24
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* A bare host, with an optional port: letters, digits, dots and hyphens, that
|
|
26
|
+
* starts and ends with a letter or digit, then optionally `:` and digits
|
|
27
|
+
* (`github.com`, `ghe.acme.com`, `localhost:8080`). No scheme, path, spaces or
|
|
28
|
+
* leading `-`, so the value cannot be read by gh as a flag or a URL. It does
|
|
29
|
+
* not check that the host exists or that its labels are well formed, and it
|
|
30
|
+
* does not accept IPv6 literals.
|
|
31
|
+
*/
|
|
25
32
|
const HOSTNAME = /^[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?(?::\d+)?$/;
|
|
33
|
+
/** The target's host, or `undefined` when it has none. `''` is "not set". */
|
|
34
|
+
function hostOf(target) {
|
|
35
|
+
return target?.hostname === '' ? undefined : target?.hostname;
|
|
36
|
+
}
|
|
26
37
|
/**
|
|
27
38
|
* Throw `RangeError` unless `target.repo` is `owner/name` and
|
|
28
39
|
* `target.hostname` is a bare host. Run by every verb that uses a target, so
|
|
@@ -32,11 +43,12 @@ export function checkTarget(target) {
|
|
|
32
43
|
if (target?.repo !== undefined && !isRepoSlug(target.repo)) {
|
|
33
44
|
throw new RangeError(`repo must be owner/name, got ${JSON.stringify(target.repo)}`);
|
|
34
45
|
}
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
const hostname = hostOf(target);
|
|
47
|
+
if (hostname !== undefined && !HOSTNAME.test(hostname)) {
|
|
48
|
+
throw new RangeError(`hostname must be a bare host, got ${JSON.stringify(hostname)}`);
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
|
-
/** `--repo [host/]owner/name` for `pr`, `issue` and `label` verbs
|
|
51
|
+
/** `--repo [host/]owner/name` for `pr`, `issue` and `label` verbs. `repo` overrides the target's. */
|
|
40
52
|
function repoArgs(target, repo) {
|
|
41
53
|
checkTarget(target);
|
|
42
54
|
const slug = repo ?? target?.repo;
|
|
@@ -44,17 +56,20 @@ function repoArgs(target, repo) {
|
|
|
44
56
|
return [];
|
|
45
57
|
if (repo !== undefined)
|
|
46
58
|
checkTarget({ repo });
|
|
47
|
-
|
|
59
|
+
const hostname = hostOf(target);
|
|
60
|
+
return ['--repo', hostname === undefined ? slug : `${hostname}/${slug}`];
|
|
48
61
|
}
|
|
49
62
|
/** `--hostname <host>` for `api` and `auth` verbs. */
|
|
50
63
|
function hostArgs(target) {
|
|
51
64
|
checkTarget(target);
|
|
52
|
-
|
|
65
|
+
const hostname = hostOf(target);
|
|
66
|
+
return hostname === undefined ? [] : ['--hostname', hostname];
|
|
53
67
|
}
|
|
54
68
|
/** `GH_HOST` for the verbs gh gives no host flag. */
|
|
55
69
|
function hostEnv(target) {
|
|
56
70
|
checkTarget(target);
|
|
57
|
-
|
|
71
|
+
const hostname = hostOf(target);
|
|
72
|
+
return hostname === undefined ? undefined : { GH_HOST: hostname };
|
|
58
73
|
}
|
|
59
74
|
/**
|
|
60
75
|
* `endpoint` with `{owner}` and `{repo}` filled from the target. With no
|
|
@@ -68,21 +83,21 @@ function fillRepo(endpoint, target) {
|
|
|
68
83
|
return endpoint.replaceAll('{owner}', owner).replaceAll('{repo}', name);
|
|
69
84
|
}
|
|
70
85
|
/**
|
|
71
|
-
* The
|
|
86
|
+
* The smallest useful set: enough to identify a PR and nothing more.
|
|
72
87
|
*
|
|
73
|
-
*
|
|
74
|
-
* `
|
|
75
|
-
*
|
|
76
|
-
*
|
|
88
|
+
* Use it when the question is "which PR is this branch?". `mergeable` and
|
|
89
|
+
* `mergeStateStatus` are left out on purpose. GitHub computes them per PR
|
|
90
|
+
* when asked, so asking for them across many branches can hit the rate
|
|
91
|
+
* limit.
|
|
77
92
|
*/
|
|
78
93
|
export const PR_REF_FIELDS = ['number', 'state', 'headRefName', 'url'];
|
|
79
94
|
/**
|
|
80
|
-
* The
|
|
81
|
-
*
|
|
95
|
+
* The smaller field set for triage: enough to decide what to do with a PR,
|
|
96
|
+
* without its body, comments, reviews, or check rollup.
|
|
82
97
|
*
|
|
83
|
-
* `mergeable` and `mergeStateStatus` are the
|
|
84
|
-
* them
|
|
85
|
-
* PRs
|
|
98
|
+
* `mergeable` and `mergeStateStatus` are the costly pair. GitHub computes
|
|
99
|
+
* them only when asked and answers `UNKNOWN` until it has. So a first query
|
|
100
|
+
* over many PRs often needs a retry. `prViewSettled` on the clients does it.
|
|
86
101
|
*/
|
|
87
102
|
export const PR_LIST_FIELDS = [
|
|
88
103
|
'number',
|
|
@@ -103,7 +118,7 @@ export const PR_LIST_FIELDS = [
|
|
|
103
118
|
'createdAt',
|
|
104
119
|
'updatedAt',
|
|
105
120
|
];
|
|
106
|
-
/** {@link PR_LIST_FIELDS} plus the
|
|
121
|
+
/** {@link PR_LIST_FIELDS} plus the costly fields, for a single PR. */
|
|
107
122
|
export const PR_VIEW_FIELDS = [
|
|
108
123
|
...PR_LIST_FIELDS,
|
|
109
124
|
'body',
|
|
@@ -125,8 +140,8 @@ function oneOrNull(r) {
|
|
|
125
140
|
return parsePullRequest(data);
|
|
126
141
|
}
|
|
127
142
|
/**
|
|
128
|
-
* Parse a `--json` payload that is an array. Null
|
|
129
|
-
* failed
|
|
143
|
+
* Parse a `--json` payload that is an array. Null, not `[]`, when the call
|
|
144
|
+
* failed. A caller must be able to tell "no PRs" from "couldn't ask".
|
|
130
145
|
*/
|
|
131
146
|
function manyOrNull(r) {
|
|
132
147
|
if (r.exitCode !== 0)
|
|
@@ -163,10 +178,9 @@ function limited(rows, limit) {
|
|
|
163
178
|
/** A mutation's result carries no value. */
|
|
164
179
|
function nothing() { }
|
|
165
180
|
/**
|
|
166
|
-
* Parse a `gh label list --json name,color,description` payload. Null
|
|
167
|
-
* `[]
|
|
168
|
-
*
|
|
169
|
-
* never ran must not collapse onto the same value.
|
|
181
|
+
* Parse a `gh label list --json name,color,description` payload. Null, not
|
|
182
|
+
* `[]`, when the call failed, as in {@link manyOrNull}. A repo with no labels
|
|
183
|
+
* and a `gh` call that never ran must not give the same value.
|
|
170
184
|
*/
|
|
171
185
|
function manyLabelsOrNull(r) {
|
|
172
186
|
if (r.exitCode !== 0)
|
|
@@ -182,7 +196,7 @@ function manyLabelsOrNull(r) {
|
|
|
182
196
|
return null;
|
|
183
197
|
return parseLabels(data);
|
|
184
198
|
}
|
|
185
|
-
/** The `gh issue list`/`view --json` field set
|
|
199
|
+
/** The `gh issue list`/`view --json` field set an {@link IssueRow} reads. */
|
|
186
200
|
const ISSUE_FIELDS = 'number,title,body,labels,state';
|
|
187
201
|
/** Parse one `gh issue list`/`view --json` row into an {@link IssueRow}. */
|
|
188
202
|
function parseIssueRow(data) {
|
|
@@ -214,9 +228,8 @@ function oneIssueOrNull(r) {
|
|
|
214
228
|
return parseIssueRow(data);
|
|
215
229
|
}
|
|
216
230
|
/**
|
|
217
|
-
* Parse a `gh issue list --json` payload that is an array. Null
|
|
218
|
-
* on failure,
|
|
219
|
-
* makes for PRs.
|
|
231
|
+
* Parse a `gh issue list --json` payload that is an array. Null, not `[]`,
|
|
232
|
+
* on failure, as {@link manyOrNull} does for PRs.
|
|
220
233
|
*/
|
|
221
234
|
function manyIssuesOrNull(r) {
|
|
222
235
|
if (r.exitCode !== 0)
|
|
@@ -238,7 +251,7 @@ function manyIssuesOrNull(r) {
|
|
|
238
251
|
}
|
|
239
252
|
return rows;
|
|
240
253
|
}
|
|
241
|
-
/** `gh pr list
|
|
254
|
+
/** `gh pr list …`: open PRs by default. `opts.repo` overrides the target's repo. */
|
|
242
255
|
export function prList(opts = {}, target) {
|
|
243
256
|
const limit = opts.limit ?? 100;
|
|
244
257
|
checkLimit(limit);
|
|
@@ -251,8 +264,8 @@ export function prList(opts = {}, target) {
|
|
|
251
264
|
argv.push('--search', opts.search);
|
|
252
265
|
if (opts.author !== undefined)
|
|
253
266
|
argv.push('--author', opts.author);
|
|
254
|
-
// A row without `number` does not parse and is dropped, so always ask for it
|
|
255
|
-
//
|
|
267
|
+
// A row without `number` does not parse and is dropped, so always ask for it.
|
|
268
|
+
// The exact-head filter below also reads `headRefName`.
|
|
256
269
|
const asked = withFields(opts.fields ?? PR_LIST_FIELDS, [
|
|
257
270
|
'number',
|
|
258
271
|
...(opts.head === undefined ? [] : ['headRefName']),
|
|
@@ -286,7 +299,7 @@ function notAFlag(what, value) {
|
|
|
286
299
|
}
|
|
287
300
|
}
|
|
288
301
|
/**
|
|
289
|
-
* `gh pr view <number|branch|url> --json
|
|
302
|
+
* `gh pr view <number|branch|url> --json …`: one PR, with the full field set
|
|
290
303
|
* by default.
|
|
291
304
|
*
|
|
292
305
|
* A branch is the PR whose head is that branch, as gh picks it. A URL names
|
|
@@ -318,15 +331,15 @@ export function prView(ref, fields = PR_VIEW_FIELDS, target) {
|
|
|
318
331
|
/**
|
|
319
332
|
* Open PRs whose head is exactly `branch`.
|
|
320
333
|
*
|
|
321
|
-
* The
|
|
322
|
-
*
|
|
334
|
+
* The usual way to ask "which PR is this branch?". It uses the smallest field
|
|
335
|
+
* set and a small limit. More than one row comes back only when forks
|
|
323
336
|
* share the branch name.
|
|
324
337
|
*/
|
|
325
338
|
export function openPrsForBranch(branch, limit = 5, target) {
|
|
326
339
|
return prList({ head: branch, limit, fields: PR_REF_FIELDS }, target);
|
|
327
340
|
}
|
|
328
341
|
/**
|
|
329
|
-
* `gh pr view <n> --json body -q .body
|
|
342
|
+
* `gh pr view <n> --json body -q .body`: just the PR body. Null when gh
|
|
330
343
|
* failed. gh's `-q` output ends with a newline that is not part of the body,
|
|
331
344
|
* so every trailing `\r` and `\n` is trimmed (including any that were part of
|
|
332
345
|
* the body, such as the `\r\n` of a body saved with CRLF line endings).
|
|
@@ -340,10 +353,10 @@ export function prBody(number, target) {
|
|
|
340
353
|
};
|
|
341
354
|
}
|
|
342
355
|
/**
|
|
343
|
-
* `gh api [-i] [-H If-None-Match: …] <endpoint
|
|
356
|
+
* `gh api [-i] [-H If-None-Match: …] <endpoint>`: a GET against the REST API. `{owner}` and
|
|
344
357
|
* `{repo}` placeholders in the endpoint are filled from the target's repo, or by gh from the
|
|
345
358
|
* cwd's repo when the target names none.
|
|
346
|
-
* Unchecked
|
|
359
|
+
* Unchecked. Failure is a value (`ok: false`), so a caller can tell a 304
|
|
347
360
|
* from an outage.
|
|
348
361
|
*/
|
|
349
362
|
export function api(endpoint, opts = {}, target) {
|
|
@@ -380,16 +393,16 @@ function flattenPages(body) {
|
|
|
380
393
|
return body.flat();
|
|
381
394
|
}
|
|
382
395
|
/**
|
|
383
|
-
* `gh api graphql -f query=… [--paginate --slurp]
|
|
396
|
+
* `gh api graphql -f query=… [--paginate --slurp]`: a GraphQL query.
|
|
384
397
|
*
|
|
385
|
-
* Returns each page's `data`, one entry per page (one without `paginate`)
|
|
386
|
-
*
|
|
387
|
-
* carries `errors` or lacks `data
|
|
398
|
+
* Returns each page's `data`, one entry per page (one without `paginate`).
|
|
399
|
+
* Returns null when the call failed, the output did not parse, or any page
|
|
400
|
+
* carries `errors` or lacks `data`. A partial answer counts as a failure. Read one
|
|
388
401
|
* connection across the pages with {@link nodesAt}.
|
|
389
402
|
*
|
|
390
403
|
* A string variable of exactly `{owner}` or `{repo}` is filled from the
|
|
391
404
|
* target's repo, or by gh from the clone. A mutation document throws
|
|
392
|
-
* `RangeError
|
|
405
|
+
* `RangeError`. This is a read, and changes go through `raw`.
|
|
393
406
|
*
|
|
394
407
|
* @experimental
|
|
395
408
|
*/
|
|
@@ -458,8 +471,8 @@ export function graphql(query, variables = {}, opts = {}, target) {
|
|
|
458
471
|
};
|
|
459
472
|
}
|
|
460
473
|
/**
|
|
461
|
-
* The `nodes` of one connection, joined across {@link graphql} pages
|
|
462
|
-
* `path` into each page and
|
|
474
|
+
* The `nodes` of one connection, joined across {@link graphql} pages. It
|
|
475
|
+
* follows `path` into each page and takes its `nodes` array. Null when `pages` is
|
|
463
476
|
* null, or any page lacks the path or a `nodes` array, so a page that came
|
|
464
477
|
* back short is never read as fewer rows.
|
|
465
478
|
*
|
|
@@ -485,7 +498,7 @@ export function nodesAt(pages, path) {
|
|
|
485
498
|
}
|
|
486
499
|
return out;
|
|
487
500
|
}
|
|
488
|
-
/**
|
|
501
|
+
/** Split one header block off the front of `text`: the block, and what follows its blank line. */
|
|
489
502
|
function takeHeaderBlock(text) {
|
|
490
503
|
const crlf = text.indexOf('\r\n\r\n');
|
|
491
504
|
const lf = text.indexOf('\n\n');
|
|
@@ -498,7 +511,7 @@ function takeHeaderBlock(text) {
|
|
|
498
511
|
const STATUS_LINE = /^HTTP\/[\d.]+\s+\d{3}/i;
|
|
499
512
|
/**
|
|
500
513
|
* Split a `gh api -i` payload (status line + headers, blank line, body) and decode it.
|
|
501
|
-
* A real `gh` prints one block.
|
|
514
|
+
* A real `gh` prints one block. If there are several, the last one is read:
|
|
502
515
|
* its status and ETag, and the body that follows it.
|
|
503
516
|
*/
|
|
504
517
|
function parseApi(r, withHeaders) {
|
|
@@ -542,7 +555,7 @@ function parseApi(r, withHeaders) {
|
|
|
542
555
|
...(r.error === undefined ? {} : { error: r.error }),
|
|
543
556
|
};
|
|
544
557
|
}
|
|
545
|
-
/** `gh --version
|
|
558
|
+
/** `gh --version`: checks that `gh` is installed. True when `gh` runs. */
|
|
546
559
|
export function installed() {
|
|
547
560
|
return {
|
|
548
561
|
argv: ['--version'],
|
|
@@ -552,7 +565,7 @@ export function installed() {
|
|
|
552
565
|
parse: (r) => r.exitCode === 0,
|
|
553
566
|
};
|
|
554
567
|
}
|
|
555
|
-
/** `gh auth status [--hostname <host>]
|
|
568
|
+
/** `gh auth status [--hostname <host>]`: checks credentials, for one host when the target names it. */
|
|
556
569
|
export function authStatus(target) {
|
|
557
570
|
return {
|
|
558
571
|
argv: ['auth', 'status', ...hostArgs(target)],
|
|
@@ -563,8 +576,8 @@ export function authStatus(target) {
|
|
|
563
576
|
};
|
|
564
577
|
}
|
|
565
578
|
/**
|
|
566
|
-
* `gh label list --json name,color,description
|
|
567
|
-
* up to `limit` (default 1000
|
|
579
|
+
* `gh label list --json name,color,description`: every label on the repo,
|
|
580
|
+
* up to `limit` (default 1000. `gh`'s own default is 30).
|
|
568
581
|
*/
|
|
569
582
|
export function labelList(opts = {}, target) {
|
|
570
583
|
const limit = opts.limit ?? 1000;
|
|
@@ -584,7 +597,7 @@ export function labelList(opts = {}, target) {
|
|
|
584
597
|
parse: (r) => limited(manyLabelsOrNull(r), limit),
|
|
585
598
|
};
|
|
586
599
|
}
|
|
587
|
-
/** `gh issue list --json
|
|
600
|
+
/** `gh issue list --json …`: open issues, optionally by label. */
|
|
588
601
|
export function issueList(opts = {}, target) {
|
|
589
602
|
const limit = opts.limit ?? 200;
|
|
590
603
|
checkLimit(limit);
|
|
@@ -594,7 +607,7 @@ export function issueList(opts = {}, target) {
|
|
|
594
607
|
argv.push('--json', ISSUE_FIELDS, '--limit', String(limit + 1));
|
|
595
608
|
return { argv, checked: false, readOnly: true, parse: (r) => limited(manyIssuesOrNull(r), limit) };
|
|
596
609
|
}
|
|
597
|
-
/** `gh issue view <n> --json
|
|
610
|
+
/** `gh issue view <n> --json …`: one issue, same field set as {@link issueList}. */
|
|
598
611
|
export function issueView(number, target) {
|
|
599
612
|
return {
|
|
600
613
|
argv: ['issue', 'view', String(number), ...repoArgs(target), '--json', ISSUE_FIELDS],
|
|
@@ -643,8 +656,8 @@ function rowsOrNull(r, parse) {
|
|
|
643
656
|
return data.map(parse).filter((row) => row !== null);
|
|
644
657
|
}
|
|
645
658
|
/**
|
|
646
|
-
* `gh repo list <owner> --json
|
|
647
|
-
* from the target (`GH_HOST`)
|
|
659
|
+
* `gh repo list <owner> --json …`: an owner's repositories. The host comes
|
|
660
|
+
* from the target (`GH_HOST`). The target's repo is not used.
|
|
648
661
|
*/
|
|
649
662
|
export function repoList(owner, opts = {}, target) {
|
|
650
663
|
notAFlag('repoList owner', owner);
|
|
@@ -668,7 +681,7 @@ export function repoList(owner, opts = {}, target) {
|
|
|
668
681
|
};
|
|
669
682
|
}
|
|
670
683
|
/**
|
|
671
|
-
* `gh repo view [<[host/]owner/name>] --json
|
|
684
|
+
* `gh repo view [<[host/]owner/name>] --json …`: one repository. That is `repo`,
|
|
672
685
|
* else the target's repo, else the clone at the client's cwd.
|
|
673
686
|
*/
|
|
674
687
|
export function repoView(repo, fields = REPO_FIELDS, target) {
|
|
@@ -699,9 +712,9 @@ export function repoView(repo, fields = REPO_FIELDS, target) {
|
|
|
699
712
|
};
|
|
700
713
|
}
|
|
701
714
|
/**
|
|
702
|
-
* `gh search prs [flags] -- <query
|
|
715
|
+
* `gh search prs [flags] -- <query>`: pull requests across repositories,
|
|
703
716
|
* with GitHub's search syntax in `query` (it may start with `-`, as in
|
|
704
|
-
* `-label:bug`). The host comes from the target (`GH_HOST`)
|
|
717
|
+
* `-label:bug`). The host comes from the target (`GH_HOST`). The target's
|
|
705
718
|
* repo is not used, so pass `opts.repo` or `opts.owner` to narrow.
|
|
706
719
|
*/
|
|
707
720
|
export function searchPrs(query, opts = {}, target) {
|
|
@@ -755,7 +768,7 @@ export function createPr(opts, target) {
|
|
|
755
768
|
},
|
|
756
769
|
};
|
|
757
770
|
}
|
|
758
|
-
/** `gh pr edit <n
|
|
771
|
+
/** `gh pr edit <n>`: change the title or the body. */
|
|
759
772
|
export function editPr(number, opts, target) {
|
|
760
773
|
const argv = ['pr', 'edit', String(number), ...repoArgs(target)];
|
|
761
774
|
if (opts.title !== undefined)
|
|
@@ -764,18 +777,18 @@ export function editPr(number, opts, target) {
|
|
|
764
777
|
argv.push('--body', opts.body);
|
|
765
778
|
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
766
779
|
}
|
|
767
|
-
/** `gh pr edit <n> --body
|
|
780
|
+
/** `gh pr edit <n> --body`: rewrite just the body. */
|
|
768
781
|
export function editPrBody(number, body, target) {
|
|
769
782
|
return editPr(number, { body }, target);
|
|
770
783
|
}
|
|
771
|
-
/** `gh pr update-branch <n> [--rebase]
|
|
784
|
+
/** `gh pr update-branch <n> [--rebase]`: bring the head up to date with its base. */
|
|
772
785
|
export function prUpdateBranch(number, style = 'merge', target) {
|
|
773
786
|
const argv = ['pr', 'update-branch', String(number), ...repoArgs(target)];
|
|
774
787
|
if (style === 'rebase')
|
|
775
788
|
argv.push('--rebase');
|
|
776
789
|
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
777
790
|
}
|
|
778
|
-
/** `gh pr ready <n> [--undo]
|
|
791
|
+
/** `gh pr ready <n> [--undo]`: mark a draft PR ready for review, or (with `draft`) convert it back to a draft. */
|
|
779
792
|
export function prSetDraft(number, draft, target) {
|
|
780
793
|
const argv = ['pr', 'ready', String(number), ...repoArgs(target)];
|
|
781
794
|
if (draft)
|
|
@@ -783,13 +796,13 @@ export function prSetDraft(number, draft, target) {
|
|
|
783
796
|
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
784
797
|
}
|
|
785
798
|
/**
|
|
786
|
-
* `gh api -X PATCH repos/{owner}/{repo}/issues/comments/<id> -f body
|
|
799
|
+
* `gh api -X PATCH repos/{owner}/{repo}/issues/comments/<id> -f body=…`: rewrite one
|
|
787
800
|
* conversation comment.
|
|
788
801
|
*
|
|
789
|
-
* `commentId` is the comment's numeric REST id, as text
|
|
802
|
+
* `commentId` is the comment's numeric REST id, as text. That is the `id` that
|
|
790
803
|
* `gh api repos/{owner}/{repo}/issues/<n>/comments` returns. It is not the
|
|
791
804
|
* GraphQL node id (such as `IC_kw…`) that `gh pr view --json comments` gives
|
|
792
|
-
* in `Comment.id
|
|
805
|
+
* in `Comment.id`. The REST path answers 404 for a node id.
|
|
793
806
|
*
|
|
794
807
|
* Throws `RangeError` when `commentId` is not all digits, so a node id fails
|
|
795
808
|
* at once and no text can change which endpoint the PATCH reaches.
|
|
@@ -817,10 +830,10 @@ export function issueCommentEdit(commentId, body, target) {
|
|
|
817
830
|
}
|
|
818
831
|
/**
|
|
819
832
|
* `gh pr merge <n> --<method> --match-head-commit <sha>`. The head sha is
|
|
820
|
-
* required
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
833
|
+
* required. GitHub refuses the merge when the PR head moved since the caller
|
|
834
|
+
* checked it. So a push that lands between the check and the merge cannot be
|
|
835
|
+
* merged without review. The branch is left alone (no `--delete-branch`).
|
|
836
|
+
* Deleting it is the caller's job.
|
|
824
837
|
*/
|
|
825
838
|
export function prMerge(number, opts, target) {
|
|
826
839
|
const argv = [
|
|
@@ -837,14 +850,13 @@ export function prMerge(number, opts, target) {
|
|
|
837
850
|
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}}}}}}}}';
|
|
838
851
|
/**
|
|
839
852
|
* A PR's review threads over GraphQL (`gh pr view --json` has no thread
|
|
840
|
-
* field). Null
|
|
841
|
-
* expected shape, or more than 100 threads exist
|
|
842
|
-
* PR is clear to merge must
|
|
843
|
-
* threads".
|
|
853
|
+
* field). Null, never `[]`, when the call failed, the payload was not the
|
|
854
|
+
* expected shape, or more than 100 threads exist. A caller deciding whether a
|
|
855
|
+
* PR is clear to merge must tell "could not tell" from "no threads".
|
|
844
856
|
*/
|
|
845
857
|
export function prReviewThreads(number, target) {
|
|
846
858
|
checkTarget(target);
|
|
847
|
-
// `-F` lets gh fill the placeholders from the clone
|
|
859
|
+
// `-F` lets gh fill the placeholders from the clone. Real values go as plain strings.
|
|
848
860
|
const [owner, name] = target?.repo?.split('/') ?? [];
|
|
849
861
|
const repo = owner === undefined || name === undefined
|
|
850
862
|
? ['-F', 'owner={owner}', '-F', 'name={repo}']
|
|
@@ -890,7 +902,7 @@ export function prReviewThreads(number, target) {
|
|
|
890
902
|
}
|
|
891
903
|
/**
|
|
892
904
|
* `gh label create <name> --color <hex> --description <text> --force`.
|
|
893
|
-
* `--force` makes it safe to repeat
|
|
905
|
+
* `--force` makes it safe to repeat. When the label already exists, its color
|
|
894
906
|
* and description are set to these instead of failing.
|
|
895
907
|
*/
|
|
896
908
|
export function createLabel(opts, target) {
|
|
@@ -907,7 +919,7 @@ export function createLabel(opts, target) {
|
|
|
907
919
|
];
|
|
908
920
|
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
909
921
|
}
|
|
910
|
-
/** `gh label edit <name
|
|
922
|
+
/** `gh label edit <name>`: change the color or description. A flag not given is left out. */
|
|
911
923
|
export function editLabel(name, opts, target) {
|
|
912
924
|
const argv = ['label', 'edit', name, ...repoArgs(target)];
|
|
913
925
|
if (opts.color !== undefined)
|
|
@@ -917,21 +929,18 @@ export function editLabel(name, opts, target) {
|
|
|
917
929
|
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
918
930
|
}
|
|
919
931
|
/**
|
|
920
|
-
* `gh issue edit <n> [--remove-label <current>] --add-label <new
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
*
|
|
927
|
-
*
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
*
|
|
932
|
-
* label immediately before calling this. `currentLabel` is `null` when the
|
|
933
|
-
* issue carries no prior label to remove — the `--remove-label` flag is then
|
|
934
|
-
* omitted rather than passed empty.
|
|
932
|
+
* `gh issue edit <n> [--remove-label <current>] --add-label <new>`: swap one
|
|
933
|
+
* label for another on an issue in one call. The issue is never left with
|
|
934
|
+
* neither label or both labels between the remove and the add.
|
|
935
|
+
*
|
|
936
|
+
* That covers only the call itself. `currentLabel` is whatever the caller
|
|
937
|
+
* read earlier, perhaps from a `gh issue list` minutes before. It is not read
|
|
938
|
+
* again here. If someone relabeled the issue since then, `--remove-label
|
|
939
|
+
* <currentLabel>` names a label the issue no longer has. `gh` skips that
|
|
940
|
+
* removal with no error and still adds `newLabel`. The issue then has both
|
|
941
|
+
* its current label and `newLabel`. A caller that cares reads the label again
|
|
942
|
+
* just before calling this. Pass `null` for `currentLabel` when there is no
|
|
943
|
+
* label to remove. The `--remove-label` flag is then left out.
|
|
935
944
|
*/
|
|
936
945
|
export function setIssueLabel(number, currentLabel, newLabel, target) {
|
|
937
946
|
const argv = ['issue', 'edit', String(number), ...repoArgs(target)];
|
|
@@ -940,7 +949,7 @@ export function setIssueLabel(number, currentLabel, newLabel, target) {
|
|
|
940
949
|
argv.push('--add-label', newLabel);
|
|
941
950
|
return { argv, checked: true, readOnly: false, parse: nothing };
|
|
942
951
|
}
|
|
943
|
-
/** `gh pr diff <n
|
|
952
|
+
/** `gh pr diff <n>`: the PR's unified diff, as text. Checked, so an empty diff is a value and a failed call throws. */
|
|
944
953
|
export function prDiff(number, target) {
|
|
945
954
|
return {
|
|
946
955
|
argv: ['pr', 'diff', String(number), ...repoArgs(target)],
|
|
@@ -949,7 +958,7 @@ export function prDiff(number, target) {
|
|
|
949
958
|
parse: (r) => r.stdout,
|
|
950
959
|
};
|
|
951
960
|
}
|
|
952
|
-
/** `gh pr comment <n> --body <body
|
|
961
|
+
/** `gh pr comment <n> --body <body>`: post a conversation comment, returning the URL gh prints ('' if none). */
|
|
953
962
|
export function prComment(number, body, target) {
|
|
954
963
|
return {
|
|
955
964
|
argv: ['pr', 'comment', String(number), ...repoArgs(target), '--body', body],
|
|
@@ -967,10 +976,10 @@ export function prComment(number, body, target) {
|
|
|
967
976
|
}
|
|
968
977
|
/**
|
|
969
978
|
* Create a PR review with inline comments, returning the review's URL (empty
|
|
970
|
-
* when GitHub's reply carried none). The event is always `COMMENT
|
|
971
|
-
* library does not offer a way to approve or request changes.
|
|
979
|
+
* when GitHub's reply carried none). The event is always `COMMENT`. This
|
|
980
|
+
* library does not offer a way to approve or request changes. It goes through
|
|
972
981
|
* `gh api` with the payload on stdin, because `gh pr review` cannot attach
|
|
973
|
-
*
|
|
982
|
+
* comments to lines. `{owner}`/`{repo}` come from the target's repo, or
|
|
974
983
|
* are filled in by `gh` from the client's working directory.
|
|
975
984
|
*
|
|
976
985
|
* @experimental
|
|
@@ -1041,10 +1050,10 @@ export function branchPrFields(fields) {
|
|
|
1041
1050
|
/**
|
|
1042
1051
|
* True when GitHub has not finished computing this PR's mergeability.
|
|
1043
1052
|
*
|
|
1044
|
-
* The answer
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1053
|
+
* The first answer may not be final. GitHub starts the merge computation when
|
|
1054
|
+
* asked and answers `UNKNOWN` until it finishes. A caller that treats the
|
|
1055
|
+
* first `UNKNOWN` as final misjudges a PR whose computation is slow. So ask a
|
|
1056
|
+
* few times before trusting it.
|
|
1048
1057
|
*/
|
|
1049
1058
|
export function needsMergeStateRetry(pr) {
|
|
1050
1059
|
return pr.mergeable === 'UNKNOWN' || pr.mergeStateStatus === 'UNKNOWN';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osdlc/typed-gh",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "A typed GitHub client for TypeScript over the gh CLI. A total PullRequest/Review/CheckRun model, pure argv builders and parsers, and a tri-modal client — queries return parsed values, mutations throw, raw() is the escape hatch. Uses gh's own credentials; reads no token.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": "sksizer",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"LICENSE-APACHE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@osdlc/command-seam": "^0.
|
|
26
|
-
"@osdlc/typed-git": "^0.7.
|
|
25
|
+
"@osdlc/command-seam": "^0.10.0",
|
|
26
|
+
"@osdlc/typed-git": "^0.7.1",
|
|
27
27
|
"@osdlc/result": "^0.1.0"
|
|
28
28
|
},
|
|
29
29
|
"engines": {
|