@osdlc/typed-gh 0.9.5 → 0.11.1
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 +46 -0
- package/README.md +29 -10
- package/SECURITY.md +4 -3
- package/dist/async.d.ts +8 -2
- package/dist/async.js +39 -31
- package/dist/doors.d.ts +17 -4
- package/dist/doors.js +43 -48
- package/dist/failure.js +3 -8
- package/dist/repo.d.ts +9 -4
- package/dist/repo.js +12 -12
- package/dist/sync.d.ts +17 -5
- package/dist/sync.js +18 -6
- package/dist/types.d.ts +7 -1
- package/dist/verbs.d.ts +16 -8
- package/dist/verbs.js +98 -75
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# @osdlc/typed-gh
|
|
2
2
|
|
|
3
|
+
## 0.11.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 272da7a: A list that goes through GitHub search reports `truncated: true` when 1000 rows come back. Search returns at most 1000 results whatever the `limit`, so before, a `limit` of 1000 or more read as complete (`truncated: false`) while more matched. gh uses search for `searchPrs`, for `prList` with a non-empty `search` or an `author`, and for `issueList` with a `label`. A list that does not search is unchanged. `searchPrs` now asks gh for at most 1000 rows: a `limit` of 1000 or more used to fail, because gh refuses a larger search limit.
|
|
8
|
+
|
|
9
|
+
## 0.11.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- da5e6a3: `parseOwnerRepo` is now `@osdlc/typed-git`'s `parseRemoteRepo` without the host. Behaviour change: a remote that `parseGitUrl` recognises but names no owner and repository for is read host-agnostically (`host/owner/repo`) instead of answering null; every other remote reads as before.
|
|
14
|
+
`settle`, `runCheckedGh`, `runCheckedGhAsync`, `gh.try`, `gh.soft`, `classifyGhFailure` and the leading-dash guard now use `@osdlc/command-seam`'s `runFailed`, `tryMethods`, `methodFacade`, `classifyFailed` and `notAnOption`, with no change a caller sees: the same failures, and the same `RangeError` and message for an argument that starts with `-`. `repoView` and every `--repo` verb build `[host/]owner/name` through one helper.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [da5e6a3]
|
|
19
|
+
- Updated dependencies [da5e6a3]
|
|
20
|
+
- @osdlc/command-seam@0.14.0
|
|
21
|
+
- @osdlc/typed-git@0.10.0
|
|
22
|
+
|
|
23
|
+
## 0.10.0
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- b835ae4: Behaviour change: output that the runner cut short (`outputIncomplete`, at the output cap or a timeout) now fails as `CommandFailed` for every verb but a probe, and in `runCheckedGh`/`runCheckedGhAsync`, instead of being parsed as whole. Behaviour change: every `AsyncGh` method that builds a verb now rejects its Promise on a bad argument (a verb's `RangeError`) instead of throwing at the call site.
|
|
28
|
+
`parseOwnerRepo` now reads a GitHub Enterprise remote (a host `parseGitUrl` does not know) through `parseRemoteUrl` instead of answering null. `graphql` refuses a document holding a mutation or subscription anywhere (after a fragment, a leading `,`, or a second operation), and a variable named `query` or `operationName`. `prView` passes a scheme-less PR URL (`github.com/o/r/pull/7`) to gh with `https://`, so gh no longer takes it for a branch name. `repoView` treats a hostname of `''` as not set. `createLabel` and `editLabel` refuse a name that starts with `-`. `openPrFor` takes `timeoutMs`.
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- Updated dependencies [b835ae4]
|
|
33
|
+
- Updated dependencies [b835ae4]
|
|
34
|
+
- Updated dependencies [fdef74f]
|
|
35
|
+
- @osdlc/command-seam@0.13.0
|
|
36
|
+
- @osdlc/typed-git@0.9.0
|
|
37
|
+
|
|
38
|
+
## 0.9.6
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- 3d6d087: Document that a `Repository`'s `owner` and `name`, and a `PrSearchHit`'s `repository`, come back in GitHub's casing, not as the caller wrote them. The `Repository` and `PrSearchHit` type docs, the `repoList` and `repoView` doc comments and the README say so.
|
|
43
|
+
- f013f38: The default runners now find `git` and `gh` on absolute `PATH` entries only, and spawn the absolute path found. Before, a bare `git` or `gh` was spawned by name with the child's `cwd` set to the checkout, so a file of that name in the checkout could run in place of the real binary: on Windows, where a bare name is searched in `cwd` first, and on every platform through a relative or empty `PATH` entry. A binary that is not found on an absolute entry fails as a missing binary (`not-installed`). typed-gh's `defaultRunnerFactory` does the same lookup. An injected runner is unchanged.
|
|
44
|
+
- Updated dependencies [f013f38]
|
|
45
|
+
- Updated dependencies [f013f38]
|
|
46
|
+
- @osdlc/command-seam@0.12.1
|
|
47
|
+
- @osdlc/typed-git@0.8.2
|
|
48
|
+
|
|
3
49
|
## 0.9.5
|
|
4
50
|
|
|
5
51
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -116,8 +116,9 @@ stderr and stdout, as the classifier does.
|
|
|
116
116
|
|
|
117
117
|
Setup checks return their failure as a value. `installed()`, `authenticated()`,
|
|
118
118
|
`authStatus()`, `ready()` and `api()` return their failure as part of their
|
|
119
|
-
value,
|
|
120
|
-
|
|
119
|
+
value, from the plain method and from `try`. They have no soft form:
|
|
120
|
+
`gh.soft.api(...)` throws a `TypeError`. For them, "gh is not installed" is the
|
|
121
|
+
answer to the question they ask.
|
|
121
122
|
|
|
122
123
|
```ts
|
|
123
124
|
import { describe } from '@osdlc/result'
|
|
@@ -169,6 +170,13 @@ if (page.truncated) console.warn('more than 50 open PRs; raise the limit')
|
|
|
169
170
|
`prList`, `issueList`, `labelList`, `repoList` and `searchPrs` all work this
|
|
170
171
|
way. A `limit` that is not a positive integer throws `RangeError`.
|
|
171
172
|
|
|
173
|
+
GitHub search returns at most 1000 results, whatever the `limit`. gh uses
|
|
174
|
+
search for `searchPrs`, for `prList` with a non-empty `search` or an `author`,
|
|
175
|
+
and for `issueList` with a `label`. Those report `truncated: true` when 1000
|
|
176
|
+
rows come back: there may be more that search would not return. Narrow the
|
|
177
|
+
query to see them. `searchPrs` asks gh for at most 1000 rows, since gh refuses
|
|
178
|
+
a larger search limit.
|
|
179
|
+
|
|
172
180
|
## A repository without a clone
|
|
173
181
|
|
|
174
182
|
By default a client talks to the repository of the clone at its directory.
|
|
@@ -375,7 +383,8 @@ const repos = await runVerbAsync(repoList('acme', {}, { hostname: 'ghe.example.c
|
|
|
375
383
|
})
|
|
376
384
|
```
|
|
377
385
|
|
|
378
|
-
|
|
386
|
+
`runVerb` takes a `CommandRunner` and `runVerbAsync` an `AsyncCommandRunner`. A plain
|
|
387
|
+
`(binary, argv, opts) => result` function is not a runner; wrap it yourself as
|
|
379
388
|
`{ run: (argv, opts) => fn('gh', argv, opts) }`.
|
|
380
389
|
|
|
381
390
|
## `AsyncGh`
|
|
@@ -384,7 +393,8 @@ The awaitable form of `Gh`, with the same methods, for code that runs many calls
|
|
|
384
393
|
not block the event loop. It has the same three call styles, as Promises. The plain method rejects
|
|
385
394
|
with the classified failure. `try` resolves to a `Result<T, GhFailure>`. `soft` resolves `null` for
|
|
386
395
|
a failed query. `raw` and `rawChecked` run any `gh` call. Both clients use the same verb table, so
|
|
387
|
-
they cannot drift apart. It takes an `AsyncCommandRunner` and defaults to
|
|
396
|
+
they cannot drift apart. It takes an `AsyncCommandRunner` and defaults to
|
|
397
|
+
`spawnRunnerAsync("gh", { absolutePathOnly: true })`.
|
|
388
398
|
|
|
389
399
|
```ts
|
|
390
400
|
import { AsyncGh } from '@osdlc/typed-gh'
|
|
@@ -439,7 +449,7 @@ const prs = nodesAt(pages, ['repository', 'pullRequests'])
|
|
|
439
449
|
```
|
|
440
450
|
|
|
441
451
|
With `paginate`, the query must take `$endCursor` and page one connection. gh follows only that
|
|
442
|
-
one. `graphql` refuses a mutation with `RangeError`. Send one through `raw`.
|
|
452
|
+
one. `graphql` refuses a document holding a mutation or subscription with `RangeError`. Send one through `raw`.
|
|
443
453
|
|
|
444
454
|
## Repositories and search
|
|
445
455
|
|
|
@@ -450,9 +460,14 @@ const hits: ListResult<PrSearchHit> = gh.searchPrs('TICKET-123', { owner: 'acme'
|
|
|
450
460
|
```
|
|
451
461
|
|
|
452
462
|
A `Repository` carries `nameWithOwner`, `owner`, `name`, `description`, `defaultBranch`,
|
|
453
|
-
`visibility`, `isArchived` and `url`. `
|
|
454
|
-
|
|
455
|
-
|
|
463
|
+
`visibility`, `isArchived` and `url`. `owner` and `name` are cut from `nameWithOwner`, so they
|
|
464
|
+
come back in GitHub's casing, not as you wrote them: `repoList('ACME')` gives `owner: 'acme'` when
|
|
465
|
+
the account is `acme`, and `repoView` works the same way. To build paths that match your own
|
|
466
|
+
config, use your own value.
|
|
467
|
+
|
|
468
|
+
`gh search prs` has its own field set. A `PrSearchHit` names its `repository`, also in GitHub's
|
|
469
|
+
casing, but has no head or base branch, so it is not a `PullRequest`. Its `state` is upper-cased to
|
|
470
|
+
match. The query takes GitHub's search syntax and may start with `-`.
|
|
456
471
|
|
|
457
472
|
## Passing your own runner
|
|
458
473
|
|
|
@@ -469,7 +484,9 @@ new Gh('/repo', { runner: empty }).prList() // { rows: [], truncated: false }
|
|
|
469
484
|
This package re-exports `CommandFailed` and the runner and result types (`CommandRunner`,
|
|
470
485
|
`AsyncCommandRunner`, `CommandResult`, `CommandRunOptions`), since they appear in its signatures.
|
|
471
486
|
Import the runners themselves, such as `spawnRunner` and `spawnRunnerAsync`, from
|
|
472
|
-
`@osdlc/command-seam`.
|
|
487
|
+
`@osdlc/command-seam`. The default runners find `gh` on absolute `PATH` entries only, so a file
|
|
488
|
+
named `gh` in the working directory cannot run in its place. Pass `{ absolutePathOnly: true }` when
|
|
489
|
+
you build a real runner yourself.
|
|
473
490
|
|
|
474
491
|
## API
|
|
475
492
|
|
|
@@ -539,7 +556,9 @@ Each verb takes an optional `GhTarget` last. Each client method has a verb of th
|
|
|
539
556
|
- `parsePullRequest`: reads a `gh --json` payload into a `PullRequest`, or `null`.
|
|
540
557
|
- `anyCheckFailed`: true if any check in a list failed.
|
|
541
558
|
- `normalizeAuthorLogin`: drops a `[bot]` suffix from a login.
|
|
542
|
-
- `parseOwnerRepo`: reads a remote URL into `{ owner, repo }`, or `null`.
|
|
559
|
+
- `parseOwnerRepo`: reads a remote URL into `{ owner, repo }`, or `null`. It is
|
|
560
|
+
`@osdlc/typed-git`'s `parseRemoteRepo` without the host, so any host's remote reads, not only
|
|
561
|
+
GitHub's. Use `parseRemoteRepo` when you need the host too.
|
|
543
562
|
- `ownerRepoSlug`: writes `{ owner, repo }` as `owner/repo`.
|
|
544
563
|
- `parsePrUrl`: reads a pull request URL on any GitHub host into its host, owner, repository and
|
|
545
564
|
number, or `null`.
|
package/SECURITY.md
CHANGED
|
@@ -42,9 +42,10 @@ not intend are in scope.
|
|
|
42
42
|
value into a REST path. Pass endpoints you built, not text from a user.
|
|
43
43
|
`issueCommentEdit(commentId)` accepts only digits and throws `RangeError`
|
|
44
44
|
for anything else, so its id cannot change the path.
|
|
45
|
-
-
|
|
46
|
-
remotes, or `GH_REPO`. A checkout whose
|
|
47
|
-
another repository sends mutations there.
|
|
45
|
+
- **Without a `repo` option, `gh` chooses the repository.** It reads the
|
|
46
|
+
working directory's git remotes, or `GH_REPO`. A checkout whose
|
|
47
|
+
`.git/config` points `origin` at another repository sends mutations there.
|
|
48
|
+
Pass `repo` (and `hostname`) to name the target in argv. `ownerRepo` only parses a URL you
|
|
48
49
|
give it. It does not check where `gh` will act.
|
|
49
50
|
- **`raw` and `rawChecked` run any `gh` command.** They have no allow list.
|
|
50
51
|
- **`CommandFailed` carries the full argv.** Its message quotes the arguments,
|
package/dist/async.d.ts
CHANGED
|
@@ -106,9 +106,15 @@ export declare class AsyncGh {
|
|
|
106
106
|
issueList(opts?: IssueListOptions & AsyncCallOptions): Promise<ListResult<IssueRow>>;
|
|
107
107
|
/** One issue with its labels, or null when there is no such issue. */
|
|
108
108
|
issueView(number: number, opts?: AsyncCallOptions): Promise<IssueRow | null>;
|
|
109
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* An owner's repositories. Rejects when gh fails. Each row's `owner` is in
|
|
111
|
+
* GitHub's casing, not as passed. See `Gh.repoList`.
|
|
112
|
+
*/
|
|
110
113
|
repoList(owner: string, opts?: v.RepoListOptions & AsyncCallOptions): Promise<ListResult<Repository>>;
|
|
111
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* One repository, or null when there is no such repository. `owner` and
|
|
116
|
+
* `name` are in GitHub's casing, not as passed. See `Gh.repoView`.
|
|
117
|
+
*/
|
|
112
118
|
repoView(repo?: string | undefined, opts?: {
|
|
113
119
|
fields?: readonly string[] | undefined;
|
|
114
120
|
} & AsyncCallOptions): Promise<Repository | null>;
|
package/dist/async.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* two cannot differ on argv or parsing. Use it where the sync client would
|
|
11
11
|
* block the event loop, such as many per-PR queries at once.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { spawnRunnerAsync } from '@osdlc/command-seam';
|
|
14
14
|
import { runCheckedGhAsync, softAsync, triedAsync } from "./doors.js";
|
|
15
15
|
import { runVerbAsync } from "./run.js";
|
|
16
16
|
import { ownerRepoSlug, parseOwnerRepo, parsePrRef } from "./repo.js";
|
|
@@ -35,7 +35,7 @@ export class AsyncGh {
|
|
|
35
35
|
this.repo = opts.repo;
|
|
36
36
|
this.hostname = opts.hostname;
|
|
37
37
|
this.target = { repo: opts.repo, hostname: opts.hostname };
|
|
38
|
-
this.runner =
|
|
38
|
+
this.runner = opts.runner ?? spawnRunnerAsync('gh', { absolutePathOnly: true });
|
|
39
39
|
}
|
|
40
40
|
/** The same client rooted at a different directory (same runner, repo and host). */
|
|
41
41
|
at(cwd) {
|
|
@@ -79,12 +79,15 @@ export class AsyncGh {
|
|
|
79
79
|
return (await this.exec(verb, call));
|
|
80
80
|
}
|
|
81
81
|
// ── queries ───────────────────────────────────────────────────────────────
|
|
82
|
+
//
|
|
83
|
+
// Every method that builds a verb is `async`, so a bad argument (a verb's
|
|
84
|
+
// RangeError) rejects the Promise instead of throwing at the call site.
|
|
82
85
|
/** Pull requests matching `opts`. Rejects when gh fails. `soft.prList` resolves null instead. */
|
|
83
|
-
prList(opts = {}) {
|
|
86
|
+
async prList(opts = {}) {
|
|
84
87
|
return this.read(v.prList(opts, this.target), opts);
|
|
85
88
|
}
|
|
86
89
|
/** One PR by number, branch or URL, with the heavy fields, or null. See `Gh.prView`. */
|
|
87
|
-
prView(ref, opts = {}) {
|
|
90
|
+
async prView(ref, opts = {}) {
|
|
88
91
|
return this.exec(v.prView(ref, opts.fields ?? v.PR_VIEW_FIELDS, this.target), opts);
|
|
89
92
|
}
|
|
90
93
|
/** The one PR that `input` names, or null. See `Gh.prFind`. */
|
|
@@ -127,11 +130,11 @@ export class AsyncGh {
|
|
|
127
130
|
return last;
|
|
128
131
|
}
|
|
129
132
|
/** A PR's body text. Rejects when gh fails. */
|
|
130
|
-
prBody(number, opts = {}) {
|
|
133
|
+
async prBody(number, opts = {}) {
|
|
131
134
|
return this.read(v.prBody(number, this.target), opts);
|
|
132
135
|
}
|
|
133
136
|
/** A REST GET through `gh api`. Failure is `ok: false`, never a rejection. */
|
|
134
|
-
api(endpoint, opts = {}) {
|
|
137
|
+
async api(endpoint, opts = {}) {
|
|
135
138
|
return this.exec(v.api(endpoint, opts, this.target), opts);
|
|
136
139
|
}
|
|
137
140
|
/**
|
|
@@ -139,35 +142,41 @@ export class AsyncGh {
|
|
|
139
142
|
*
|
|
140
143
|
* @experimental
|
|
141
144
|
*/
|
|
142
|
-
graphql(query, variables = {}, opts = {}) {
|
|
145
|
+
async graphql(query, variables = {}, opts = {}) {
|
|
143
146
|
return this.read(v.graphql(query, variables, opts, this.target), opts);
|
|
144
147
|
}
|
|
145
148
|
/** Every label on the repository (up to `opts.limit`, default 1000). Rejects when gh fails. */
|
|
146
|
-
labelList(opts = {}) {
|
|
149
|
+
async labelList(opts = {}) {
|
|
147
150
|
return this.read(v.labelList(opts, this.target), opts);
|
|
148
151
|
}
|
|
149
152
|
/** Issues matching `opts`. Rejects when gh fails. */
|
|
150
|
-
issueList(opts = {}) {
|
|
153
|
+
async issueList(opts = {}) {
|
|
151
154
|
return this.read(v.issueList(opts, this.target), opts);
|
|
152
155
|
}
|
|
153
156
|
/** One issue with its labels, or null when there is no such issue. */
|
|
154
|
-
issueView(number, opts = {}) {
|
|
157
|
+
async issueView(number, opts = {}) {
|
|
155
158
|
return this.exec(v.issueView(number, this.target), opts);
|
|
156
159
|
}
|
|
157
|
-
/**
|
|
158
|
-
|
|
160
|
+
/**
|
|
161
|
+
* An owner's repositories. Rejects when gh fails. Each row's `owner` is in
|
|
162
|
+
* GitHub's casing, not as passed. See `Gh.repoList`.
|
|
163
|
+
*/
|
|
164
|
+
async repoList(owner, opts = {}) {
|
|
159
165
|
return this.read(v.repoList(owner, opts, this.target), opts);
|
|
160
166
|
}
|
|
161
|
-
/**
|
|
162
|
-
|
|
167
|
+
/**
|
|
168
|
+
* One repository, or null when there is no such repository. `owner` and
|
|
169
|
+
* `name` are in GitHub's casing, not as passed. See `Gh.repoView`.
|
|
170
|
+
*/
|
|
171
|
+
async repoView(repo, opts = {}) {
|
|
163
172
|
return this.exec(v.repoView(repo, opts.fields ?? v.REPO_FIELDS, this.target), opts);
|
|
164
173
|
}
|
|
165
174
|
/** Pull requests across repositories matching a search query. Rejects when gh fails. See `Gh.searchPrs`. */
|
|
166
|
-
searchPrs(query, opts = {}) {
|
|
175
|
+
async searchPrs(query, opts = {}) {
|
|
167
176
|
return this.read(v.searchPrs(query, opts, this.target), opts);
|
|
168
177
|
}
|
|
169
178
|
/** True iff the `gh` binary is on PATH and runs. */
|
|
170
|
-
installed() {
|
|
179
|
+
async installed() {
|
|
171
180
|
return this.exec(v.installed());
|
|
172
181
|
}
|
|
173
182
|
/** True iff `gh` holds credentials. */
|
|
@@ -175,7 +184,7 @@ export class AsyncGh {
|
|
|
175
184
|
return (await this.authStatus()).ok;
|
|
176
185
|
}
|
|
177
186
|
/** Login state and what `gh auth status` printed. */
|
|
178
|
-
authStatus() {
|
|
187
|
+
async authStatus() {
|
|
179
188
|
return this.exec(v.authStatus(this.target));
|
|
180
189
|
}
|
|
181
190
|
/** True iff `gh` is installed and authenticated. Skips the auth check when `gh` is not installed. */
|
|
@@ -191,23 +200,23 @@ export class AsyncGh {
|
|
|
191
200
|
}
|
|
192
201
|
// ── mutations (checked) ───────────────────────────────────────────────────
|
|
193
202
|
/** Open a PR and resolve to its URL ('' if gh printed none). */
|
|
194
|
-
createPr(opts) {
|
|
203
|
+
async createPr(opts) {
|
|
195
204
|
return this.exec(v.createPr(opts, this.target));
|
|
196
205
|
}
|
|
197
206
|
/** Retitle or rewrite a PR body. */
|
|
198
|
-
editPr(number, opts) {
|
|
207
|
+
async editPr(number, opts) {
|
|
199
208
|
return this.exec(v.editPr(number, opts, this.target));
|
|
200
209
|
}
|
|
201
210
|
/** Rewrite just a PR's body. */
|
|
202
|
-
editPrBody(number, body) {
|
|
211
|
+
async editPrBody(number, body) {
|
|
203
212
|
return this.exec(v.editPrBody(number, body, this.target));
|
|
204
213
|
}
|
|
205
214
|
/** The PR's unified diff. Rejects with the classified failure when gh cannot produce it. */
|
|
206
|
-
prDiff(number) {
|
|
215
|
+
async prDiff(number) {
|
|
207
216
|
return this.exec(v.prDiff(number, this.target));
|
|
208
217
|
}
|
|
209
218
|
/** Post one conversation comment on a PR. Resolves to its URL ('' if gh printed none). */
|
|
210
|
-
prComment(number, body) {
|
|
219
|
+
async prComment(number, body) {
|
|
211
220
|
return this.exec(v.prComment(number, body, this.target));
|
|
212
221
|
}
|
|
213
222
|
/**
|
|
@@ -215,15 +224,15 @@ export class AsyncGh {
|
|
|
215
224
|
*
|
|
216
225
|
* @experimental
|
|
217
226
|
*/
|
|
218
|
-
createReview(number, opts) {
|
|
227
|
+
async createReview(number, opts) {
|
|
219
228
|
return this.exec(v.createReview(number, opts, this.target));
|
|
220
229
|
}
|
|
221
230
|
/** Bring a PR's head branch up to date with its base. */
|
|
222
|
-
prUpdateBranch(number, style = 'merge') {
|
|
231
|
+
async prUpdateBranch(number, style = 'merge') {
|
|
223
232
|
return this.exec(v.prUpdateBranch(number, style, this.target));
|
|
224
233
|
}
|
|
225
234
|
/** Mark a PR ready for review (`draft: false`) or convert it to a draft (`draft: true`). */
|
|
226
|
-
prSetDraft(number, draft) {
|
|
235
|
+
async prSetDraft(number, draft) {
|
|
227
236
|
return this.exec(v.prSetDraft(number, draft, this.target));
|
|
228
237
|
}
|
|
229
238
|
/**
|
|
@@ -232,27 +241,26 @@ export class AsyncGh {
|
|
|
232
241
|
* @experimental
|
|
233
242
|
*/
|
|
234
243
|
async issueCommentEdit(commentId, body) {
|
|
235
|
-
// async, so a bad id rejects the Promise instead of throwing at the call site.
|
|
236
244
|
return this.exec(v.issueCommentEdit(commentId, body, this.target));
|
|
237
245
|
}
|
|
238
246
|
/** Merge a PR. GitHub refuses when its head is no longer `opts.headSha`. */
|
|
239
|
-
prMerge(number, opts) {
|
|
247
|
+
async prMerge(number, opts) {
|
|
240
248
|
return this.exec(v.prMerge(number, opts, this.target));
|
|
241
249
|
}
|
|
242
250
|
/** A PR's review threads. Rejects when they could not be read completely. `soft.prReviewThreads` resolves null instead. */
|
|
243
|
-
prReviewThreads(number) {
|
|
251
|
+
async prReviewThreads(number) {
|
|
244
252
|
return this.read(v.prReviewThreads(number, this.target));
|
|
245
253
|
}
|
|
246
254
|
/** Create a label (or force it to `opts`'s color/description if it exists). */
|
|
247
|
-
createLabel(opts) {
|
|
255
|
+
async createLabel(opts) {
|
|
248
256
|
return this.exec(v.createLabel(opts, this.target));
|
|
249
257
|
}
|
|
250
258
|
/** Recolor and/or redescribe an existing label. */
|
|
251
|
-
editLabel(name, opts) {
|
|
259
|
+
async editLabel(name, opts) {
|
|
252
260
|
return this.exec(v.editLabel(name, opts, this.target));
|
|
253
261
|
}
|
|
254
262
|
/** Swap an issue's label in a single `gh issue edit` call. */
|
|
255
|
-
setIssueLabel(number, currentLabel, newLabel) {
|
|
263
|
+
async setIssueLabel(number, currentLabel, newLabel) {
|
|
256
264
|
return this.exec(v.setIssueLabel(number, currentLabel, newLabel, this.target));
|
|
257
265
|
}
|
|
258
266
|
}
|
package/dist/doors.d.ts
CHANGED
|
@@ -27,12 +27,17 @@ import type { GhVerb } from './verbs.ts';
|
|
|
27
27
|
* Read the result of running `verb` as the methods that throw do.
|
|
28
28
|
*
|
|
29
29
|
* A checked verb has already thrown on a non-zero exit (the runner is
|
|
30
|
-
*
|
|
30
|
+
* {@link runCheckedGh}). A probe (`installed`, `authStatus`, `api`) carries its
|
|
31
31
|
* failure in its value. A read that exited non-zero throws the classified
|
|
32
32
|
* failure, unless the verb's `absent` says gh's answer was "there is no such
|
|
33
33
|
* thing", which `parse` turns into `null`. A read that exited 0 but parsed to `null`
|
|
34
34
|
* could not be read completely, and throws.
|
|
35
35
|
*
|
|
36
|
+
* A result flagged `outputIncomplete` throws for every verb but a probe, as
|
|
37
|
+
* typed-git's `gate` does: gh exited, but the runner stopped reading at the
|
|
38
|
+
* output cap or a timeout, so a parse would take cut-short output as whole.
|
|
39
|
+
* Both read a result through command-seam's `runFailed`.
|
|
40
|
+
*
|
|
36
41
|
* @throws `FailureError` carrying the classified {@link GhFailure} for a failed run.
|
|
37
42
|
* @throws `FailureError` carrying an `unreadable` {@link GhFailure}.
|
|
38
43
|
*/
|
|
@@ -43,9 +48,14 @@ export declare function settle<T>(verb: GhVerb<T>, result: CommandResult, binary
|
|
|
43
48
|
* `CommandFailed` as its `cause`. Anything else is returned as it is.
|
|
44
49
|
*/
|
|
45
50
|
export declare function ghFailureError(error: unknown): unknown;
|
|
46
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Run `argv` and throw {@link ghFailureError}'s classified failure unless gh
|
|
53
|
+
* exited 0 with its whole output. Output cut short (`outputIncomplete`) is a
|
|
54
|
+
* failure too, which `runChecked` alone would not catch: it checks only the
|
|
55
|
+
* exit code.
|
|
56
|
+
*/
|
|
47
57
|
export declare function runCheckedGh(runner: CommandRunner, argv: string[], opts: CommandRunOptions): CommandResult;
|
|
48
|
-
/** {@link
|
|
58
|
+
/** {@link runCheckedGh} for an awaitable runner, rejecting with the classified failure. */
|
|
49
59
|
export declare function runCheckedGhAsync(runner: AsyncCommandRunner, argv: string[], opts: AsyncCommandRunOptions): Promise<CommandResult>;
|
|
50
60
|
/** The members `try` and `soft` leave out: `raw`, `rawChecked`, helpers that run nothing, and properties. */
|
|
51
61
|
type Triable<C> = Omit<C, 'raw' | 'rawChecked' | 'at' | 'withRepo' | 'ownerRepo' | 'try' | 'soft' | 'cwd' | 'repo' | 'hostname'>;
|
|
@@ -73,7 +83,10 @@ export type Soft<C> = {
|
|
|
73
83
|
export declare function failureOf(error: {
|
|
74
84
|
failure: Failure;
|
|
75
85
|
}): GhFailure;
|
|
76
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Wrap a synchronous client so every method returns a `Result<T, GhFailure>`.
|
|
88
|
+
* The proxy is command-seam's `tryMethods`, so a new method needs no entry.
|
|
89
|
+
*/
|
|
77
90
|
export declare function triedSync<C extends object>(client: C): TriedSync<C>;
|
|
78
91
|
/** Wrap an awaitable client so every method promises a `Result<T, GhFailure>`. */
|
|
79
92
|
export declare function triedAsync<C extends object>(client: C): TriedAsync<C>;
|
package/dist/doors.js
CHANGED
|
@@ -19,30 +19,39 @@
|
|
|
19
19
|
* `try` and `soft` are proxies over the whole client, so a new method needs no
|
|
20
20
|
* change here.
|
|
21
21
|
*/
|
|
22
|
-
import { CommandFailed, isCommandFailed,
|
|
23
|
-
import {
|
|
22
|
+
import { CommandFailed, isCommandFailed, methodFacade, runFailed, tryMethods, tryMethodsAsync, } from '@osdlc/command-seam';
|
|
23
|
+
import { FailureError, isFailure } from '@osdlc/result';
|
|
24
24
|
import { classifyGhFailure, unreadableFailure } from "./failure.js";
|
|
25
25
|
/**
|
|
26
26
|
* Read the result of running `verb` as the methods that throw do.
|
|
27
27
|
*
|
|
28
28
|
* A checked verb has already thrown on a non-zero exit (the runner is
|
|
29
|
-
*
|
|
29
|
+
* {@link runCheckedGh}). A probe (`installed`, `authStatus`, `api`) carries its
|
|
30
30
|
* failure in its value. A read that exited non-zero throws the classified
|
|
31
31
|
* failure, unless the verb's `absent` says gh's answer was "there is no such
|
|
32
32
|
* thing", which `parse` turns into `null`. A read that exited 0 but parsed to `null`
|
|
33
33
|
* could not be read completely, and throws.
|
|
34
34
|
*
|
|
35
|
+
* A result flagged `outputIncomplete` throws for every verb but a probe, as
|
|
36
|
+
* typed-git's `gate` does: gh exited, but the runner stopped reading at the
|
|
37
|
+
* output cap or a timeout, so a parse would take cut-short output as whole.
|
|
38
|
+
* Both read a result through command-seam's `runFailed`.
|
|
39
|
+
*
|
|
35
40
|
* @throws `FailureError` carrying the classified {@link GhFailure} for a failed run.
|
|
36
41
|
* @throws `FailureError` carrying an `unreadable` {@link GhFailure}.
|
|
37
42
|
*/
|
|
38
43
|
export function settle(verb, result, binary) {
|
|
39
|
-
if (verb.
|
|
44
|
+
if (verb.probe === true)
|
|
40
45
|
return verb.parse(result);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
// A checked verb's runner has already thrown on a non-zero exit.
|
|
47
|
+
const failed = verb.checked
|
|
48
|
+
? result.outputIncomplete === true
|
|
49
|
+
: runFailed(result, (r) => verb.absent?.(r) === true);
|
|
50
|
+
if (failed)
|
|
44
51
|
throw ghFailureError(new CommandFailed(verb.argv, result, binary));
|
|
45
|
-
|
|
52
|
+
// A non-zero exit that got this far is gh's "there is no such thing".
|
|
53
|
+
if (verb.checked || result.exitCode !== 0)
|
|
54
|
+
return verb.parse(result);
|
|
46
55
|
const value = verb.parse(result);
|
|
47
56
|
if (value === null) {
|
|
48
57
|
throw new FailureError(unreadableFailure(verb.argv, result.stdout, binary));
|
|
@@ -57,24 +66,35 @@ export function settle(verb, result, binary) {
|
|
|
57
66
|
export function ghFailureError(error) {
|
|
58
67
|
return isCommandFailed(error) ? new FailureError(classifyGhFailure(error)) : error;
|
|
59
68
|
}
|
|
60
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Run `argv` and throw {@link ghFailureError}'s classified failure unless gh
|
|
71
|
+
* exited 0 with its whole output. Output cut short (`outputIncomplete`) is a
|
|
72
|
+
* failure too, which `runChecked` alone would not catch: it checks only the
|
|
73
|
+
* exit code.
|
|
74
|
+
*/
|
|
61
75
|
export function runCheckedGh(runner, argv, opts) {
|
|
62
76
|
try {
|
|
63
|
-
return
|
|
77
|
+
return whole(runner.run(argv, opts), argv, runner.binary);
|
|
64
78
|
}
|
|
65
79
|
catch (error) {
|
|
66
80
|
throw ghFailureError(error);
|
|
67
81
|
}
|
|
68
82
|
}
|
|
69
|
-
/** {@link
|
|
83
|
+
/** {@link runCheckedGh} for an awaitable runner, rejecting with the classified failure. */
|
|
70
84
|
export async function runCheckedGhAsync(runner, argv, opts) {
|
|
71
85
|
try {
|
|
72
|
-
return await
|
|
86
|
+
return whole(await runner.run(argv, opts), argv, runner.binary);
|
|
73
87
|
}
|
|
74
88
|
catch (error) {
|
|
75
89
|
throw ghFailureError(error);
|
|
76
90
|
}
|
|
77
91
|
}
|
|
92
|
+
/** `result`, or a `CommandFailed` thrown when it is a failure (command-seam's `runFailed`). */
|
|
93
|
+
function whole(result, argv, binary) {
|
|
94
|
+
if (runFailed(result))
|
|
95
|
+
throw new CommandFailed(argv, result, binary);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
78
98
|
/**
|
|
79
99
|
* The queries that have a soft form. A mutation has none. If a write's failure
|
|
80
100
|
* were dropped, the caller would believe the write worked.
|
|
@@ -110,45 +130,20 @@ export function failureOf(error) {
|
|
|
110
130
|
const thrown = error;
|
|
111
131
|
return isCommandFailed(thrown) ? classifyGhFailure(thrown) : error.failure;
|
|
112
132
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Read from the target, not the proxy. A getter (`try` itself) must run
|
|
117
|
-
// against the real client, not recurse through the proxy.
|
|
118
|
-
const value = Reflect.get(target, prop);
|
|
119
|
-
if (typeof value !== 'function')
|
|
120
|
-
return value;
|
|
121
|
-
const method = value;
|
|
122
|
-
const name = String(prop);
|
|
123
|
-
return (...args) => wrap(() => method.apply(target, args), name);
|
|
124
|
-
},
|
|
125
|
-
});
|
|
133
|
+
/** The failure a thrown value carries, or `null` for a bug to rethrow. */
|
|
134
|
+
function recognize(error) {
|
|
135
|
+
return isGhFailure(error) ? failureOf(error) : null;
|
|
126
136
|
}
|
|
127
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Wrap a synchronous client so every method returns a `Result<T, GhFailure>`.
|
|
139
|
+
* The proxy is command-seam's `tryMethods`, so a new method needs no entry.
|
|
140
|
+
*/
|
|
128
141
|
export function triedSync(client) {
|
|
129
|
-
return
|
|
130
|
-
try {
|
|
131
|
-
return ok(call());
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
if (isGhFailure(error))
|
|
135
|
-
return err(failureOf(error));
|
|
136
|
-
throw error;
|
|
137
|
-
}
|
|
138
|
-
});
|
|
142
|
+
return tryMethods(client, recognize);
|
|
139
143
|
}
|
|
140
144
|
/** Wrap an awaitable client so every method promises a `Result<T, GhFailure>`. */
|
|
141
145
|
export function triedAsync(client) {
|
|
142
|
-
return
|
|
143
|
-
try {
|
|
144
|
-
return ok(await call());
|
|
145
|
-
}
|
|
146
|
-
catch (error) {
|
|
147
|
-
if (isGhFailure(error))
|
|
148
|
-
return err(failureOf(error));
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
});
|
|
146
|
+
return tryMethodsAsync(client, recognize);
|
|
152
147
|
}
|
|
153
148
|
function checkSoft(name) {
|
|
154
149
|
if (!SOFT_METHODS.includes(name)) {
|
|
@@ -157,7 +152,7 @@ function checkSoft(name) {
|
|
|
157
152
|
}
|
|
158
153
|
/** Wrap a synchronous client so a failed query returns `null`. */
|
|
159
154
|
export function softSync(client) {
|
|
160
|
-
return
|
|
155
|
+
return methodFacade(client, (call, name) => {
|
|
161
156
|
checkSoft(name);
|
|
162
157
|
try {
|
|
163
158
|
return call();
|
|
@@ -171,7 +166,7 @@ export function softSync(client) {
|
|
|
171
166
|
}
|
|
172
167
|
/** Wrap an awaitable client so a failed query resolves to `null`. */
|
|
173
168
|
export function softAsync(client) {
|
|
174
|
-
return
|
|
169
|
+
return methodFacade(client, async (call, name) => {
|
|
175
170
|
checkSoft(name);
|
|
176
171
|
try {
|
|
177
172
|
return await call();
|
package/dist/failure.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* in English. A message that matches none stays the subprocess layer's `exit`
|
|
8
8
|
* with `gh`'s first stderr line, never a wrong kind.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { classifyFailed } from '@osdlc/command-seam';
|
|
11
11
|
import {} from '@osdlc/result';
|
|
12
12
|
const INSTALL_HINT = 'Install it from https://cli.github.com, then run `gh auth login`.';
|
|
13
13
|
const RATE_LIMITED = /API rate limit exceeded|secondary rate limit|rate limit exceeded|HTTP 429/i;
|
|
@@ -37,13 +37,8 @@ export function isAbsent(thing, text) {
|
|
|
37
37
|
export function classifyGhFailure(failed) {
|
|
38
38
|
// The client's runner may not name its binary. It is gh either way, so the
|
|
39
39
|
// messages say `gh` rather than "The command".
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
: failed.failure;
|
|
43
|
-
const base = { ...f, cause: failed };
|
|
44
|
-
if (f.kind === 'not-installed')
|
|
45
|
-
return { ...base, hint: INSTALL_HINT };
|
|
46
|
-
if (f.kind !== 'exit')
|
|
40
|
+
const base = classifyFailed(failed, { binary: 'gh', installHint: INSTALL_HINT });
|
|
41
|
+
if (base.kind !== 'exit')
|
|
47
42
|
return base;
|
|
48
43
|
const text = `${failed.result.stderr}\n${failed.result.stdout}`;
|
|
49
44
|
if (RATE_LIMITED.test(text)) {
|
package/dist/repo.d.ts
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* Reading the GitHub owner and repository from a git remote, and a pull
|
|
3
3
|
* request from a URL or what a person typed.
|
|
4
4
|
*
|
|
5
|
-
* `parseOwnerRepo`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* `parseOwnerRepo` is `@osdlc/typed-git`'s `parseRemoteRepo` without the
|
|
6
|
+
* host, so it reads every remote form typed-git reads (scp-style, a trailing
|
|
7
|
+
* `.git`, an `ssh://` port, a bare `owner/repo`, an Enterprise host) and no
|
|
8
|
+
* caller writes its own regex over `remote.origin.url`.
|
|
9
9
|
*/
|
|
10
10
|
import type { OwnerRepo } from './types.ts';
|
|
11
11
|
/**
|
|
12
12
|
* `{owner, repo}` from any git remote reference, or null when it names no
|
|
13
13
|
* project. Handles https, ssh, scp-style, and bare `owner/repo`.
|
|
14
|
+
*
|
|
15
|
+
* It is typed-git's `parseRemoteRepo` without the host: a clone URL on a host
|
|
16
|
+
* `parseGitUrl` does not know, such as a GitHub Enterprise host or another
|
|
17
|
+
* forge's `host/owner/repo`, is read host-agnostically. It does not check
|
|
18
|
+
* that the host is GitHub; read `parseRemoteRepo(url).host` for that.
|
|
14
19
|
*/
|
|
15
20
|
export declare function parseOwnerRepo(remoteUrl: string): OwnerRepo | null;
|
|
16
21
|
/** `owner/repo`, the form `gh api` paths and `--repo` both take. */
|
package/dist/repo.js
CHANGED
|
@@ -2,24 +2,24 @@
|
|
|
2
2
|
* Reading the GitHub owner and repository from a git remote, and a pull
|
|
3
3
|
* request from a URL or what a person typed.
|
|
4
4
|
*
|
|
5
|
-
* `parseOwnerRepo`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* `parseOwnerRepo` is `@osdlc/typed-git`'s `parseRemoteRepo` without the
|
|
6
|
+
* host, so it reads every remote form typed-git reads (scp-style, a trailing
|
|
7
|
+
* `.git`, an `ssh://` port, a bare `owner/repo`, an Enterprise host) and no
|
|
8
|
+
* caller writes its own regex over `remote.origin.url`.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { parseRemoteRepo } from '@osdlc/typed-git';
|
|
11
11
|
/**
|
|
12
12
|
* `{owner, repo}` from any git remote reference, or null when it names no
|
|
13
13
|
* project. Handles https, ssh, scp-style, and bare `owner/repo`.
|
|
14
|
+
*
|
|
15
|
+
* It is typed-git's `parseRemoteRepo` without the host: a clone URL on a host
|
|
16
|
+
* `parseGitUrl` does not know, such as a GitHub Enterprise host or another
|
|
17
|
+
* forge's `host/owner/repo`, is read host-agnostically. It does not check
|
|
18
|
+
* that the host is GitHub; read `parseRemoteRepo(url).host` for that.
|
|
14
19
|
*/
|
|
15
20
|
export function parseOwnerRepo(remoteUrl) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
return null;
|
|
19
|
-
const { user, project } = info;
|
|
20
|
-
if (!user || !project)
|
|
21
|
-
return null;
|
|
22
|
-
return { owner: user, repo: project };
|
|
21
|
+
const remote = parseRemoteRepo(remoteUrl);
|
|
22
|
+
return remote === null ? null : { owner: remote.owner, repo: remote.repo };
|
|
23
23
|
}
|
|
24
24
|
/** `owner/repo`, the form `gh api` paths and `--repo` both take. */
|
|
25
25
|
export function ownerRepoSlug(r) {
|
package/dist/sync.d.ts
CHANGED
|
@@ -22,11 +22,15 @@ import type { ApiOptions, ApiResponse, AuthStatus, GhTarget, IssueListOptions, L
|
|
|
22
22
|
import type { IssueRow, Label, OwnerRepo, PrSearchHit, PullRequest, Repository } from './types.ts';
|
|
23
23
|
/**
|
|
24
24
|
* Makes a runner for a binary. A CLI passes one factory to every place that
|
|
25
|
-
* runs a command. Real code uses
|
|
26
|
-
* records calls, so the CLI runs offline.
|
|
25
|
+
* runs a command. Real code uses {@link defaultRunnerFactory}. Tests pass a
|
|
26
|
+
* factory that records calls, so the CLI runs offline.
|
|
27
27
|
*/
|
|
28
28
|
export type RunnerFactory = (binary: string) => CommandRunner;
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* The default factory: real runners that use `spawnSync`. A bare name is
|
|
31
|
+
* found on absolute `PATH` entries only, so a file of that name in the
|
|
32
|
+
* working directory cannot run in its place.
|
|
33
|
+
*/
|
|
30
34
|
export declare const defaultRunnerFactory: RunnerFactory;
|
|
31
35
|
/** What a client is built with: the runner, and the repository and host it talks to. */
|
|
32
36
|
export interface GhOptions extends GhTarget {
|
|
@@ -126,6 +130,7 @@ export declare class Gh {
|
|
|
126
130
|
openPrFor(branch: string, opts?: {
|
|
127
131
|
limit?: number | undefined;
|
|
128
132
|
fields?: readonly string[] | undefined;
|
|
133
|
+
timeoutMs?: number | undefined;
|
|
129
134
|
}): PullRequest | null;
|
|
130
135
|
/**
|
|
131
136
|
* {@link prView}, asked again until GitHub has computed the merge state.
|
|
@@ -178,11 +183,18 @@ export declare class Gh {
|
|
|
178
183
|
issueView(number: number, opts?: {
|
|
179
184
|
timeoutMs?: number | undefined;
|
|
180
185
|
}): IssueRow | null;
|
|
181
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* An owner's repositories (up to `opts.limit`, default 100). Throws when gh
|
|
188
|
+
* fails. Each row's `owner` is in GitHub's casing, not as passed.
|
|
189
|
+
*/
|
|
182
190
|
repoList(owner: string, opts?: v.RepoListOptions & {
|
|
183
191
|
timeoutMs?: number | undefined;
|
|
184
192
|
}): ListResult<Repository>;
|
|
185
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* One repository: `repo`, else the client's repo, else the clone at `cwd`.
|
|
195
|
+
* Null when there is no such repository. `owner` and `name` are in
|
|
196
|
+
* GitHub's casing, not as passed.
|
|
197
|
+
*/
|
|
186
198
|
repoView(repo?: string | undefined, opts?: {
|
|
187
199
|
fields?: readonly string[] | undefined;
|
|
188
200
|
timeoutMs?: number | undefined;
|
package/dist/sync.js
CHANGED
|
@@ -15,13 +15,17 @@
|
|
|
15
15
|
* the failure as a `Result<T, GhFailure>`. `gh.soft.*` returns `null` for it.
|
|
16
16
|
* `raw` and `rawChecked` run any `gh` call.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { spawnRunner } from '@osdlc/command-seam';
|
|
19
19
|
import { runCheckedGh, softSync, triedSync } from "./doors.js";
|
|
20
20
|
import { runVerb } from "./run.js";
|
|
21
21
|
import * as v from "./verbs.js";
|
|
22
22
|
import { ownerRepoSlug, parseOwnerRepo, parsePrRef } from "./repo.js";
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The default factory: real runners that use `spawnSync`. A bare name is
|
|
25
|
+
* found on absolute `PATH` entries only, so a file of that name in the
|
|
26
|
+
* working directory cannot run in its place.
|
|
27
|
+
*/
|
|
28
|
+
export const defaultRunnerFactory = (binary) => spawnRunner(binary, { absolutePathOnly: true });
|
|
25
29
|
/** How long to wait between merge-state retries, in milliseconds. */
|
|
26
30
|
const MERGE_STATE_RETRY_DELAY_MS = 1_500;
|
|
27
31
|
/**
|
|
@@ -52,7 +56,7 @@ export class Gh {
|
|
|
52
56
|
this.repo = opts.repo;
|
|
53
57
|
this.hostname = opts.hostname;
|
|
54
58
|
this.target = { repo: opts.repo, hostname: opts.hostname };
|
|
55
|
-
this.runner =
|
|
59
|
+
this.runner = opts.runner ?? defaultRunnerFactory('gh');
|
|
56
60
|
}
|
|
57
61
|
/** The same client rooted at a different directory (same runner, repo and host). */
|
|
58
62
|
at(cwd) {
|
|
@@ -150,6 +154,7 @@ export class Gh {
|
|
|
150
154
|
head: branch,
|
|
151
155
|
limit: opts.limit ?? 5,
|
|
152
156
|
fields: opts.fields ?? v.PR_REF_FIELDS,
|
|
157
|
+
timeoutMs: opts.timeoutMs,
|
|
153
158
|
});
|
|
154
159
|
return v.pickPrForBranch(rows.rows, branch);
|
|
155
160
|
}
|
|
@@ -210,11 +215,18 @@ export class Gh {
|
|
|
210
215
|
issueView(number, opts = {}) {
|
|
211
216
|
return this.exec(v.issueView(number, this.target), opts.timeoutMs);
|
|
212
217
|
}
|
|
213
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* An owner's repositories (up to `opts.limit`, default 100). Throws when gh
|
|
220
|
+
* fails. Each row's `owner` is in GitHub's casing, not as passed.
|
|
221
|
+
*/
|
|
214
222
|
repoList(owner, opts = {}) {
|
|
215
223
|
return this.read(v.repoList(owner, opts, this.target), opts.timeoutMs);
|
|
216
224
|
}
|
|
217
|
-
/**
|
|
225
|
+
/**
|
|
226
|
+
* One repository: `repo`, else the client's repo, else the clone at `cwd`.
|
|
227
|
+
* Null when there is no such repository. `owner` and `name` are in
|
|
228
|
+
* GitHub's casing, not as passed.
|
|
229
|
+
*/
|
|
218
230
|
repoView(repo, opts = {}) {
|
|
219
231
|
return this.exec(v.repoView(repo, opts.fields ?? v.REPO_FIELDS, this.target), opts.timeoutMs);
|
|
220
232
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -156,9 +156,14 @@ export type RepositoryVisibility = 'PUBLIC' | 'PRIVATE' | 'INTERNAL' | (string &
|
|
|
156
156
|
* without one: a row without it is dropped.
|
|
157
157
|
*/
|
|
158
158
|
export interface Repository {
|
|
159
|
-
/** `owner/name
|
|
159
|
+
/** `owner/name`, in GitHub's casing. */
|
|
160
160
|
nameWithOwner: string;
|
|
161
|
+
/**
|
|
162
|
+
* Cut from `nameWithOwner`, so in GitHub's casing, not as the caller wrote
|
|
163
|
+
* it: `repoList('ACME')` gives `owner: 'acme'` when the account is `acme`.
|
|
164
|
+
*/
|
|
161
165
|
owner: string;
|
|
166
|
+
/** Cut from `nameWithOwner`, so in GitHub's casing. */
|
|
162
167
|
name: string;
|
|
163
168
|
description: string;
|
|
164
169
|
/** The default branch. Null when not asked for, or when the repository is empty. */
|
|
@@ -178,6 +183,7 @@ export interface PrSearchHit {
|
|
|
178
183
|
state: PullRequestState;
|
|
179
184
|
url: string;
|
|
180
185
|
isDraft: boolean;
|
|
186
|
+
/** Cut from the hit's `nameWithOwner`, so in GitHub's casing. */
|
|
181
187
|
repository: OwnerRepo | null;
|
|
182
188
|
author: Actor | null;
|
|
183
189
|
labels: Label[];
|
package/dist/verbs.d.ts
CHANGED
|
@@ -35,9 +35,9 @@ export interface GhVerb<T> {
|
|
|
35
35
|
env?: Record<string, string> | undefined;
|
|
36
36
|
/**
|
|
37
37
|
* True for a read whose failure is part of its answer. `installed`,
|
|
38
|
-
* `authStatus` and `api` return what `parse` makes of any result,
|
|
39
|
-
*
|
|
40
|
-
* non-zero.
|
|
38
|
+
* `authStatus` and `api` return what `parse` makes of any result, from the
|
|
39
|
+
* plain method and from `try`; they have no soft form. Other reads throw
|
|
40
|
+
* from the plain method when gh exits non-zero.
|
|
41
41
|
*/
|
|
42
42
|
probe?: boolean | undefined;
|
|
43
43
|
/**
|
|
@@ -94,7 +94,10 @@ export declare const PR_VIEW_FIELDS: readonly ["number", "state", "title", "url"
|
|
|
94
94
|
/**
|
|
95
95
|
* A list query's answer: the rows, and whether `limit` cut the list short.
|
|
96
96
|
* The verb asks gh for one row more than `limit`. `truncated` is true when
|
|
97
|
-
* that row came back, and `rows` holds at most `limit`.
|
|
97
|
+
* that row came back, and `rows` holds at most `limit`. A query that goes
|
|
98
|
+
* through GitHub search, which returns at most 1000 results, is also
|
|
99
|
+
* `truncated` when 1000 rows came back: `prList` with a non-empty `search` or
|
|
100
|
+
* an `author`, `issueList` with a `label`, and `searchPrs`.
|
|
98
101
|
*/
|
|
99
102
|
export interface ListResult<T> {
|
|
100
103
|
rows: T[];
|
|
@@ -223,8 +226,10 @@ export interface GraphqlOptions {
|
|
|
223
226
|
* connection across the pages with {@link nodesAt}.
|
|
224
227
|
*
|
|
225
228
|
* A string variable of exactly `{owner}` or `{repo}` is filled from the
|
|
226
|
-
* target's repo, or by gh from the clone. A mutation
|
|
227
|
-
* `RangeError
|
|
229
|
+
* target's repo, or by gh from the clone. A document holding any mutation or
|
|
230
|
+
* subscription throws `RangeError`, as does a variable named `query` or
|
|
231
|
+
* `operationName`, which gh would send as the request's own fields. This is a
|
|
232
|
+
* read, and changes go through `raw`.
|
|
228
233
|
*
|
|
229
234
|
* @experimental
|
|
230
235
|
*/
|
|
@@ -284,12 +289,15 @@ export interface RepoListOptions {
|
|
|
284
289
|
}
|
|
285
290
|
/**
|
|
286
291
|
* `gh repo list <owner> --json …`: an owner's repositories. The host comes
|
|
287
|
-
* from the target (`GH_HOST`). The target's repo is not used.
|
|
292
|
+
* from the target (`GH_HOST`). The target's repo is not used. Each row's
|
|
293
|
+
* `owner` is in GitHub's casing, not as passed. To build paths that match
|
|
294
|
+
* your config, use your own value.
|
|
288
295
|
*/
|
|
289
296
|
export declare function repoList(owner: string, opts?: RepoListOptions, target?: GhTarget): GhVerb<ListResult<Repository> | null>;
|
|
290
297
|
/**
|
|
291
298
|
* `gh repo view [<[host/]owner/name>] --json …`: one repository. That is `repo`,
|
|
292
|
-
* else the target's repo, else the clone at the client's cwd.
|
|
299
|
+
* else the target's repo, else the clone at the client's cwd. The result's
|
|
300
|
+
* `owner` and `name` are in GitHub's casing, not as passed.
|
|
293
301
|
*/
|
|
294
302
|
export declare function repoView(repo?: string | undefined, fields?: readonly string[], target?: GhTarget): GhVerb<Repository | null>;
|
|
295
303
|
/** What to narrow a pull request search to. Every filter is optional. */
|
package/dist/verbs.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* `settle` (see `doors.ts`) instead. There a failure throws, except where the
|
|
14
14
|
* verb declares `absent` or is a `probe`.
|
|
15
15
|
*/
|
|
16
|
-
import { stderrOrError } from '@osdlc/command-seam';
|
|
16
|
+
import { notAnOption, stderrOrError } from '@osdlc/command-seam';
|
|
17
17
|
import { parseLabels, parsePrSearchHit, parsePullRequest, parseRepository } from "./parse.js";
|
|
18
18
|
import { isAbsent } from "./failure.js";
|
|
19
19
|
import { isRepoSlug, parsePrUrl } from "./repo.js";
|
|
@@ -48,16 +48,26 @@ export function checkTarget(target) {
|
|
|
48
48
|
throw new RangeError(`hostname must be a bare host, got ${JSON.stringify(hostname)}`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
/**
|
|
52
|
-
|
|
51
|
+
/**
|
|
52
|
+
* `[host/]owner/name` for `repo` (else the target's repo), the form `--repo`
|
|
53
|
+
* and `gh repo view` take, or `undefined` when neither names one. The host is
|
|
54
|
+
* the target's, through {@link hostOf}, so a hostname of `''` is "not set".
|
|
55
|
+
* Both are checked first.
|
|
56
|
+
*/
|
|
57
|
+
function qualifiedRepo(target, repo) {
|
|
53
58
|
checkTarget(target);
|
|
54
|
-
const slug = repo ?? target?.repo;
|
|
55
|
-
if (slug === undefined)
|
|
56
|
-
return [];
|
|
57
59
|
if (repo !== undefined)
|
|
58
60
|
checkTarget({ repo });
|
|
61
|
+
const slug = repo ?? target?.repo;
|
|
62
|
+
if (slug === undefined)
|
|
63
|
+
return undefined;
|
|
59
64
|
const hostname = hostOf(target);
|
|
60
|
-
return
|
|
65
|
+
return hostname === undefined ? slug : `${hostname}/${slug}`;
|
|
66
|
+
}
|
|
67
|
+
/** `--repo [host/]owner/name` for `pr`, `issue` and `label` verbs. `repo` overrides the target's. */
|
|
68
|
+
function repoArgs(target, repo) {
|
|
69
|
+
const qualified = qualifiedRepo(target, repo);
|
|
70
|
+
return qualified === undefined ? [] : ['--repo', qualified];
|
|
61
71
|
}
|
|
62
72
|
/** `--hostname <host>` for `api` and `auth` verbs. */
|
|
63
73
|
function hostArgs(target) {
|
|
@@ -144,24 +154,7 @@ function oneOrNull(r) {
|
|
|
144
154
|
* failed. A caller must be able to tell "no PRs" from "couldn't ask".
|
|
145
155
|
*/
|
|
146
156
|
function manyOrNull(r) {
|
|
147
|
-
|
|
148
|
-
return null;
|
|
149
|
-
let data;
|
|
150
|
-
try {
|
|
151
|
-
data = JSON.parse(r.stdout || '[]');
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
if (!Array.isArray(data))
|
|
157
|
-
return null;
|
|
158
|
-
const rows = [];
|
|
159
|
-
for (const item of data) {
|
|
160
|
-
const pr = parsePullRequest(item);
|
|
161
|
-
if (pr !== null)
|
|
162
|
-
rows.push(pr);
|
|
163
|
-
}
|
|
164
|
-
return rows;
|
|
157
|
+
return rowsOrNull(r, (item) => parsePullRequest(item));
|
|
165
158
|
}
|
|
166
159
|
/** Throw `RangeError` unless `limit` is a positive integer. */
|
|
167
160
|
function checkLimit(limit) {
|
|
@@ -169,11 +162,23 @@ function checkLimit(limit) {
|
|
|
169
162
|
throw new RangeError(`limit must be a positive integer, got ${limit}`);
|
|
170
163
|
}
|
|
171
164
|
}
|
|
172
|
-
/**
|
|
173
|
-
|
|
165
|
+
/**
|
|
166
|
+
* The most results GitHub search returns for one query. A call that goes
|
|
167
|
+
* through search and gets this many rows may have been cut off by search,
|
|
168
|
+
* whatever the `limit`. gh uses search for `pr list` with a non-empty
|
|
169
|
+
* `--search` or with `--author`, for `issue list` with `--label`, and for
|
|
170
|
+
* `search prs`.
|
|
171
|
+
*/
|
|
172
|
+
const SEARCH_RESULT_CAP = 1000;
|
|
173
|
+
/**
|
|
174
|
+
* `rows` cut to `limit`, with whether there were more. With `cap`, reaching
|
|
175
|
+
* it counts as more too, since the source stops there. Null stays null.
|
|
176
|
+
*/
|
|
177
|
+
function limited(rows, limit, cap) {
|
|
174
178
|
if (rows === null)
|
|
175
179
|
return null;
|
|
176
|
-
|
|
180
|
+
const truncated = rows.length > limit || (cap !== undefined && rows.length >= cap);
|
|
181
|
+
return { rows: rows.slice(0, limit), truncated };
|
|
177
182
|
}
|
|
178
183
|
/** A mutation's result carries no value. */
|
|
179
184
|
function nothing() { }
|
|
@@ -232,24 +237,7 @@ function oneIssueOrNull(r) {
|
|
|
232
237
|
* on failure, as {@link manyOrNull} does for PRs.
|
|
233
238
|
*/
|
|
234
239
|
function manyIssuesOrNull(r) {
|
|
235
|
-
|
|
236
|
-
return null;
|
|
237
|
-
let data;
|
|
238
|
-
try {
|
|
239
|
-
data = JSON.parse(r.stdout || '[]');
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
if (!Array.isArray(data))
|
|
245
|
-
return null;
|
|
246
|
-
const rows = [];
|
|
247
|
-
for (const item of data) {
|
|
248
|
-
const row = parseIssueRow(item);
|
|
249
|
-
if (row !== null)
|
|
250
|
-
rows.push(row);
|
|
251
|
-
}
|
|
252
|
-
return rows;
|
|
240
|
+
return rowsOrNull(r, parseIssueRow);
|
|
253
241
|
}
|
|
254
242
|
/** `gh pr list …`: open PRs by default. `opts.repo` overrides the target's repo. */
|
|
255
243
|
export function prList(opts = {}, target) {
|
|
@@ -278,7 +266,8 @@ export function prList(opts = {}, target) {
|
|
|
278
266
|
checked: false,
|
|
279
267
|
readOnly: true,
|
|
280
268
|
parse(r) {
|
|
281
|
-
|
|
269
|
+
// A search stops at 1000 results, so 1000 rows may be a cut, not the end.
|
|
270
|
+
const result = limited(manyOrNull(r), limit, (opts.search ?? '') !== '' || opts.author !== undefined ? SEARCH_RESULT_CAP : undefined);
|
|
282
271
|
if (result === null || head === undefined)
|
|
283
272
|
return result;
|
|
284
273
|
return { ...result, rows: result.rows.filter((pr) => pr.headRefName === head) };
|
|
@@ -294,9 +283,8 @@ function withFields(fields, required) {
|
|
|
294
283
|
* argument would read as a flag.
|
|
295
284
|
*/
|
|
296
285
|
function notAFlag(what, value) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
286
|
+
const message = (v) => `${what} must not start with "-", got ${JSON.stringify(v)}`;
|
|
287
|
+
notAnOption(value, (v) => new RangeError(message(v)));
|
|
300
288
|
}
|
|
301
289
|
/**
|
|
302
290
|
* `gh pr view <number|branch|url> --json …`: one PR, with the full field set
|
|
@@ -313,15 +301,14 @@ export function prView(ref, fields = PR_VIEW_FIELDS, target) {
|
|
|
313
301
|
// omits it would read as "no such PR". Always ask for it, and each field once.
|
|
314
302
|
const asked = withFields(fields, ['number']);
|
|
315
303
|
const isUrl = typeof ref === 'string' && parsePrUrl(ref) !== null;
|
|
304
|
+
// gh reads a selector as a URL only with an http(s) scheme; a scheme-less
|
|
305
|
+
// one (`github.com/o/r/pull/7`, which parsePrUrl accepts) would be taken as
|
|
306
|
+
// a branch name.
|
|
307
|
+
const selector = typeof ref === 'string' && isUrl && !/^https?:\/\//i.test(ref.trim())
|
|
308
|
+
? `https://${ref.trim()}`
|
|
309
|
+
: String(ref);
|
|
316
310
|
return {
|
|
317
|
-
argv: [
|
|
318
|
-
'pr',
|
|
319
|
-
'view',
|
|
320
|
-
String(ref),
|
|
321
|
-
...(isUrl ? [] : repoArgs(target)),
|
|
322
|
-
'--json',
|
|
323
|
-
asked.join(','),
|
|
324
|
-
],
|
|
311
|
+
argv: ['pr', 'view', selector, ...(isUrl ? [] : repoArgs(target)), '--json', asked.join(',')],
|
|
325
312
|
checked: false,
|
|
326
313
|
readOnly: true,
|
|
327
314
|
absent: noSuch('pr'),
|
|
@@ -392,6 +379,27 @@ function flattenPages(body) {
|
|
|
392
379
|
return body;
|
|
393
380
|
return body.flat();
|
|
394
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* True when any operation in a GraphQL document is a `mutation` or a
|
|
384
|
+
* `subscription`. Strings and comments are dropped first, and only the words
|
|
385
|
+
* outside every `{…}` and `(…)` are read, which is where an operation's
|
|
386
|
+
* keyword stands. So a leading fragment, a leading `,` (an ignored token) or a
|
|
387
|
+
* second operation after a query cannot hide one.
|
|
388
|
+
*/
|
|
389
|
+
function writesOrStreams(document) {
|
|
390
|
+
const code = document.replace(/"""[\s\S]*?"""|"(?:[^"\\\n]|\\.)*"|#[^\n]*/g, ' ');
|
|
391
|
+
let depth = 0;
|
|
392
|
+
for (const token of code.matchAll(/[{}()]|[_A-Za-z][_0-9A-Za-z]*/g)) {
|
|
393
|
+
const t = token[0];
|
|
394
|
+
if (t === '{' || t === '(')
|
|
395
|
+
depth += 1;
|
|
396
|
+
else if (t === '}' || t === ')')
|
|
397
|
+
depth = Math.max(0, depth - 1);
|
|
398
|
+
else if (depth === 0 && (t === 'mutation' || t === 'subscription'))
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
395
403
|
/**
|
|
396
404
|
* `gh api graphql -f query=… [--paginate --slurp]`: a GraphQL query.
|
|
397
405
|
*
|
|
@@ -401,15 +409,16 @@ function flattenPages(body) {
|
|
|
401
409
|
* connection across the pages with {@link nodesAt}.
|
|
402
410
|
*
|
|
403
411
|
* A string variable of exactly `{owner}` or `{repo}` is filled from the
|
|
404
|
-
* target's repo, or by gh from the clone. A mutation
|
|
405
|
-
* `RangeError
|
|
412
|
+
* target's repo, or by gh from the clone. A document holding any mutation or
|
|
413
|
+
* subscription throws `RangeError`, as does a variable named `query` or
|
|
414
|
+
* `operationName`, which gh would send as the request's own fields. This is a
|
|
415
|
+
* read, and changes go through `raw`.
|
|
406
416
|
*
|
|
407
417
|
* @experimental
|
|
408
418
|
*/
|
|
409
419
|
export function graphql(query, variables = {}, opts = {}, target) {
|
|
410
420
|
checkTarget(target);
|
|
411
|
-
|
|
412
|
-
if (/^mutation\b/.test(body) || /^subscription\b/.test(body)) {
|
|
421
|
+
if (writesOrStreams(query)) {
|
|
413
422
|
throw new RangeError('graphql: only queries are allowed; send a mutation through raw');
|
|
414
423
|
}
|
|
415
424
|
const paginate = opts.paginate === true;
|
|
@@ -421,7 +430,10 @@ export function graphql(query, variables = {}, opts = {}, target) {
|
|
|
421
430
|
argv.push('--paginate', '--slurp');
|
|
422
431
|
const [owner, name] = target?.repo?.split('/') ?? [];
|
|
423
432
|
for (const [key, value] of Object.entries(variables)) {
|
|
424
|
-
|
|
433
|
+
// gh lifts `query` and `operationName` out of the variables into the
|
|
434
|
+
// request itself, so either would replace the checked document or pick
|
|
435
|
+
// another operation in it.
|
|
436
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || key === 'query' || key === 'operationName') {
|
|
425
437
|
throw new RangeError(`graphql: bad variable name ${JSON.stringify(key)}`);
|
|
426
438
|
}
|
|
427
439
|
if (typeof value === 'string') {
|
|
@@ -605,7 +617,14 @@ export function issueList(opts = {}, target) {
|
|
|
605
617
|
if (opts.label !== undefined)
|
|
606
618
|
argv.push('--label', opts.label);
|
|
607
619
|
argv.push('--json', ISSUE_FIELDS, '--limit', String(limit + 1));
|
|
608
|
-
|
|
620
|
+
// gh lists issues by label through search, which stops at 1000 results.
|
|
621
|
+
const cap = opts.label === undefined ? undefined : SEARCH_RESULT_CAP;
|
|
622
|
+
return {
|
|
623
|
+
argv,
|
|
624
|
+
checked: false,
|
|
625
|
+
readOnly: true,
|
|
626
|
+
parse: (r) => limited(manyIssuesOrNull(r), limit, cap),
|
|
627
|
+
};
|
|
609
628
|
}
|
|
610
629
|
/** `gh issue view <n> --json …`: one issue, same field set as {@link issueList}. */
|
|
611
630
|
export function issueView(number, target) {
|
|
@@ -657,7 +676,9 @@ function rowsOrNull(r, parse) {
|
|
|
657
676
|
}
|
|
658
677
|
/**
|
|
659
678
|
* `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.
|
|
679
|
+
* from the target (`GH_HOST`). The target's repo is not used. Each row's
|
|
680
|
+
* `owner` is in GitHub's casing, not as passed. To build paths that match
|
|
681
|
+
* your config, use your own value.
|
|
661
682
|
*/
|
|
662
683
|
export function repoList(owner, opts = {}, target) {
|
|
663
684
|
notAFlag('repoList owner', owner);
|
|
@@ -682,17 +703,15 @@ export function repoList(owner, opts = {}, target) {
|
|
|
682
703
|
}
|
|
683
704
|
/**
|
|
684
705
|
* `gh repo view [<[host/]owner/name>] --json …`: one repository. That is `repo`,
|
|
685
|
-
* else the target's repo, else the clone at the client's cwd.
|
|
706
|
+
* else the target's repo, else the clone at the client's cwd. The result's
|
|
707
|
+
* `owner` and `name` are in GitHub's casing, not as passed.
|
|
686
708
|
*/
|
|
687
709
|
export function repoView(repo, fields = REPO_FIELDS, target) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
checkTarget({ repo });
|
|
691
|
-
const slug = repo ?? target?.repo;
|
|
710
|
+
// The same `[host/]owner/name` rule as `--repo`.
|
|
711
|
+
const qualified = qualifiedRepo(target, repo);
|
|
692
712
|
const argv = ['repo', 'view'];
|
|
693
|
-
if (
|
|
694
|
-
argv.push(
|
|
695
|
-
}
|
|
713
|
+
if (qualified !== undefined)
|
|
714
|
+
argv.push(qualified);
|
|
696
715
|
argv.push('--json', withFields(fields, ['nameWithOwner']).join(','));
|
|
697
716
|
return {
|
|
698
717
|
argv,
|
|
@@ -734,7 +753,9 @@ export function searchPrs(query, opts = {}, target) {
|
|
|
734
753
|
else if (opts.state !== undefined)
|
|
735
754
|
argv.push('--state', opts.state);
|
|
736
755
|
argv.push('--json', withFields(opts.fields ?? SEARCH_PR_FIELDS, ['number']).join(','));
|
|
737
|
-
|
|
756
|
+
// gh refuses a search limit over 1000, so ask for at most that. 1000 rows
|
|
757
|
+
// then read as truncated: search may hold more.
|
|
758
|
+
argv.push('--limit', String(Math.min(limit + 1, SEARCH_RESULT_CAP)));
|
|
738
759
|
if (query !== '')
|
|
739
760
|
argv.push('--', query);
|
|
740
761
|
return {
|
|
@@ -742,7 +763,7 @@ export function searchPrs(query, opts = {}, target) {
|
|
|
742
763
|
checked: false,
|
|
743
764
|
readOnly: true,
|
|
744
765
|
env: hostEnv(target),
|
|
745
|
-
parse: (r) => limited(rowsOrNull(r, parsePrSearchHit), limit),
|
|
766
|
+
parse: (r) => limited(rowsOrNull(r, parsePrSearchHit), limit, SEARCH_RESULT_CAP),
|
|
746
767
|
};
|
|
747
768
|
}
|
|
748
769
|
/**
|
|
@@ -906,6 +927,7 @@ export function prReviewThreads(number, target) {
|
|
|
906
927
|
* and description are set to these instead of failing.
|
|
907
928
|
*/
|
|
908
929
|
export function createLabel(opts, target) {
|
|
930
|
+
notAFlag('createLabel name', opts.name);
|
|
909
931
|
const argv = [
|
|
910
932
|
'label',
|
|
911
933
|
'create',
|
|
@@ -921,6 +943,7 @@ export function createLabel(opts, target) {
|
|
|
921
943
|
}
|
|
922
944
|
/** `gh label edit <name>`: change the color or description. A flag not given is left out. */
|
|
923
945
|
export function editLabel(name, opts, target) {
|
|
946
|
+
notAFlag('editLabel name', name);
|
|
924
947
|
const argv = ['label', 'edit', name, ...repoArgs(target)];
|
|
925
948
|
if (opts.color !== undefined)
|
|
926
949
|
argv.push('--color', opts.color);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osdlc/typed-gh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
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.
|
|
25
|
+
"@osdlc/command-seam": "^0.14.0",
|
|
26
|
+
"@osdlc/typed-git": "^0.10.0",
|
|
27
27
|
"@osdlc/result": "^0.1.0"
|
|
28
28
|
},
|
|
29
29
|
"engines": {
|