@osdlc/typed-gh 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +168 -36
- package/async.ts +131 -37
- package/dist/async.d.ts +43 -15
- package/dist/async.d.ts.map +1 -1
- package/dist/async.js +92 -30
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/parse.d.ts +5 -1
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +48 -0
- package/dist/repo.d.ts +42 -10
- package/dist/repo.d.ts.map +1 -1
- package/dist/repo.js +76 -12
- package/dist/sync.d.ts +64 -12
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +107 -29
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/verbs.d.ts +194 -41
- package/dist/verbs.d.ts.map +1 -1
- package/dist/verbs.js +482 -83
- package/index.ts +26 -3
- package/package.json +3 -3
- package/parse.ts +50 -0
- package/repo.ts +87 -13
- package/sync.ts +146 -34
- package/types.ts +40 -0
- package/verbs.ts +606 -81
package/index.ts
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* Authentication is `gh`'s. No token is read, stored, or forwarded.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
export { AsyncGh, type AsyncCallOptions } from './async.ts'
|
|
20
|
+
export { AsyncGh, type AsyncCallOptions, type AsyncGhOptions } from './async.ts'
|
|
21
21
|
|
|
22
|
-
export { Gh, defaultRunnerFactory, type RunnerFactory } from './sync.ts'
|
|
22
|
+
export { Gh, defaultRunnerFactory, type GhOptions, type RunnerFactory } from './sync.ts'
|
|
23
23
|
|
|
24
24
|
export {
|
|
25
25
|
api,
|
|
@@ -27,6 +27,7 @@ export {
|
|
|
27
27
|
createPr,
|
|
28
28
|
editPr,
|
|
29
29
|
editPrBody,
|
|
30
|
+
graphql,
|
|
30
31
|
createLabel,
|
|
31
32
|
createReview,
|
|
32
33
|
editLabel,
|
|
@@ -34,6 +35,7 @@ export {
|
|
|
34
35
|
issueList,
|
|
35
36
|
issueView,
|
|
36
37
|
labelList,
|
|
38
|
+
nodesAt,
|
|
37
39
|
openPrsForBranch,
|
|
38
40
|
pickPrForBranch,
|
|
39
41
|
prBody,
|
|
@@ -46,27 +48,45 @@ export {
|
|
|
46
48
|
prReviewThreads,
|
|
47
49
|
prUpdateBranch,
|
|
48
50
|
prView,
|
|
51
|
+
repoList,
|
|
52
|
+
repoView,
|
|
53
|
+
searchPrs,
|
|
49
54
|
setIssueLabel,
|
|
50
55
|
PR_LIST_FIELDS,
|
|
51
56
|
PR_REF_FIELDS,
|
|
52
57
|
PR_VIEW_FIELDS,
|
|
58
|
+
REPO_FIELDS,
|
|
59
|
+
SEARCH_PR_FIELDS,
|
|
53
60
|
type ApiOptions,
|
|
54
61
|
type ApiResponse,
|
|
55
62
|
type AuthStatus,
|
|
56
63
|
type CreateLabelOptions,
|
|
57
64
|
type CreatePrOptions,
|
|
58
65
|
type CreateReviewOptions,
|
|
66
|
+
type GhTarget,
|
|
67
|
+
type GraphqlOptions,
|
|
68
|
+
type GraphqlVariable,
|
|
59
69
|
type GhVerb,
|
|
60
70
|
type IssueListOptions,
|
|
71
|
+
type ListResult,
|
|
61
72
|
type PrListOptions,
|
|
62
73
|
type PrMergeMethod,
|
|
74
|
+
type RepoListOptions,
|
|
63
75
|
type ReviewComment,
|
|
64
76
|
type ReviewThread,
|
|
77
|
+
type SearchPrsOptions,
|
|
65
78
|
} from './verbs.ts'
|
|
66
79
|
|
|
67
80
|
export { anyCheckFailed, normalizeAuthorLogin, parsePullRequest } from './parse.ts'
|
|
68
81
|
|
|
69
|
-
export {
|
|
82
|
+
export {
|
|
83
|
+
ownerRepoSlug,
|
|
84
|
+
parseOwnerRepo,
|
|
85
|
+
parsePrRef,
|
|
86
|
+
parsePrUrl,
|
|
87
|
+
type PrRef,
|
|
88
|
+
type PullRequestRef,
|
|
89
|
+
} from './repo.ts'
|
|
70
90
|
|
|
71
91
|
export type {
|
|
72
92
|
Actor,
|
|
@@ -79,7 +99,10 @@ export type {
|
|
|
79
99
|
MergeStateStatus,
|
|
80
100
|
OwnerRepo,
|
|
81
101
|
PullRequest,
|
|
102
|
+
PrSearchHit,
|
|
82
103
|
PullRequestState,
|
|
104
|
+
Repository,
|
|
105
|
+
RepositoryVisibility,
|
|
83
106
|
Review,
|
|
84
107
|
ReviewDecision,
|
|
85
108
|
ReviewState,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osdlc/typed-gh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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
|
"type": "module",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"LICENSE-APACHE"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@osdlc/
|
|
33
|
-
"@osdlc/
|
|
32
|
+
"@osdlc/typed-git": "^0.5.3",
|
|
33
|
+
"@osdlc/command-seam": "^0.8.0"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=22"
|
package/parse.ts
CHANGED
|
@@ -25,7 +25,9 @@ import type {
|
|
|
25
25
|
Comment,
|
|
26
26
|
Label,
|
|
27
27
|
MergeableState,
|
|
28
|
+
PrSearchHit,
|
|
28
29
|
PullRequest,
|
|
30
|
+
Repository,
|
|
29
31
|
Review,
|
|
30
32
|
ReviewDecision,
|
|
31
33
|
} from './types.ts'
|
|
@@ -250,3 +252,51 @@ export function parsePullRequest(raw: unknown): PullRequest | null {
|
|
|
250
252
|
updatedAt: strOrNull(raw['updatedAt'] ?? raw['updated_at']),
|
|
251
253
|
}
|
|
252
254
|
}
|
|
255
|
+
|
|
256
|
+
// ── repositories and search ─────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
/** A `gh repo list`/`view --json` row as a {@link Repository}, or null without `nameWithOwner`. */
|
|
259
|
+
export function parseRepository(raw: unknown): Repository | null {
|
|
260
|
+
if (!isRecord(raw)) return null
|
|
261
|
+
const nameWithOwner = str(raw['nameWithOwner'])
|
|
262
|
+
const slash = nameWithOwner.indexOf('/')
|
|
263
|
+
if (slash <= 0 || slash === nameWithOwner.length - 1) return null
|
|
264
|
+
const branch = raw['defaultBranchRef']
|
|
265
|
+
return {
|
|
266
|
+
nameWithOwner,
|
|
267
|
+
owner: nameWithOwner.slice(0, slash),
|
|
268
|
+
name: nameWithOwner.slice(slash + 1),
|
|
269
|
+
description: str(raw['description']),
|
|
270
|
+
defaultBranch: isRecord(branch) ? strOrNull(branch['name']) : null,
|
|
271
|
+
visibility: strOrNull(raw['visibility']),
|
|
272
|
+
isArchived: bool(raw['isArchived']),
|
|
273
|
+
url: str(raw['url']),
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** A `gh search prs --json` row as a {@link PrSearchHit}, or null without a `number`. */
|
|
278
|
+
export function parsePrSearchHit(raw: unknown): PrSearchHit | null {
|
|
279
|
+
if (!isRecord(raw)) return null
|
|
280
|
+
const number = raw['number']
|
|
281
|
+
if (typeof number !== 'number') return null
|
|
282
|
+
const repo = raw['repository']
|
|
283
|
+
const slug = isRecord(repo) ? str(repo['nameWithOwner']) : ''
|
|
284
|
+
const slash = slug.indexOf('/')
|
|
285
|
+
return {
|
|
286
|
+
number,
|
|
287
|
+
title: str(raw['title']),
|
|
288
|
+
// Search answers in lower case; `PullRequest.state` is upper case.
|
|
289
|
+
state: str(raw['state']).toUpperCase(),
|
|
290
|
+
url: str(raw['url']),
|
|
291
|
+
isDraft: bool(raw['isDraft']),
|
|
292
|
+
repository:
|
|
293
|
+
slash > 0 && slash < slug.length - 1
|
|
294
|
+
? { owner: slug.slice(0, slash), repo: slug.slice(slash + 1) }
|
|
295
|
+
: null,
|
|
296
|
+
author: parseActor(raw['author']),
|
|
297
|
+
labels: parseLabels(raw['labels']),
|
|
298
|
+
createdAt: strOrNull(raw['createdAt']),
|
|
299
|
+
updatedAt: strOrNull(raw['updatedAt']),
|
|
300
|
+
closedAt: strOrNull(raw['closedAt']),
|
|
301
|
+
}
|
|
302
|
+
}
|
package/repo.ts
CHANGED
|
@@ -29,31 +29,105 @@ export function ownerRepoSlug(r: OwnerRepo): string {
|
|
|
29
29
|
return `${r.owner}/${r.repo}`
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** A pull request coordinate: the repository and the PR's number within it. */
|
|
32
|
+
/** A pull request coordinate: the host, the repository and the PR's number within it. */
|
|
33
33
|
export interface PullRequestRef extends OwnerRepo {
|
|
34
|
+
/** The GitHub host, lower-cased: `github.com`, or an Enterprise host. */
|
|
35
|
+
hostname: string
|
|
34
36
|
number: number
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
|
-
* The `{owner, repo, number}` a GitHub pull-request URL names, or
|
|
39
|
-
* `url` is not one.
|
|
40
|
+
* The `{hostname, owner, repo, number}` a GitHub pull-request URL names, or
|
|
41
|
+
* null when `url` is not one.
|
|
40
42
|
*
|
|
41
|
-
* Accepts `https
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* Accepts `https://<host>/<owner>/<repo>/pull/<n>` with or without a scheme,
|
|
44
|
+
* a trailing slash, a sub-page (`/files`, `/commits`), a query string or a
|
|
45
|
+
* fragment — every shape a browser's address bar hands over. The host is
|
|
46
|
+
* `github.com` (a `www.` prefix is dropped) or a GitHub Enterprise host such
|
|
47
|
+
* as `github.acme.com` or `acme.ghe.com`; without a scheme the host must
|
|
48
|
+
* contain a dot. `parseGitUrl` is the wrong tool here: it reads `/pull/<n>`
|
|
45
49
|
* as part of the project path.
|
|
46
|
-
*
|
|
47
|
-
* GitHub.com only. A GitHub Enterprise host is a different coordinate space
|
|
48
|
-
* (the `gh` CLI needs `--hostname`), so it is not silently accepted.
|
|
49
50
|
*/
|
|
50
51
|
export function parsePrUrl(url: string): PullRequestRef | null {
|
|
51
52
|
const m =
|
|
52
|
-
/^(?:https
|
|
53
|
+
/^(?:(https?:)\/\/)?([a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::\d+)?)\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)(?:[/?#].*)?$/i.exec(
|
|
53
54
|
url.trim(),
|
|
54
55
|
)
|
|
55
56
|
if (m === null) return null
|
|
56
|
-
const
|
|
57
|
+
const host = m[2]!.toLowerCase()
|
|
58
|
+
if (m[1] === undefined && !host.includes('.')) return null
|
|
59
|
+
const number = Number(m[5])
|
|
57
60
|
if (!Number.isSafeInteger(number) || number <= 0) return null
|
|
58
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
hostname: host === 'www.github.com' ? 'github.com' : host,
|
|
63
|
+
owner: m[3]!,
|
|
64
|
+
repo: m[4]!.replace(/\.git$/, ''),
|
|
65
|
+
number,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* What a person typed to name a pull request, read by {@link parsePrRef}.
|
|
71
|
+
*
|
|
72
|
+
* - `number`: `42`, `#42`, or `owner/name#42` (then `repo` is set). For bare
|
|
73
|
+
* digits, `orBranch` holds the same text, since a branch can be named `42`.
|
|
74
|
+
* - `url`: a pull request URL, with what {@link parsePrUrl} read from it.
|
|
75
|
+
* - `branch`: any other text that is a valid branch name.
|
|
76
|
+
*/
|
|
77
|
+
export type PrRef =
|
|
78
|
+
| { kind: 'number'; number: number; repo: OwnerRepo | null; orBranch: string | null }
|
|
79
|
+
| ({ kind: 'url'; url: string } & PullRequestRef)
|
|
80
|
+
| { kind: 'branch'; name: string }
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read what a person typed to name a pull request: a number, `#42`,
|
|
84
|
+
* `owner/name#42`, a URL, or a branch name. Null when it is none of these.
|
|
85
|
+
*
|
|
86
|
+
* Pure: it starts no process. Digits are read as a number first, the way
|
|
87
|
+
* `gh pr view 42` reads them; `orBranch` keeps the branch reading. The
|
|
88
|
+
* clients' `prFind` tries both.
|
|
89
|
+
*/
|
|
90
|
+
export function parsePrRef(input: string): PrRef | null {
|
|
91
|
+
const text = input.trim()
|
|
92
|
+
const digits = /^(#?)(\d+)$/.exec(text)
|
|
93
|
+
if (digits !== null) {
|
|
94
|
+
const number = Number(digits[2])
|
|
95
|
+
if (Number.isSafeInteger(number) && number > 0) {
|
|
96
|
+
return { kind: 'number', number, repo: null, orBranch: digits[1] === '' ? text : null }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const qualified = /^([^/\s#]+)\/([^/\s#]+)#(\d+)$/.exec(text)
|
|
100
|
+
if (qualified !== null) {
|
|
101
|
+
const number = Number(qualified[3])
|
|
102
|
+
if (!Number.isSafeInteger(number) || number <= 0) return null
|
|
103
|
+
if (!isRepoSlug(`${qualified[1]}/${qualified[2]}`)) return null
|
|
104
|
+
return {
|
|
105
|
+
kind: 'number',
|
|
106
|
+
number,
|
|
107
|
+
repo: { owner: qualified[1]!, repo: qualified[2]! },
|
|
108
|
+
orBranch: null,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const url = parsePrUrl(text)
|
|
112
|
+
if (url !== null) return { kind: 'url', url: text, ...url }
|
|
113
|
+
return isBranchName(text) ? { kind: 'branch', name: text } : null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** True for `owner/name` as GitHub spells it. */
|
|
117
|
+
export function isRepoSlug(text: string): boolean {
|
|
118
|
+
return /^[A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9_.][A-Za-z0-9_.-]*$/.test(text)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* True when `name` is a branch name git would accept (`git check-ref-format
|
|
123
|
+
* --branch`), and does not start with `-`, so it cannot read as a flag.
|
|
124
|
+
*/
|
|
125
|
+
function isBranchName(name: string): boolean {
|
|
126
|
+
if (name === '' || name === '@' || name.startsWith('-') || name.startsWith('/')) return false
|
|
127
|
+
if (name.endsWith('/') || name.endsWith('.')) return false
|
|
128
|
+
// Control characters, space, and the characters git reserves.
|
|
129
|
+
// oxlint-disable-next-line no-control-regex
|
|
130
|
+
if (/[\x00-\x20\x7f~^:?*[\\]/.test(name)) return false
|
|
131
|
+
if (name.includes('..') || name.includes('@{') || name.includes('//')) return false
|
|
132
|
+
return name.split('/').every((part) => !part.startsWith('.') && !part.endsWith('.lock'))
|
|
59
133
|
}
|
package/sync.ts
CHANGED
|
@@ -26,12 +26,14 @@ import type {
|
|
|
26
26
|
ApiOptions,
|
|
27
27
|
ApiResponse,
|
|
28
28
|
AuthStatus,
|
|
29
|
+
GhTarget,
|
|
29
30
|
GhVerb,
|
|
30
31
|
IssueListOptions,
|
|
32
|
+
ListResult,
|
|
31
33
|
PrListOptions,
|
|
32
34
|
} from './verbs.ts'
|
|
33
|
-
import { parseOwnerRepo } from './repo.ts'
|
|
34
|
-
import type { IssueRow, Label, OwnerRepo, PullRequest } from './types.ts'
|
|
35
|
+
import { ownerRepoSlug, parseOwnerRepo, parsePrRef } from './repo.ts'
|
|
36
|
+
import type { IssueRow, Label, OwnerRepo, PrSearchHit, PullRequest, Repository } from './types.ts'
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
39
|
* Factory of per-binary runners — the injectable subprocess seam a CLI tail
|
|
@@ -46,8 +48,20 @@ export const defaultRunnerFactory: RunnerFactory = spawnRunner
|
|
|
46
48
|
/** How long to wait between merge-state retries, in milliseconds. */
|
|
47
49
|
const MERGE_STATE_RETRY_DELAY_MS = 1_500
|
|
48
50
|
|
|
51
|
+
/** What a client is built with: the runner, and the repository and host it talks to. */
|
|
52
|
+
export interface GhOptions extends GhTarget {
|
|
53
|
+
runner?: CommandRunner | undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
/**
|
|
50
|
-
* A typed gh client bound to one working directory
|
|
57
|
+
* A typed gh client bound to one working directory, and optionally to one
|
|
58
|
+
* repository and host.
|
|
59
|
+
*
|
|
60
|
+
* With `repo`, every call names that repository (`--repo`, or the
|
|
61
|
+
* `{owner}/{repo}` of an `api` endpoint), so no clone is needed. Without it,
|
|
62
|
+
* gh uses the repository of the clone at `cwd`. With `hostname`, every call
|
|
63
|
+
* goes to that GitHub host. An invalid `repo` or `hostname` throws
|
|
64
|
+
* `RangeError`.
|
|
51
65
|
*
|
|
52
66
|
* Every `timeoutMs` option is a hard cap on the call. An invalid value
|
|
53
67
|
* throws, as in command-seam: `RangeError` for 0, a negative value or `NaN`;
|
|
@@ -55,16 +69,28 @@ const MERGE_STATE_RETRY_DELAY_MS = 1_500
|
|
|
55
69
|
*/
|
|
56
70
|
export class Gh {
|
|
57
71
|
readonly cwd: string
|
|
72
|
+
readonly repo: string | undefined
|
|
73
|
+
readonly hostname: string | undefined
|
|
74
|
+
private readonly target: GhTarget
|
|
58
75
|
private readonly runner: CommandRunner
|
|
59
76
|
|
|
60
|
-
constructor(cwd: string, opts:
|
|
77
|
+
constructor(cwd: string, opts: GhOptions = {}) {
|
|
78
|
+
v.checkTarget(opts)
|
|
61
79
|
this.cwd = cwd
|
|
80
|
+
this.repo = opts.repo
|
|
81
|
+
this.hostname = opts.hostname
|
|
82
|
+
this.target = { repo: opts.repo, hostname: opts.hostname }
|
|
62
83
|
this.runner = resolveRunner('gh', opts.runner)
|
|
63
84
|
}
|
|
64
85
|
|
|
65
|
-
/** The same client rooted at a different directory (same runner). */
|
|
86
|
+
/** The same client rooted at a different directory (same runner, repo and host). */
|
|
66
87
|
at(cwd: string): Gh {
|
|
67
|
-
return new Gh(cwd, { runner: this.runner })
|
|
88
|
+
return new Gh(cwd, { runner: this.runner, repo: this.repo, hostname: this.hostname })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The same client aimed at another repository (same runner, directory and host). */
|
|
92
|
+
withRepo(repo: string): Gh {
|
|
93
|
+
return new Gh(this.cwd, { runner: this.runner, repo, hostname: this.hostname })
|
|
68
94
|
}
|
|
69
95
|
|
|
70
96
|
// ── escape hatches ────────────────────────────────────────────────────────
|
|
@@ -83,6 +109,7 @@ export class Gh {
|
|
|
83
109
|
private exec<T>(verb: GhVerb<T>, timeoutMs?: number | undefined): T {
|
|
84
110
|
const opts: CommandRunOptions = { cwd: this.cwd }
|
|
85
111
|
if (verb.input !== undefined) opts.input = verb.input
|
|
112
|
+
if (verb.env !== undefined) opts.env = verb.env
|
|
86
113
|
if (timeoutMs !== undefined) opts.timeoutMs = timeoutMs
|
|
87
114
|
const result = verb.checked
|
|
88
115
|
? runChecked(this.runner, verb.argv, opts)
|
|
@@ -93,16 +120,45 @@ export class Gh {
|
|
|
93
120
|
// ── queries ───────────────────────────────────────────────────────────────
|
|
94
121
|
|
|
95
122
|
/** Pull requests matching `opts`, or null when gh failed. */
|
|
96
|
-
prList(
|
|
97
|
-
|
|
123
|
+
prList(
|
|
124
|
+
opts: PrListOptions & { timeoutMs?: number | undefined } = {},
|
|
125
|
+
): ListResult<PullRequest> | null {
|
|
126
|
+
return this.exec(v.prList(opts, this.target), opts.timeoutMs)
|
|
98
127
|
}
|
|
99
128
|
|
|
100
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* One PR by number, branch or URL, with the heavy fields; null when gh
|
|
131
|
+
* failed or there is no such PR.
|
|
132
|
+
*/
|
|
101
133
|
prView(
|
|
102
|
-
|
|
134
|
+
ref: number | string,
|
|
135
|
+
opts: { fields?: readonly string[] | undefined; timeoutMs?: number | undefined } = {},
|
|
136
|
+
): PullRequest | null {
|
|
137
|
+
return this.exec(v.prView(ref, opts.fields ?? v.PR_VIEW_FIELDS, this.target), opts.timeoutMs)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The one PR that `input` names, or null: what a person typed, such as `42`,
|
|
142
|
+
* `#42`, `owner/name#42`, a URL or a branch (read by `parsePrRef`).
|
|
143
|
+
*
|
|
144
|
+
* Digits are a PR number first; when no such PR comes back, they are tried
|
|
145
|
+
* as a branch. A branch gives its open PR, or else its newest closed or
|
|
146
|
+
* merged one, so a tool run after the merge still finds it. Fields default
|
|
147
|
+
* to {@link PR_LIST_FIELDS}.
|
|
148
|
+
*/
|
|
149
|
+
prFind(
|
|
150
|
+
input: string,
|
|
103
151
|
opts: { fields?: readonly string[] | undefined; timeoutMs?: number | undefined } = {},
|
|
104
152
|
): PullRequest | null {
|
|
105
|
-
|
|
153
|
+
const ref = parsePrRef(input)
|
|
154
|
+
if (ref === null) return null
|
|
155
|
+
const fields = opts.fields ?? v.PR_LIST_FIELDS
|
|
156
|
+
const call = { fields, timeoutMs: opts.timeoutMs }
|
|
157
|
+
if (ref.kind === 'url') return this.prView(ref.url, call)
|
|
158
|
+
if (ref.kind === 'branch') return prForBranchOn(this, ref.name, call)
|
|
159
|
+
const client = ref.repo === null ? this : this.withRepo(ownerRepoSlug(ref.repo))
|
|
160
|
+
const pr = client.prView(ref.number, call)
|
|
161
|
+
return pr !== null || ref.orBranch === null ? pr : prForBranchOn(this, ref.orBranch, call)
|
|
106
162
|
}
|
|
107
163
|
|
|
108
164
|
/**
|
|
@@ -124,7 +180,7 @@ export class Gh {
|
|
|
124
180
|
limit: opts.limit ?? 5,
|
|
125
181
|
fields: opts.fields ?? v.PR_REF_FIELDS,
|
|
126
182
|
})
|
|
127
|
-
return v.pickPrForBranch(rows, branch)
|
|
183
|
+
return v.pickPrForBranch(rows?.rows ?? null, branch)
|
|
128
184
|
}
|
|
129
185
|
|
|
130
186
|
/**
|
|
@@ -161,7 +217,7 @@ export class Gh {
|
|
|
161
217
|
|
|
162
218
|
/** A PR's body text, or null when gh failed. */
|
|
163
219
|
prBody(number: number, opts: { timeoutMs?: number | undefined } = {}): string | null {
|
|
164
|
-
return this.exec(v.prBody(number), opts.timeoutMs)
|
|
220
|
+
return this.exec(v.prBody(number, this.target), opts.timeoutMs)
|
|
165
221
|
}
|
|
166
222
|
|
|
167
223
|
/**
|
|
@@ -169,24 +225,64 @@ export class Gh {
|
|
|
169
225
|
* returned {@link ApiResponse}.
|
|
170
226
|
*/
|
|
171
227
|
api(endpoint: string, opts: ApiOptions & { timeoutMs?: number | undefined } = {}): ApiResponse {
|
|
172
|
-
return this.exec(v.api(endpoint, opts), opts.timeoutMs)
|
|
228
|
+
return this.exec(v.api(endpoint, opts, this.target), opts.timeoutMs)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A GraphQL query; each page's `data`, or null on a failed or partial
|
|
233
|
+
* result. See the `graphql` verb.
|
|
234
|
+
*
|
|
235
|
+
* @experimental
|
|
236
|
+
*/
|
|
237
|
+
graphql(
|
|
238
|
+
query: string,
|
|
239
|
+
variables: Record<string, v.GraphqlVariable> = {},
|
|
240
|
+
opts: v.GraphqlOptions & { timeoutMs?: number | undefined } = {},
|
|
241
|
+
): unknown[] | null {
|
|
242
|
+
return this.exec(v.graphql(query, variables, opts, this.target), opts.timeoutMs)
|
|
173
243
|
}
|
|
174
244
|
|
|
175
245
|
/** Every label on the repository (up to `opts.limit`, default 1000), or null when gh failed. */
|
|
176
246
|
labelList(
|
|
177
247
|
opts: { limit?: number | undefined; timeoutMs?: number | undefined } = {},
|
|
178
|
-
): Label
|
|
179
|
-
return this.exec(v.labelList(opts), opts.timeoutMs)
|
|
248
|
+
): ListResult<Label> | null {
|
|
249
|
+
return this.exec(v.labelList(opts, this.target), opts.timeoutMs)
|
|
180
250
|
}
|
|
181
251
|
|
|
182
252
|
/** Issues matching `opts` (open by default, up to 200), or null when gh failed. */
|
|
183
|
-
issueList(
|
|
184
|
-
|
|
253
|
+
issueList(
|
|
254
|
+
opts: IssueListOptions & { timeoutMs?: number | undefined } = {},
|
|
255
|
+
): ListResult<IssueRow> | null {
|
|
256
|
+
return this.exec(v.issueList(opts, this.target), opts.timeoutMs)
|
|
185
257
|
}
|
|
186
258
|
|
|
187
259
|
/** One issue with its labels, or null when gh failed. */
|
|
188
260
|
issueView(number: number, opts: { timeoutMs?: number | undefined } = {}): IssueRow | null {
|
|
189
|
-
return this.exec(v.issueView(number), opts.timeoutMs)
|
|
261
|
+
return this.exec(v.issueView(number, this.target), opts.timeoutMs)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** An owner's repositories (up to `opts.limit`, default 100), or null when gh failed. */
|
|
265
|
+
repoList(
|
|
266
|
+
owner: string,
|
|
267
|
+
opts: v.RepoListOptions & { timeoutMs?: number | undefined } = {},
|
|
268
|
+
): ListResult<Repository> | null {
|
|
269
|
+
return this.exec(v.repoList(owner, opts, this.target), opts.timeoutMs)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** One repository: `repo`, else the client's repo, else the clone at `cwd`. Null when gh failed. */
|
|
273
|
+
repoView(
|
|
274
|
+
repo?: string | undefined,
|
|
275
|
+
opts: { fields?: readonly string[] | undefined; timeoutMs?: number | undefined } = {},
|
|
276
|
+
): Repository | null {
|
|
277
|
+
return this.exec(v.repoView(repo, opts.fields ?? v.REPO_FIELDS, this.target), opts.timeoutMs)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Pull requests across repositories matching a search query, or null when gh failed. */
|
|
281
|
+
searchPrs(
|
|
282
|
+
query: string,
|
|
283
|
+
opts: v.SearchPrsOptions & { timeoutMs?: number | undefined } = {},
|
|
284
|
+
): ListResult<PrSearchHit> | null {
|
|
285
|
+
return this.exec(v.searchPrs(query, opts, this.target), opts.timeoutMs)
|
|
190
286
|
}
|
|
191
287
|
|
|
192
288
|
/** True iff the `gh` binary is on PATH and runs. */
|
|
@@ -206,7 +302,7 @@ export class Gh {
|
|
|
206
302
|
|
|
207
303
|
/** Login state and what `gh auth status` printed; `authenticated()` is its `ok`. */
|
|
208
304
|
authStatus(): AuthStatus {
|
|
209
|
-
return this.exec(v.authStatus())
|
|
305
|
+
return this.exec(v.authStatus(this.target))
|
|
210
306
|
}
|
|
211
307
|
|
|
212
308
|
/**
|
|
@@ -232,22 +328,22 @@ export class Gh {
|
|
|
232
328
|
|
|
233
329
|
/** Open a PR and return its URL ('' if gh printed none). */
|
|
234
330
|
createPr(opts: v.CreatePrOptions): string {
|
|
235
|
-
return this.exec(v.createPr(opts))
|
|
331
|
+
return this.exec(v.createPr(opts, this.target))
|
|
236
332
|
}
|
|
237
333
|
|
|
238
334
|
/** Retitle or rewrite a PR body. */
|
|
239
335
|
editPr(number: number, opts: { title?: string | undefined; body?: string | undefined }): void {
|
|
240
|
-
this.exec(v.editPr(number, opts))
|
|
336
|
+
this.exec(v.editPr(number, opts, this.target))
|
|
241
337
|
}
|
|
242
338
|
|
|
243
339
|
/** The PR's unified diff; throws `CommandFailed` when gh cannot produce it. */
|
|
244
340
|
prDiff(number: number): string {
|
|
245
|
-
return this.exec(v.prDiff(number))
|
|
341
|
+
return this.exec(v.prDiff(number, this.target))
|
|
246
342
|
}
|
|
247
343
|
|
|
248
344
|
/** Post one conversation comment on a PR; returns its URL ('' if gh printed none). */
|
|
249
345
|
prComment(number: number, body: string): string {
|
|
250
|
-
return this.exec(v.prComment(number, body))
|
|
346
|
+
return this.exec(v.prComment(number, body, this.target))
|
|
251
347
|
}
|
|
252
348
|
|
|
253
349
|
/**
|
|
@@ -256,22 +352,22 @@ export class Gh {
|
|
|
256
352
|
* @experimental
|
|
257
353
|
*/
|
|
258
354
|
createReview(number: number, opts: v.CreateReviewOptions): string {
|
|
259
|
-
return this.exec(v.createReview(number, opts))
|
|
355
|
+
return this.exec(v.createReview(number, opts, this.target))
|
|
260
356
|
}
|
|
261
357
|
|
|
262
358
|
/** Rewrite just a PR's body. */
|
|
263
359
|
editPrBody(number: number, body: string): void {
|
|
264
|
-
this.exec(v.editPrBody(number, body))
|
|
360
|
+
this.exec(v.editPrBody(number, body, this.target))
|
|
265
361
|
}
|
|
266
362
|
|
|
267
363
|
/** Bring a PR's head branch up to date with its base. */
|
|
268
364
|
prUpdateBranch(number: number, style: 'merge' | 'rebase' = 'merge'): void {
|
|
269
|
-
this.exec(v.prUpdateBranch(number, style))
|
|
365
|
+
this.exec(v.prUpdateBranch(number, style, this.target))
|
|
270
366
|
}
|
|
271
367
|
|
|
272
368
|
/** Mark a PR ready for review (`draft: false`) or convert it to a draft (`draft: true`). */
|
|
273
369
|
prSetDraft(number: number, draft: boolean): void {
|
|
274
|
-
this.exec(v.prSetDraft(number, draft))
|
|
370
|
+
this.exec(v.prSetDraft(number, draft, this.target))
|
|
275
371
|
}
|
|
276
372
|
|
|
277
373
|
/**
|
|
@@ -280,22 +376,22 @@ export class Gh {
|
|
|
280
376
|
* @experimental
|
|
281
377
|
*/
|
|
282
378
|
issueCommentEdit(commentId: string, body: string): void {
|
|
283
|
-
this.exec(v.issueCommentEdit(commentId, body))
|
|
379
|
+
this.exec(v.issueCommentEdit(commentId, body, this.target))
|
|
284
380
|
}
|
|
285
381
|
|
|
286
382
|
/** Merge a PR; GitHub refuses when its head is no longer `opts.headSha`. */
|
|
287
383
|
prMerge(number: number, opts: { method: v.PrMergeMethod; headSha: string }): void {
|
|
288
|
-
this.exec(v.prMerge(number, opts))
|
|
384
|
+
this.exec(v.prMerge(number, opts, this.target))
|
|
289
385
|
}
|
|
290
386
|
|
|
291
387
|
/** A PR's review threads, or null when they could not be read completely. */
|
|
292
388
|
prReviewThreads(number: number): v.ReviewThread[] | null {
|
|
293
|
-
return this.exec(v.prReviewThreads(number))
|
|
389
|
+
return this.exec(v.prReviewThreads(number, this.target))
|
|
294
390
|
}
|
|
295
391
|
|
|
296
392
|
/** Create a label (or force it to `opts`'s color/description if it already exists). */
|
|
297
393
|
createLabel(opts: v.CreateLabelOptions): void {
|
|
298
|
-
this.exec(v.createLabel(opts))
|
|
394
|
+
this.exec(v.createLabel(opts, this.target))
|
|
299
395
|
}
|
|
300
396
|
|
|
301
397
|
/** Recolor and/or redescribe an existing label. */
|
|
@@ -303,7 +399,7 @@ export class Gh {
|
|
|
303
399
|
name: string,
|
|
304
400
|
opts: { color?: string | undefined; description?: string | undefined },
|
|
305
401
|
): void {
|
|
306
|
-
this.exec(v.editLabel(name, opts))
|
|
402
|
+
this.exec(v.editLabel(name, opts, this.target))
|
|
307
403
|
}
|
|
308
404
|
|
|
309
405
|
/**
|
|
@@ -312,10 +408,26 @@ export class Gh {
|
|
|
312
408
|
* or briefly-double-labeled intermediate state.
|
|
313
409
|
*/
|
|
314
410
|
setIssueLabel(number: number, currentLabel: string | null, newLabel: string): void {
|
|
315
|
-
this.exec(v.setIssueLabel(number, currentLabel, newLabel))
|
|
411
|
+
this.exec(v.setIssueLabel(number, currentLabel, newLabel, this.target))
|
|
316
412
|
}
|
|
317
413
|
}
|
|
318
414
|
|
|
415
|
+
/** The open PR for `branch`, else its newest closed or merged one. */
|
|
416
|
+
function prForBranchOn(
|
|
417
|
+
gh: Gh,
|
|
418
|
+
branch: string,
|
|
419
|
+
call: { fields: readonly string[]; timeoutMs?: number | undefined },
|
|
420
|
+
): PullRequest | null {
|
|
421
|
+
const rows = gh.prList({
|
|
422
|
+
head: branch,
|
|
423
|
+
state: 'all',
|
|
424
|
+
limit: 20,
|
|
425
|
+
fields: v.branchPrFields(call.fields),
|
|
426
|
+
timeoutMs: call.timeoutMs,
|
|
427
|
+
})
|
|
428
|
+
return v.branchPr(rows)
|
|
429
|
+
}
|
|
430
|
+
|
|
319
431
|
/**
|
|
320
432
|
* Block the calling thread. Crude on purpose: the synchronous client has no
|
|
321
433
|
* event loop to await on, and the alternative — making every caller async to
|
package/types.ts
CHANGED
|
@@ -184,3 +184,43 @@ export interface OwnerRepo {
|
|
|
184
184
|
owner: string
|
|
185
185
|
repo: string
|
|
186
186
|
}
|
|
187
|
+
|
|
188
|
+
/** Who can see a repository. */
|
|
189
|
+
export type RepositoryVisibility = 'PUBLIC' | 'PRIVATE' | 'INTERNAL' | (string & {})
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A repository, off `gh repo list`/`gh repo view --json`. A field not asked
|
|
193
|
+
* for comes back at its "not known" value. `nameWithOwner` is the only field
|
|
194
|
+
* without one: a row without it is dropped.
|
|
195
|
+
*/
|
|
196
|
+
export interface Repository {
|
|
197
|
+
/** `owner/name`. */
|
|
198
|
+
nameWithOwner: string
|
|
199
|
+
owner: string
|
|
200
|
+
name: string
|
|
201
|
+
description: string
|
|
202
|
+
/** The default branch; null when not asked for, or the repository is empty. */
|
|
203
|
+
defaultBranch: string | null
|
|
204
|
+
visibility: RepositoryVisibility | null
|
|
205
|
+
isArchived: boolean
|
|
206
|
+
url: string
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* One pull request found by `gh search prs`. Search has its own field set:
|
|
211
|
+
* it names the repository but carries no head or base branch, so this is not
|
|
212
|
+
* a {@link PullRequest}. `state` is upper-cased to match `PullRequest`.
|
|
213
|
+
*/
|
|
214
|
+
export interface PrSearchHit {
|
|
215
|
+
number: number
|
|
216
|
+
title: string
|
|
217
|
+
state: PullRequestState
|
|
218
|
+
url: string
|
|
219
|
+
isDraft: boolean
|
|
220
|
+
repository: OwnerRepo | null
|
|
221
|
+
author: Actor | null
|
|
222
|
+
labels: Label[]
|
|
223
|
+
createdAt: string | null
|
|
224
|
+
updatedAt: string | null
|
|
225
|
+
closedAt: string | null
|
|
226
|
+
}
|