@kitlangton/ghui 0.1.19 → 0.1.20
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/README.md +14 -3
- package/bin/ghui.js +64 -1
- package/package.json +6 -2
- package/src/App.tsx +818 -403
- package/src/appCommands.ts +330 -0
- package/src/commands.ts +68 -0
- package/src/config.ts +10 -0
- package/src/domain.ts +29 -20
- package/src/errors.ts +10 -0
- package/src/index.tsx +23 -2
- package/src/mergeActions.ts +1 -6
- package/src/pullRequestCache.ts +19 -0
- package/src/pullRequestViews.ts +45 -0
- package/src/services/BrowserOpener.ts +22 -0
- package/src/services/Clipboard.ts +46 -0
- package/src/services/CommandRunner.ts +14 -6
- package/src/services/GitHubService.ts +290 -126
- package/src/services/MockGitHubService.ts +146 -0
- package/src/ui/CommandPalette.tsx +143 -0
- package/src/ui/DetailsPane.tsx +49 -63
- package/src/ui/FooterHints.tsx +84 -179
- package/src/ui/PullRequestDiffPane.tsx +29 -50
- package/src/ui/PullRequestList.tsx +99 -45
- package/src/ui/colors.ts +167 -1
- package/src/ui/diff.ts +34 -44
- package/src/ui/diffStats.tsx +25 -0
- package/src/ui/modals.tsx +263 -295
- package/src/ui/primitives.tsx +90 -0
- package/src/ui/pullRequests.ts +44 -12
- package/src/ui/singleLineInput.ts +25 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Schema } from "effect"
|
|
2
2
|
import { config } from "../config.js"
|
|
3
|
-
import { DiffCommentSide, pullRequestQueueSearchQualifier, type CheckItem, type CreatePullRequestCommentInput, type Mergeable, type PullRequestItem, type PullRequestMergeAction, type PullRequestMergeInfo, type PullRequestQueueMode, type PullRequestReviewComment, type ReviewStatus } from "../domain.js"
|
|
3
|
+
import { DiffCommentSide, pullRequestQueueSearchQualifier, type CheckItem, type CreatePullRequestCommentInput, type ListPullRequestPageInput, type Mergeable, type PullRequestItem, type PullRequestMergeAction, type PullRequestMergeInfo, type PullRequestPage, type PullRequestQueueMode, type PullRequestReviewComment, type ReviewStatus } from "../domain.js"
|
|
4
4
|
import { getMergeActionDefinition } from "../mergeActions.js"
|
|
5
|
-
import {
|
|
5
|
+
import { CommandError, CommandRunner, type JsonParseError } from "./CommandRunner.js"
|
|
6
6
|
|
|
7
7
|
const NullableString = Schema.NullOr(Schema.String)
|
|
8
8
|
const OptionalNullableString = Schema.optionalKey(NullableString)
|
|
@@ -29,6 +29,10 @@ const RawLabelSchema = Schema.Struct({
|
|
|
29
29
|
color: OptionalNullableString,
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
+
const RawStatusCheckRollupSchema = Schema.Struct({
|
|
33
|
+
contexts: Schema.Struct({ nodes: Schema.Array(RawCheckContextSchema) }),
|
|
34
|
+
})
|
|
35
|
+
|
|
32
36
|
const RawPullRequestSummaryFields = {
|
|
33
37
|
number: Schema.Number,
|
|
34
38
|
title: Schema.String,
|
|
@@ -45,7 +49,10 @@ const RawPullRequestSummaryFields = {
|
|
|
45
49
|
repository: RawRepositorySchema,
|
|
46
50
|
} as const
|
|
47
51
|
|
|
48
|
-
const RawPullRequestSummaryNodeSchema = Schema.Struct(
|
|
52
|
+
const RawPullRequestSummaryNodeSchema = Schema.Struct({
|
|
53
|
+
...RawPullRequestSummaryFields,
|
|
54
|
+
statusCheckRollup: Schema.optionalKey(Schema.NullOr(RawStatusCheckRollupSchema)),
|
|
55
|
+
})
|
|
49
56
|
|
|
50
57
|
const RawPullRequestNodeSchema = Schema.Struct({
|
|
51
58
|
...RawPullRequestSummaryFields,
|
|
@@ -54,9 +61,15 @@ const RawPullRequestNodeSchema = Schema.Struct({
|
|
|
54
61
|
additions: Schema.Number,
|
|
55
62
|
deletions: Schema.Number,
|
|
56
63
|
changedFiles: Schema.Number,
|
|
57
|
-
statusCheckRollup: Schema.optionalKey(Schema.NullOr(
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
statusCheckRollup: Schema.optionalKey(Schema.NullOr(RawStatusCheckRollupSchema)),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const PullRequestDetailResponseSchema = Schema.Struct({
|
|
68
|
+
data: Schema.Struct({
|
|
69
|
+
repository: Schema.NullOr(Schema.Struct({
|
|
70
|
+
pullRequest: Schema.NullOr(RawPullRequestNodeSchema),
|
|
71
|
+
})),
|
|
72
|
+
}),
|
|
60
73
|
})
|
|
61
74
|
|
|
62
75
|
const PageInfoSchema = Schema.Struct({
|
|
@@ -74,6 +87,17 @@ const SearchResponseSchema = <Item extends Schema.Top>(item: Item) =>
|
|
|
74
87
|
}),
|
|
75
88
|
})
|
|
76
89
|
|
|
90
|
+
const RepositoryPullRequestsResponseSchema = Schema.Struct({
|
|
91
|
+
data: Schema.Struct({
|
|
92
|
+
repository: Schema.NullOr(Schema.Struct({
|
|
93
|
+
pullRequests: Schema.Struct({
|
|
94
|
+
nodes: Schema.Array(Schema.NullOr(RawPullRequestSummaryNodeSchema)),
|
|
95
|
+
pageInfo: PageInfoSchema,
|
|
96
|
+
}),
|
|
97
|
+
})),
|
|
98
|
+
}),
|
|
99
|
+
})
|
|
100
|
+
|
|
77
101
|
const ViewerSchema = Schema.Struct({ login: Schema.String })
|
|
78
102
|
|
|
79
103
|
const MergeInfoResponseSchema = Schema.Struct({
|
|
@@ -103,11 +127,23 @@ const PullRequestCommentSchema = Schema.Struct({
|
|
|
103
127
|
side: Schema.optionalKey(Schema.NullOr(DiffCommentSide)),
|
|
104
128
|
})
|
|
105
129
|
|
|
130
|
+
const PullRequestFileSchema = Schema.Struct({
|
|
131
|
+
filename: Schema.String,
|
|
132
|
+
previous_filename: OptionalNullableString,
|
|
133
|
+
status: OptionalNullableString,
|
|
134
|
+
patch: OptionalNullableString,
|
|
135
|
+
})
|
|
136
|
+
|
|
106
137
|
const CommentsResponseSchema = Schema.Union([
|
|
107
138
|
Schema.Array(PullRequestCommentSchema),
|
|
108
139
|
Schema.Array(Schema.Array(PullRequestCommentSchema)),
|
|
109
140
|
])
|
|
110
141
|
|
|
142
|
+
const PullRequestFilesResponseSchema = Schema.Union([
|
|
143
|
+
Schema.Array(PullRequestFileSchema),
|
|
144
|
+
Schema.Array(Schema.Array(PullRequestFileSchema)),
|
|
145
|
+
])
|
|
146
|
+
|
|
111
147
|
const RepoLabelsResponseSchema = Schema.Array(Schema.Struct({
|
|
112
148
|
name: Schema.String,
|
|
113
149
|
color: Schema.String,
|
|
@@ -117,6 +153,7 @@ type RawPullRequestSummaryNode = Schema.Schema.Type<typeof RawPullRequestSummary
|
|
|
117
153
|
type RawPullRequestNode = Schema.Schema.Type<typeof RawPullRequestNodeSchema>
|
|
118
154
|
type RawCheckContext = Schema.Schema.Type<typeof RawCheckContextSchema>
|
|
119
155
|
type RawPullRequestComment = Schema.Schema.Type<typeof PullRequestCommentSchema>
|
|
156
|
+
type RawPullRequestFile = Schema.Schema.Type<typeof PullRequestFileSchema>
|
|
120
157
|
|
|
121
158
|
type SearchResponse<Item> = {
|
|
122
159
|
readonly data: {
|
|
@@ -130,11 +167,41 @@ type SearchResponse<Item> = {
|
|
|
130
167
|
}
|
|
131
168
|
}
|
|
132
169
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
170
|
+
type PullRequestConnection<Item> = {
|
|
171
|
+
readonly nodes: readonly (Item | null)[]
|
|
172
|
+
readonly pageInfo: {
|
|
173
|
+
readonly hasNextPage: boolean
|
|
174
|
+
readonly endCursor: string | null
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const STATUS_CHECK_FRAGMENT = `
|
|
179
|
+
statusCheckRollup {
|
|
180
|
+
contexts(first: 100) {
|
|
181
|
+
nodes {
|
|
182
|
+
__typename
|
|
183
|
+
... on CheckRun { name status conclusion }
|
|
184
|
+
... on StatusContext { context state }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}`
|
|
188
|
+
|
|
189
|
+
const SUMMARY_FIELDS_FRAGMENT = `
|
|
190
|
+
number
|
|
191
|
+
title
|
|
192
|
+
isDraft
|
|
193
|
+
reviewDecision
|
|
194
|
+
autoMergeRequest { enabledAt }
|
|
195
|
+
state
|
|
196
|
+
merged
|
|
197
|
+
createdAt
|
|
198
|
+
closedAt
|
|
199
|
+
url
|
|
200
|
+
author { login }
|
|
201
|
+
headRefOid
|
|
202
|
+
repository { nameWithOwner }${STATUS_CHECK_FRAGMENT}`
|
|
203
|
+
|
|
204
|
+
const DETAIL_FIELDS_FRAGMENT = `
|
|
138
205
|
number
|
|
139
206
|
title
|
|
140
207
|
body
|
|
@@ -152,16 +219,13 @@ query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
|
152
219
|
author { login }
|
|
153
220
|
headRefOid
|
|
154
221
|
repository { nameWithOwner }
|
|
155
|
-
labels(first: 20) { nodes { name color } }
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
222
|
+
labels(first: 20) { nodes { name color } }${STATUS_CHECK_FRAGMENT}`
|
|
223
|
+
|
|
224
|
+
const pullRequestSearchQuery = `
|
|
225
|
+
query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
226
|
+
search(query: $searchQuery, type: ISSUE, first: $first, after: $after) {
|
|
227
|
+
nodes {
|
|
228
|
+
... on PullRequest {${DETAIL_FIELDS_FRAGMENT}
|
|
165
229
|
}
|
|
166
230
|
}
|
|
167
231
|
pageInfo { hasNextPage endCursor }
|
|
@@ -169,24 +233,20 @@ query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
|
169
233
|
}
|
|
170
234
|
`
|
|
171
235
|
|
|
236
|
+
const pullRequestDetailQuery = `
|
|
237
|
+
query PullRequest($owner: String!, $name: String!, $number: Int!) {
|
|
238
|
+
repository(owner: $owner, name: $name) {
|
|
239
|
+
pullRequest(number: $number) {${DETAIL_FIELDS_FRAGMENT}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
`
|
|
244
|
+
|
|
172
245
|
const pullRequestSummarySearchQuery = `
|
|
173
246
|
query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
174
247
|
search(query: $searchQuery, type: ISSUE, first: $first, after: $after) {
|
|
175
248
|
nodes {
|
|
176
|
-
... on PullRequest {
|
|
177
|
-
number
|
|
178
|
-
title
|
|
179
|
-
isDraft
|
|
180
|
-
reviewDecision
|
|
181
|
-
autoMergeRequest { enabledAt }
|
|
182
|
-
state
|
|
183
|
-
merged
|
|
184
|
-
createdAt
|
|
185
|
-
closedAt
|
|
186
|
-
url
|
|
187
|
-
author { login }
|
|
188
|
-
headRefOid
|
|
189
|
-
repository { nameWithOwner }
|
|
249
|
+
... on PullRequest {${SUMMARY_FIELDS_FRAGMENT}
|
|
190
250
|
}
|
|
191
251
|
}
|
|
192
252
|
pageInfo { hasNextPage endCursor }
|
|
@@ -194,6 +254,18 @@ query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
|
194
254
|
}
|
|
195
255
|
`
|
|
196
256
|
|
|
257
|
+
const repositoryPullRequestsQuery = `
|
|
258
|
+
query RepositoryPullRequests($owner: String!, $name: String!, $first: Int!, $after: String) {
|
|
259
|
+
repository(owner: $owner, name: $name) {
|
|
260
|
+
pullRequests(states: OPEN, first: $first, after: $after, orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
261
|
+
nodes {${SUMMARY_FIELDS_FRAGMENT}
|
|
262
|
+
}
|
|
263
|
+
pageInfo { hasNextPage endCursor }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
`
|
|
268
|
+
|
|
197
269
|
const normalizeDate = (value: string | null | undefined) => {
|
|
198
270
|
if (!value || value.startsWith("0001-01-01")) return null
|
|
199
271
|
return new Date(value)
|
|
@@ -296,28 +368,31 @@ const getCheckInfoFromContexts = (contexts: readonly RawCheckContext[]): Pick<Pu
|
|
|
296
368
|
return { checkStatus: "passing", checkSummary: `checks ${successful}/${contexts.length}`, checks }
|
|
297
369
|
}
|
|
298
370
|
|
|
299
|
-
const parsePullRequestSummary = (item: RawPullRequestSummaryNode): PullRequestItem =>
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
371
|
+
const parsePullRequestSummary = (item: RawPullRequestSummaryNode): PullRequestItem => {
|
|
372
|
+
const checkInfo = getCheckInfoFromContexts(item.statusCheckRollup?.contexts.nodes ?? [])
|
|
373
|
+
return {
|
|
374
|
+
repository: item.repository.nameWithOwner,
|
|
375
|
+
author: item.author.login,
|
|
376
|
+
headRefOid: item.headRefOid,
|
|
377
|
+
number: item.number,
|
|
378
|
+
title: item.title,
|
|
379
|
+
body: "",
|
|
380
|
+
labels: [],
|
|
381
|
+
additions: 0,
|
|
382
|
+
deletions: 0,
|
|
383
|
+
changedFiles: 0,
|
|
384
|
+
state: getPullRequestState(item),
|
|
385
|
+
reviewStatus: getReviewStatus(item),
|
|
386
|
+
checkStatus: checkInfo.checkStatus,
|
|
387
|
+
checkSummary: checkInfo.checkSummary,
|
|
388
|
+
checks: checkInfo.checks,
|
|
389
|
+
autoMergeEnabled: item.autoMergeRequest !== null,
|
|
390
|
+
detailLoaded: false,
|
|
391
|
+
createdAt: new Date(item.createdAt),
|
|
392
|
+
closedAt: normalizeDate(item.closedAt),
|
|
393
|
+
url: item.url,
|
|
394
|
+
}
|
|
395
|
+
}
|
|
321
396
|
|
|
322
397
|
const parsePullRequest = (item: RawPullRequestNode): PullRequestItem => {
|
|
323
398
|
const checkInfo = getCheckInfoFromContexts(item.statusCheckRollup?.contexts.nodes ?? [])
|
|
@@ -338,10 +413,21 @@ const parsePullRequest = (item: RawPullRequestNode): PullRequestItem => {
|
|
|
338
413
|
}
|
|
339
414
|
}
|
|
340
415
|
|
|
341
|
-
const searchQuery = (mode: PullRequestQueueMode, author: string
|
|
416
|
+
const searchQuery = (mode: PullRequestQueueMode, author: string, repository: string | null) => {
|
|
417
|
+
const sort = mode === "repository" ? "sort:updated-desc" : "sort:created-desc"
|
|
418
|
+
return `${pullRequestQueueSearchQualifier(mode, author, repository)} is:pr is:open ${sort}`
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const pullRequestPage = <Item>(connection: PullRequestConnection<Item>, parse: (node: Item) => PullRequestItem): PullRequestPage => ({
|
|
422
|
+
items: connection.nodes.flatMap((node) => node ? [parse(node)] : []),
|
|
423
|
+
endCursor: connection.pageInfo.endCursor,
|
|
424
|
+
hasNextPage: connection.pageInfo.hasNextPage && connection.pageInfo.endCursor !== null,
|
|
425
|
+
})
|
|
342
426
|
|
|
343
|
-
const
|
|
344
|
-
[
|
|
427
|
+
const repositoryParts = (repository: string) => {
|
|
428
|
+
const [owner, name] = repository.split("/")
|
|
429
|
+
return owner && name ? { owner, name } : null
|
|
430
|
+
}
|
|
345
431
|
|
|
346
432
|
const parsePullRequestComment = (comment: RawPullRequestComment): PullRequestReviewComment | null => {
|
|
347
433
|
const line = comment.line ?? comment.original_line
|
|
@@ -359,15 +445,40 @@ const parsePullRequestComment = (comment: RawPullRequestComment): PullRequestRev
|
|
|
359
445
|
}
|
|
360
446
|
|
|
361
447
|
const parsePullRequestComments = (response: Schema.Schema.Type<typeof CommentsResponseSchema>): readonly PullRequestReviewComment[] => {
|
|
362
|
-
|
|
363
|
-
? response as readonly (readonly RawPullRequestComment[])[]
|
|
364
|
-
: [response as readonly RawPullRequestComment[]]
|
|
365
|
-
return pages.flatMap((page) => page.flatMap((comment) => {
|
|
448
|
+
return flattenSlurpedPages(response).flatMap((comment) => {
|
|
366
449
|
const parsed = parsePullRequestComment(comment)
|
|
367
450
|
return parsed ? [parsed] : []
|
|
368
|
-
})
|
|
451
|
+
})
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const flattenSlurpedPages = <Item>(response: readonly Item[] | readonly (readonly Item[])[]): readonly Item[] =>
|
|
455
|
+
Array.isArray(response[0]) ? (response as readonly (readonly Item[])[]).flat() : response as readonly Item[]
|
|
456
|
+
|
|
457
|
+
const parsePullRequestFiles = (response: Schema.Schema.Type<typeof PullRequestFilesResponseSchema>): readonly RawPullRequestFile[] =>
|
|
458
|
+
flattenSlurpedPages(response)
|
|
459
|
+
|
|
460
|
+
const diffPath = (path: string) => /\s|"/.test(path) ? JSON.stringify(path) : path
|
|
461
|
+
|
|
462
|
+
const prefixedDiffPath = (prefix: "a" | "b", path: string) => diffPath(`${prefix}/${path}`)
|
|
463
|
+
|
|
464
|
+
const fileHeaderPatch = (file: RawPullRequestFile) => {
|
|
465
|
+
const oldPath = file.previous_filename ?? file.filename
|
|
466
|
+
const newPath = file.filename
|
|
467
|
+
const oldRef = file.status === "added" ? "/dev/null" : prefixedDiffPath("a", oldPath)
|
|
468
|
+
const newRef = file.status === "removed" ? "/dev/null" : prefixedDiffPath("b", newPath)
|
|
469
|
+
const lines = [
|
|
470
|
+
`diff --git ${prefixedDiffPath("a", oldPath)} ${prefixedDiffPath("b", newPath)}`,
|
|
471
|
+
...(file.status === "renamed" && file.previous_filename ? [`rename from ${oldPath}`, `rename to ${newPath}`] : []),
|
|
472
|
+
`--- ${oldRef}`,
|
|
473
|
+
`+++ ${newRef}`,
|
|
474
|
+
]
|
|
475
|
+
if (file.patch) lines.push(file.patch.trimEnd())
|
|
476
|
+
return lines.join("\n")
|
|
369
477
|
}
|
|
370
478
|
|
|
479
|
+
export const pullRequestFilesToPatch = (files: readonly RawPullRequestFile[]) =>
|
|
480
|
+
files.map(fileHeaderPatch).join("\n")
|
|
481
|
+
|
|
371
482
|
const fallbackCreatedComment = (input: CreatePullRequestCommentInput): PullRequestReviewComment => ({
|
|
372
483
|
id: `created:${input.repository}:${input.number}:${input.path}:${input.side}:${input.line}:${Date.now()}`,
|
|
373
484
|
path: input.path,
|
|
@@ -390,10 +501,12 @@ const normalizeMergeable = (value: string): Mergeable =>
|
|
|
390
501
|
MERGEABLE_BY_RAW[value] ?? "unknown"
|
|
391
502
|
|
|
392
503
|
export class GitHubService extends Context.Service<GitHubService, {
|
|
393
|
-
readonly listOpenPullRequests: (mode: PullRequestQueueMode) => Effect.Effect<readonly PullRequestItem[], GitHubError>
|
|
394
|
-
readonly
|
|
504
|
+
readonly listOpenPullRequests: (mode: PullRequestQueueMode, repository: string | null) => Effect.Effect<readonly PullRequestItem[], GitHubError>
|
|
505
|
+
readonly listOpenPullRequestPage: (input: ListPullRequestPageInput) => Effect.Effect<PullRequestPage, GitHubError>
|
|
506
|
+
readonly listOpenPullRequestDetails: (mode: PullRequestQueueMode, repository: string | null) => Effect.Effect<readonly PullRequestItem[], GitHubError>
|
|
507
|
+
readonly getPullRequestDetails: (repository: string, number: number) => Effect.Effect<PullRequestItem, GitHubError>
|
|
395
508
|
readonly getAuthenticatedUser: () => Effect.Effect<string, GitHubError>
|
|
396
|
-
readonly getPullRequestDiff: (repository: string, number: number) => Effect.Effect<string,
|
|
509
|
+
readonly getPullRequestDiff: (repository: string, number: number) => Effect.Effect<string, GitHubError>
|
|
397
510
|
readonly listPullRequestComments: (repository: string, number: number) => Effect.Effect<readonly PullRequestReviewComment[], GitHubError>
|
|
398
511
|
readonly getPullRequestMergeInfo: (repository: string, number: number) => Effect.Effect<PullRequestMergeInfo, GitHubError>
|
|
399
512
|
readonly mergePullRequest: (repository: string, number: number, action: PullRequestMergeAction) => Effect.Effect<void, CommandError>
|
|
@@ -409,55 +522,112 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
409
522
|
Effect.gen(function*() {
|
|
410
523
|
const command = yield* CommandRunner
|
|
411
524
|
|
|
412
|
-
const
|
|
525
|
+
const ghJson = <S extends Schema.Top>(label: string, schema: S, args: readonly string[]) =>
|
|
526
|
+
command.runSchema(schema, "gh", args).pipe(Effect.withSpan(`GitHubService.${label}`))
|
|
527
|
+
|
|
528
|
+
const ghVoid = (label: string, args: readonly string[]) =>
|
|
529
|
+
command.run("gh", args).pipe(Effect.withSpan(`GitHubService.${label}`), Effect.asVoid)
|
|
530
|
+
|
|
531
|
+
const searchPage = <Item extends Schema.Top>(label: string, query: string, schema: Item, parse: (node: Item["Type"]) => PullRequestItem) => {
|
|
413
532
|
const responseSchema = SearchResponseSchema(schema)
|
|
414
|
-
return Effect.fn(`GitHubService.${label}`)(function*(
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
"-F", `searchQuery=${searchQuery(mode, config.author)}`,
|
|
424
|
-
"-F", `first=${pageSize}`,
|
|
425
|
-
...(cursor ? ["-F", `after=${cursor}`] : []),
|
|
426
|
-
])
|
|
427
|
-
|
|
428
|
-
for (const node of response.data.search.nodes) {
|
|
429
|
-
if (node) pullRequests.push(parse(node))
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (!response.data.search.pageInfo.hasNextPage) break
|
|
433
|
-
cursor = response.data.search.pageInfo.endCursor
|
|
434
|
-
if (!cursor) break
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
return sortNewestFirst(pullRequests)
|
|
533
|
+
return Effect.fn(`GitHubService.${label}`)(function*(input: ListPullRequestPageInput) {
|
|
534
|
+
const response: SearchResponse<Item["Type"]> = yield* command.runSchema(responseSchema, "gh", [
|
|
535
|
+
"api", "graphql",
|
|
536
|
+
"-f", `query=${query}`,
|
|
537
|
+
"-F", `searchQuery=${searchQuery(input.mode, config.author, input.repository)}`,
|
|
538
|
+
"-F", `first=${input.pageSize}`,
|
|
539
|
+
...(input.cursor ? ["-F", `after=${input.cursor}`] : []),
|
|
540
|
+
])
|
|
541
|
+
return pullRequestPage(response.data.search, parse)
|
|
438
542
|
})
|
|
439
543
|
}
|
|
440
544
|
|
|
441
|
-
const
|
|
442
|
-
const
|
|
545
|
+
const listOpenPullRequestSearchPage = searchPage("listOpenPullRequestSearchPage", pullRequestSummarySearchQuery, RawPullRequestSummaryNodeSchema, parsePullRequestSummary)
|
|
546
|
+
const listOpenPullRequestDetailsPage = searchPage("listOpenPullRequestDetailsPage", pullRequestSearchQuery, RawPullRequestNodeSchema, parsePullRequest)
|
|
547
|
+
|
|
548
|
+
const listRepositoryPullRequestPage = Effect.fn("GitHubService.listRepositoryPullRequestPage")(function*(input: ListPullRequestPageInput) {
|
|
549
|
+
if (!input.repository) return { items: [], endCursor: null, hasNextPage: false } satisfies PullRequestPage
|
|
550
|
+
const repo = repositoryParts(input.repository)
|
|
551
|
+
if (!repo) {
|
|
552
|
+
return yield* new CommandError({ command: "gh", args: [], detail: `Invalid repository: ${input.repository}`, cause: input.repository })
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const response = yield* command.runSchema(RepositoryPullRequestsResponseSchema, "gh", [
|
|
556
|
+
"api", "graphql",
|
|
557
|
+
"-f", `query=${repositoryPullRequestsQuery}`,
|
|
558
|
+
"-F", `owner=${repo.owner}`,
|
|
559
|
+
"-F", `name=${repo.name}`,
|
|
560
|
+
"-F", `first=${input.pageSize}`,
|
|
561
|
+
...(input.cursor ? ["-F", `after=${input.cursor}`] : []),
|
|
562
|
+
])
|
|
563
|
+
const connection = response.data.repository?.pullRequests
|
|
564
|
+
if (!connection) {
|
|
565
|
+
return yield* new CommandError({ command: "gh", args: [], detail: `Repository not found: ${input.repository}`, cause: input.repository })
|
|
566
|
+
}
|
|
567
|
+
return pullRequestPage(connection, parsePullRequestSummary)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
const listOpenPullRequestPage = Effect.fn("GitHubService.listOpenPullRequestPage")(function*(input: ListPullRequestPageInput) {
|
|
571
|
+
const pageSize = Math.max(1, Math.min(100, input.pageSize))
|
|
572
|
+
const pageInput = { ...input, pageSize }
|
|
573
|
+
if (pageInput.mode === "repository" && pageInput.repository) return yield* listRepositoryPullRequestPage(pageInput)
|
|
574
|
+
return yield* listOpenPullRequestSearchPage(pageInput)
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
const paginatePages = Effect.fn("GitHubService.paginatePages")(function*(mode: PullRequestQueueMode, repository: string | null, loadPage: (input: ListPullRequestPageInput) => Effect.Effect<PullRequestPage, GitHubError>) {
|
|
578
|
+
const pullRequests: PullRequestItem[] = []
|
|
579
|
+
let cursor: string | null = null
|
|
580
|
+
|
|
581
|
+
while (pullRequests.length < config.prFetchLimit) {
|
|
582
|
+
const page: PullRequestPage = yield* loadPage({ mode, repository, cursor, pageSize: Math.min(100, config.prFetchLimit - pullRequests.length) })
|
|
583
|
+
pullRequests.push(...page.items)
|
|
584
|
+
if (!page.hasNextPage || !page.endCursor) break
|
|
585
|
+
cursor = page.endCursor
|
|
586
|
+
}
|
|
443
587
|
|
|
444
|
-
|
|
445
|
-
const viewer = yield* command.runSchema(ViewerSchema, "gh", ["api", "user"])
|
|
446
|
-
return viewer.login
|
|
588
|
+
return pullRequests
|
|
447
589
|
})
|
|
448
590
|
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
591
|
+
const listOpenPullRequests = Effect.fn("GitHubService.listOpenPullRequests")(function*(mode: PullRequestQueueMode, repository: string | null) {
|
|
592
|
+
return yield* paginatePages(mode, repository, listOpenPullRequestPage)
|
|
593
|
+
})
|
|
594
|
+
const listOpenPullRequestDetails = Effect.fn("GitHubService.listOpenPullRequestDetails")(function*(mode: PullRequestQueueMode, repository: string | null) {
|
|
595
|
+
return yield* paginatePages(mode, repository, listOpenPullRequestDetailsPage)
|
|
452
596
|
})
|
|
453
597
|
|
|
454
|
-
const
|
|
455
|
-
const
|
|
456
|
-
|
|
598
|
+
const getPullRequestDetails = Effect.fn("GitHubService.getPullRequestDetails")(function*(repository: string, number: number) {
|
|
599
|
+
const repo = repositoryParts(repository)
|
|
600
|
+
if (!repo) {
|
|
601
|
+
return yield* new CommandError({ command: "gh", args: [], detail: `Invalid repository: ${repository}`, cause: repository })
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const response = yield* command.runSchema(PullRequestDetailResponseSchema, "gh", [
|
|
605
|
+
"api", "graphql",
|
|
606
|
+
"-f", `query=${pullRequestDetailQuery}`,
|
|
607
|
+
"-F", `owner=${repo.owner}`,
|
|
608
|
+
"-F", `name=${repo.name}`,
|
|
609
|
+
"-F", `number=${number}`,
|
|
457
610
|
])
|
|
458
|
-
|
|
611
|
+
const pullRequest = response.data.repository?.pullRequest
|
|
612
|
+
if (!pullRequest) {
|
|
613
|
+
return yield* new CommandError({ command: "gh", args: [], detail: `Pull request not found: ${repository}#${number}`, cause: `${repository}#${number}` })
|
|
614
|
+
}
|
|
615
|
+
return parsePullRequest(pullRequest)
|
|
459
616
|
})
|
|
460
617
|
|
|
618
|
+
const getAuthenticatedUser = () =>
|
|
619
|
+
ghJson("getAuthenticatedUser", ViewerSchema, ["api", "user"]).pipe(Effect.map((viewer) => viewer.login))
|
|
620
|
+
|
|
621
|
+
const getPullRequestDiff = (repository: string, number: number) =>
|
|
622
|
+
ghJson("getPullRequestDiff", PullRequestFilesResponseSchema, [
|
|
623
|
+
"api", "--paginate", "--slurp", `repos/${repository}/pulls/${number}/files`,
|
|
624
|
+
]).pipe(Effect.map((response) => pullRequestFilesToPatch(parsePullRequestFiles(response))))
|
|
625
|
+
|
|
626
|
+
const listPullRequestComments = (repository: string, number: number) =>
|
|
627
|
+
ghJson("listPullRequestComments", CommentsResponseSchema, [
|
|
628
|
+
"api", "--paginate", "--slurp", `repos/${repository}/pulls/${number}/comments`,
|
|
629
|
+
]).pipe(Effect.map(parsePullRequestComments))
|
|
630
|
+
|
|
461
631
|
const getPullRequestMergeInfo = Effect.fn("GitHubService.getPullRequestMergeInfo")(function*(repository: string, number: number) {
|
|
462
632
|
const info = yield* command.runSchema(MergeInfoResponseSchema, "gh", [
|
|
463
633
|
"pr", "view", String(number), "--repo", repository,
|
|
@@ -479,14 +649,11 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
479
649
|
} satisfies PullRequestMergeInfo
|
|
480
650
|
})
|
|
481
651
|
|
|
482
|
-
const mergePullRequest =
|
|
483
|
-
|
|
484
|
-
yield* command.run("gh", [...base, ...getMergeActionDefinition(action).cliArgs])
|
|
485
|
-
})
|
|
652
|
+
const mergePullRequest = (repository: string, number: number, action: PullRequestMergeAction) =>
|
|
653
|
+
ghVoid("mergePullRequest", ["pr", "merge", String(number), "--repo", repository, ...getMergeActionDefinition(action).cliArgs])
|
|
486
654
|
|
|
487
|
-
const closePullRequest =
|
|
488
|
-
|
|
489
|
-
})
|
|
655
|
+
const closePullRequest = (repository: string, number: number) =>
|
|
656
|
+
ghVoid("closePullRequest", ["pr", "close", String(number), "--repo", repository])
|
|
490
657
|
|
|
491
658
|
const createPullRequestComment = Effect.fn("GitHubService.createPullRequestComment")(function*(input: CreatePullRequestCommentInput) {
|
|
492
659
|
const response = yield* command.runSchema(PullRequestCommentSchema, "gh", [
|
|
@@ -500,28 +667,25 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
500
667
|
return parsePullRequestComment(response) ?? fallbackCreatedComment(input)
|
|
501
668
|
})
|
|
502
669
|
|
|
503
|
-
const toggleDraftStatus =
|
|
504
|
-
|
|
505
|
-
})
|
|
670
|
+
const toggleDraftStatus = (repository: string, number: number, isDraft: boolean) =>
|
|
671
|
+
ghVoid("toggleDraftStatus", ["pr", "ready", String(number), "--repo", repository, ...(isDraft ? [] : ["--undo"])])
|
|
506
672
|
|
|
507
|
-
const listRepoLabels =
|
|
508
|
-
|
|
673
|
+
const listRepoLabels = (repository: string) =>
|
|
674
|
+
ghJson("listRepoLabels", RepoLabelsResponseSchema, [
|
|
509
675
|
"label", "list", "--repo", repository, "--json", "name,color", "--limit", "100",
|
|
510
|
-
])
|
|
511
|
-
return labels.map((label) => ({ name: label.name, color: `#${label.color}` }))
|
|
512
|
-
})
|
|
676
|
+
]).pipe(Effect.map((labels) => labels.map((label) => ({ name: label.name, color: `#${label.color}` }))))
|
|
513
677
|
|
|
514
|
-
const addPullRequestLabel =
|
|
515
|
-
|
|
516
|
-
})
|
|
678
|
+
const addPullRequestLabel = (repository: string, number: number, label: string) =>
|
|
679
|
+
ghVoid("addPullRequestLabel", ["pr", "edit", String(number), "--repo", repository, "--add-label", label])
|
|
517
680
|
|
|
518
|
-
const removePullRequestLabel =
|
|
519
|
-
|
|
520
|
-
})
|
|
681
|
+
const removePullRequestLabel = (repository: string, number: number, label: string) =>
|
|
682
|
+
ghVoid("removePullRequestLabel", ["pr", "edit", String(number), "--repo", repository, "--remove-label", label])
|
|
521
683
|
|
|
522
684
|
return GitHubService.of({
|
|
523
685
|
listOpenPullRequests,
|
|
686
|
+
listOpenPullRequestPage,
|
|
524
687
|
listOpenPullRequestDetails,
|
|
688
|
+
getPullRequestDetails,
|
|
525
689
|
getAuthenticatedUser,
|
|
526
690
|
getPullRequestDiff,
|
|
527
691
|
listPullRequestComments,
|