@kitlangton/ghui 0.1.16 → 0.1.18
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 +29 -27
- package/package.json +2 -1
- package/src/App.tsx +1006 -132
- package/src/domain.ts +47 -1
- package/src/services/CommandRunner.ts +8 -1
- package/src/services/GitHubService.ts +286 -184
- package/src/ui/DetailsPane.tsx +26 -25
- package/src/ui/FooterHints.tsx +60 -0
- package/src/ui/PullRequestDiffPane.tsx +105 -33
- package/src/ui/PullRequestList.tsx +38 -13
- package/src/ui/colors.ts +41 -0
- package/src/ui/commentEditor.ts +126 -0
- package/src/ui/diff.ts +204 -7
- package/src/ui/modals.tsx +322 -8
- package/src/ui/pullRequests.ts +2 -0
|
@@ -1,71 +1,130 @@
|
|
|
1
|
-
import { Context, Effect, Layer } from "effect"
|
|
1
|
+
import { Context, Effect, Layer, Schema } from "effect"
|
|
2
2
|
import { config } from "../config.js"
|
|
3
|
-
import type
|
|
3
|
+
import { pullRequestQueueSearchQualifier, type CheckItem, type CreatePullRequestCommentInput, type PullRequestItem, type PullRequestMergeAction, type PullRequestMergeInfo, type PullRequestQueueMode, type PullRequestReviewComment } from "../domain.js"
|
|
4
4
|
import { getMergeActionDefinition } from "../mergeActions.js"
|
|
5
5
|
import { CommandRunner, type CommandError, type JsonParseError } from "./CommandRunner.js"
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
readonly isDraft: boolean
|
|
11
|
-
readonly reviewDecision: string | null
|
|
12
|
-
readonly autoMergeRequest: unknown | null
|
|
13
|
-
readonly state: string
|
|
14
|
-
readonly createdAt: string
|
|
15
|
-
readonly closedAt?: string | null
|
|
16
|
-
readonly url: string
|
|
17
|
-
readonly repository: {
|
|
18
|
-
readonly nameWithOwner: string
|
|
19
|
-
}
|
|
20
|
-
}
|
|
7
|
+
const NullableString = Schema.NullOr(Schema.String)
|
|
8
|
+
const OptionalNullableString = Schema.optional(NullableString)
|
|
9
|
+
const OptionalNullableNumber = Schema.optional(Schema.NullOr(Schema.Number))
|
|
21
10
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}[]
|
|
29
|
-
}
|
|
30
|
-
readonly additions: number
|
|
31
|
-
readonly deletions: number
|
|
32
|
-
readonly changedFiles: number
|
|
33
|
-
readonly statusCheckRollup?: {
|
|
34
|
-
readonly contexts: {
|
|
35
|
-
readonly nodes: readonly GraphQLCheckContext[]
|
|
36
|
-
}
|
|
37
|
-
} | null
|
|
38
|
-
}
|
|
11
|
+
const RawCheckRunSchema = Schema.Struct({
|
|
12
|
+
__typename: Schema.Literal("CheckRun"),
|
|
13
|
+
name: OptionalNullableString,
|
|
14
|
+
status: OptionalNullableString,
|
|
15
|
+
conclusion: OptionalNullableString,
|
|
16
|
+
})
|
|
39
17
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
readonly conclusion?: string | null
|
|
46
|
-
}
|
|
47
|
-
| {
|
|
48
|
-
readonly __typename: "StatusContext"
|
|
49
|
-
readonly context?: string | null
|
|
50
|
-
readonly state?: string | null
|
|
51
|
-
}
|
|
18
|
+
const RawStatusContextSchema = Schema.Struct({
|
|
19
|
+
__typename: Schema.Literal("StatusContext"),
|
|
20
|
+
context: OptionalNullableString,
|
|
21
|
+
state: OptionalNullableString,
|
|
22
|
+
})
|
|
52
23
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
24
|
+
const RawCheckContextSchema = Schema.Union([RawCheckRunSchema, RawStatusContextSchema])
|
|
25
|
+
|
|
26
|
+
const RawAuthorSchema = Schema.Struct({ login: Schema.String })
|
|
27
|
+
const RawRepositorySchema = Schema.Struct({ nameWithOwner: Schema.String })
|
|
28
|
+
const RawLabelSchema = Schema.Struct({
|
|
29
|
+
name: Schema.String,
|
|
30
|
+
color: OptionalNullableString,
|
|
31
|
+
})
|
|
64
32
|
|
|
65
|
-
|
|
33
|
+
const RawPullRequestSummaryFields = {
|
|
34
|
+
number: Schema.Number,
|
|
35
|
+
title: Schema.String,
|
|
36
|
+
isDraft: Schema.Boolean,
|
|
37
|
+
reviewDecision: NullableString,
|
|
38
|
+
autoMergeRequest: Schema.NullOr(Schema.Unknown),
|
|
39
|
+
state: Schema.String,
|
|
40
|
+
merged: Schema.Boolean,
|
|
41
|
+
createdAt: Schema.String,
|
|
42
|
+
closedAt: OptionalNullableString,
|
|
43
|
+
url: Schema.String,
|
|
44
|
+
author: RawAuthorSchema,
|
|
45
|
+
headRefOid: Schema.String,
|
|
46
|
+
repository: RawRepositorySchema,
|
|
47
|
+
} as const
|
|
48
|
+
|
|
49
|
+
const RawPullRequestSummaryNodeSchema = Schema.Struct(RawPullRequestSummaryFields)
|
|
50
|
+
|
|
51
|
+
const RawPullRequestNodeSchema = Schema.Struct({
|
|
52
|
+
...RawPullRequestSummaryFields,
|
|
53
|
+
body: Schema.String,
|
|
54
|
+
labels: Schema.Struct({ nodes: Schema.Array(RawLabelSchema) }),
|
|
55
|
+
additions: Schema.Number,
|
|
56
|
+
deletions: Schema.Number,
|
|
57
|
+
changedFiles: Schema.Number,
|
|
58
|
+
statusCheckRollup: Schema.optional(Schema.NullOr(Schema.Struct({
|
|
59
|
+
contexts: Schema.Struct({ nodes: Schema.Array(RawCheckContextSchema) }),
|
|
60
|
+
}))),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const PageInfoSchema = Schema.Struct({
|
|
64
|
+
hasNextPage: Schema.Boolean,
|
|
65
|
+
endCursor: NullableString,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const SearchResponseSchema = <Item extends Schema.Top>(item: Item) =>
|
|
69
|
+
Schema.Struct({
|
|
70
|
+
data: Schema.Struct({
|
|
71
|
+
search: Schema.Struct({
|
|
72
|
+
nodes: Schema.Array(Schema.NullOr(item)),
|
|
73
|
+
pageInfo: PageInfoSchema,
|
|
74
|
+
}),
|
|
75
|
+
}),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const ViewerSchema = Schema.Struct({ login: Schema.String })
|
|
79
|
+
|
|
80
|
+
const MergeInfoResponseSchema = Schema.Struct({
|
|
81
|
+
number: Schema.Number,
|
|
82
|
+
title: Schema.String,
|
|
83
|
+
state: Schema.String,
|
|
84
|
+
isDraft: Schema.Boolean,
|
|
85
|
+
mergeable: Schema.String,
|
|
86
|
+
reviewDecision: NullableString,
|
|
87
|
+
autoMergeRequest: Schema.NullOr(Schema.Unknown),
|
|
88
|
+
statusCheckRollup: Schema.Array(RawCheckContextSchema),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const DiffCommentSideSchema = Schema.Union([Schema.Literal("LEFT"), Schema.Literal("RIGHT")])
|
|
92
|
+
|
|
93
|
+
const PullRequestCommentSchema = Schema.Struct({
|
|
94
|
+
id: Schema.optional(Schema.NullOr(Schema.Union([Schema.Number, Schema.String]))),
|
|
95
|
+
node_id: OptionalNullableString,
|
|
96
|
+
body: OptionalNullableString,
|
|
97
|
+
html_url: OptionalNullableString,
|
|
98
|
+
url: OptionalNullableString,
|
|
99
|
+
created_at: OptionalNullableString,
|
|
100
|
+
user: Schema.optional(Schema.NullOr(Schema.Struct({
|
|
101
|
+
login: OptionalNullableString,
|
|
102
|
+
}))),
|
|
103
|
+
path: OptionalNullableString,
|
|
104
|
+
line: OptionalNullableNumber,
|
|
105
|
+
original_line: OptionalNullableNumber,
|
|
106
|
+
side: Schema.optional(Schema.NullOr(DiffCommentSideSchema)),
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const CommentsResponseSchema = Schema.Union([
|
|
110
|
+
Schema.Array(PullRequestCommentSchema),
|
|
111
|
+
Schema.Array(Schema.Array(PullRequestCommentSchema)),
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
const RepoLabelsResponseSchema = Schema.Array(Schema.Struct({
|
|
115
|
+
name: Schema.String,
|
|
116
|
+
color: Schema.String,
|
|
117
|
+
}))
|
|
118
|
+
|
|
119
|
+
type RawPullRequestSummaryNode = Schema.Schema.Type<typeof RawPullRequestSummaryNodeSchema>
|
|
120
|
+
type RawPullRequestNode = Schema.Schema.Type<typeof RawPullRequestNodeSchema>
|
|
121
|
+
type RawCheckContext = Schema.Schema.Type<typeof RawCheckContextSchema>
|
|
122
|
+
type RawPullRequestComment = Schema.Schema.Type<typeof PullRequestCommentSchema>
|
|
123
|
+
|
|
124
|
+
type SearchResponse<Item> = {
|
|
66
125
|
readonly data: {
|
|
67
126
|
readonly search: {
|
|
68
|
-
readonly nodes: readonly (
|
|
127
|
+
readonly nodes: readonly (Item | null)[]
|
|
69
128
|
readonly pageInfo: {
|
|
70
129
|
readonly hasNextPage: boolean
|
|
71
130
|
readonly endCursor: string | null
|
|
@@ -74,21 +133,6 @@ interface GraphQLSearchSummaryResponse {
|
|
|
74
133
|
}
|
|
75
134
|
}
|
|
76
135
|
|
|
77
|
-
interface GitHubViewer {
|
|
78
|
-
readonly login: string
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
interface GitHubMergeInfoResponse {
|
|
82
|
-
readonly number: number
|
|
83
|
-
readonly title: string
|
|
84
|
-
readonly state: string
|
|
85
|
-
readonly isDraft: boolean
|
|
86
|
-
readonly mergeable: string
|
|
87
|
-
readonly reviewDecision: string | null
|
|
88
|
-
readonly autoMergeRequest: unknown | null
|
|
89
|
-
readonly statusCheckRollup: readonly GraphQLCheckContext[]
|
|
90
|
-
}
|
|
91
|
-
|
|
92
136
|
const pullRequestSearchQuery = `
|
|
93
137
|
query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
94
138
|
search(query: $searchQuery, type: ISSUE, first: $first, after: $after) {
|
|
@@ -104,9 +148,12 @@ query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
|
104
148
|
deletions
|
|
105
149
|
changedFiles
|
|
106
150
|
state
|
|
151
|
+
merged
|
|
107
152
|
createdAt
|
|
108
153
|
closedAt
|
|
109
154
|
url
|
|
155
|
+
author { login }
|
|
156
|
+
headRefOid
|
|
110
157
|
repository { nameWithOwner }
|
|
111
158
|
labels(first: 20) { nodes { name color } }
|
|
112
159
|
statusCheckRollup {
|
|
@@ -136,9 +183,12 @@ query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
|
136
183
|
reviewDecision
|
|
137
184
|
autoMergeRequest { enabledAt }
|
|
138
185
|
state
|
|
186
|
+
merged
|
|
139
187
|
createdAt
|
|
140
188
|
closedAt
|
|
141
189
|
url
|
|
190
|
+
author { login }
|
|
191
|
+
headRefOid
|
|
142
192
|
repository { nameWithOwner }
|
|
143
193
|
}
|
|
144
194
|
}
|
|
@@ -152,45 +202,57 @@ const normalizeDate = (value: string | null | undefined) => {
|
|
|
152
202
|
return new Date(value)
|
|
153
203
|
}
|
|
154
204
|
|
|
205
|
+
const getPullRequestState = (item: { readonly state: string; readonly merged: boolean }): PullRequestItem["state"] =>
|
|
206
|
+
item.merged ? "merged" : item.state.toLowerCase() === "open" ? "open" : "closed"
|
|
207
|
+
|
|
208
|
+
const REVIEW_STATUS_BY_DECISION: Record<string, PullRequestItem["reviewStatus"]> = {
|
|
209
|
+
APPROVED: "approved",
|
|
210
|
+
CHANGES_REQUESTED: "changes",
|
|
211
|
+
REVIEW_REQUIRED: "review",
|
|
212
|
+
}
|
|
213
|
+
|
|
155
214
|
const getReviewStatus = (item: { readonly isDraft: boolean; readonly reviewDecision: string | null }): PullRequestItem["reviewStatus"] => {
|
|
156
215
|
if (item.isDraft) return "draft"
|
|
157
|
-
if (item.reviewDecision
|
|
158
|
-
if (item.reviewDecision === "CHANGES_REQUESTED") return "changes"
|
|
159
|
-
if (item.reviewDecision === "REVIEW_REQUIRED") return "review"
|
|
216
|
+
if (item.reviewDecision) return REVIEW_STATUS_BY_DECISION[item.reviewDecision] ?? "none"
|
|
160
217
|
return "none"
|
|
161
218
|
}
|
|
162
219
|
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return "pending"
|
|
220
|
+
const CHECK_STATUS_BY_RAW: Record<string, CheckItem["status"]> = {
|
|
221
|
+
COMPLETED: "completed",
|
|
222
|
+
IN_PROGRESS: "in_progress",
|
|
223
|
+
QUEUED: "queued",
|
|
168
224
|
}
|
|
169
225
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
226
|
+
const CHECK_CONCLUSION_BY_RAW: Record<string, NonNullable<CheckItem["conclusion"]>> = {
|
|
227
|
+
SUCCESS: "success",
|
|
228
|
+
FAILURE: "failure",
|
|
229
|
+
ERROR: "failure",
|
|
230
|
+
NEUTRAL: "neutral",
|
|
231
|
+
SKIPPED: "skipped",
|
|
232
|
+
CANCELLED: "cancelled",
|
|
233
|
+
TIMED_OUT: "timed_out",
|
|
178
234
|
}
|
|
179
235
|
|
|
180
|
-
const
|
|
236
|
+
const normalizeCheckStatus = (raw: string | null | undefined): CheckItem["status"] =>
|
|
237
|
+
raw ? CHECK_STATUS_BY_RAW[raw] ?? "pending" : "pending"
|
|
238
|
+
|
|
239
|
+
const normalizeCheckConclusion = (raw: string | null | undefined): CheckItem["conclusion"] =>
|
|
240
|
+
raw ? CHECK_CONCLUSION_BY_RAW[raw] ?? null : null
|
|
241
|
+
|
|
242
|
+
const getContextStatus = (context: RawCheckContext): CheckItem["status"] => {
|
|
181
243
|
if (context.__typename === "CheckRun") return normalizeCheckStatus(context.status)
|
|
182
244
|
if (context.state === "PENDING") return "in_progress"
|
|
183
245
|
return "completed"
|
|
184
246
|
}
|
|
185
247
|
|
|
186
|
-
const getContextConclusion = (context:
|
|
248
|
+
const getContextConclusion = (context: RawCheckContext): CheckItem["conclusion"] => {
|
|
187
249
|
if (context.__typename === "CheckRun") return normalizeCheckConclusion(context.conclusion)
|
|
188
250
|
if (context.state === "SUCCESS") return "success"
|
|
189
251
|
if (context.state === "FAILURE" || context.state === "ERROR") return "failure"
|
|
190
252
|
return null
|
|
191
253
|
}
|
|
192
254
|
|
|
193
|
-
const getCheckInfoFromContexts = (contexts: readonly
|
|
255
|
+
const getCheckInfoFromContexts = (contexts: readonly RawCheckContext[]): Pick<PullRequestItem, "checkStatus" | "checkSummary" | "checks"> => {
|
|
194
256
|
if (contexts.length === 0) {
|
|
195
257
|
return { checkStatus: "none", checkSummary: null, checks: [] }
|
|
196
258
|
}
|
|
@@ -232,39 +294,10 @@ const getCheckInfoFromContexts = (contexts: readonly GraphQLCheckContext[]): Pic
|
|
|
232
294
|
return { checkStatus: "passing", checkSummary: `checks ${successful}/${contexts.length}`, checks }
|
|
233
295
|
}
|
|
234
296
|
|
|
235
|
-
const
|
|
236
|
-
getCheckInfoFromContexts(item.statusCheckRollup?.contexts.nodes ?? [])
|
|
237
|
-
|
|
238
|
-
const parsePullRequest = (item: GitHubPullRequestNode): PullRequestItem => {
|
|
239
|
-
const checkInfo = getCheckInfo(item)
|
|
240
|
-
|
|
241
|
-
return {
|
|
242
|
-
repository: item.repository.nameWithOwner,
|
|
243
|
-
number: item.number,
|
|
244
|
-
title: item.title,
|
|
245
|
-
body: item.body,
|
|
246
|
-
labels: item.labels.nodes.map((label) => ({
|
|
247
|
-
name: label.name,
|
|
248
|
-
color: label.color ? `#${label.color}` : null,
|
|
249
|
-
})),
|
|
250
|
-
additions: item.additions,
|
|
251
|
-
deletions: item.deletions,
|
|
252
|
-
changedFiles: item.changedFiles,
|
|
253
|
-
state: item.state.toLowerCase() === "open" ? "open" : "closed",
|
|
254
|
-
reviewStatus: getReviewStatus(item),
|
|
255
|
-
checkStatus: checkInfo.checkStatus,
|
|
256
|
-
checkSummary: checkInfo.checkSummary,
|
|
257
|
-
checks: checkInfo.checks,
|
|
258
|
-
autoMergeEnabled: item.autoMergeRequest !== null,
|
|
259
|
-
detailLoaded: true,
|
|
260
|
-
createdAt: new Date(item.createdAt),
|
|
261
|
-
closedAt: normalizeDate(item.closedAt),
|
|
262
|
-
url: item.url,
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const parsePullRequestSummary = (item: GitHubPullRequestSummaryNode): PullRequestItem => ({
|
|
297
|
+
const parsePullRequestSummary = (item: RawPullRequestSummaryNode): PullRequestItem => ({
|
|
267
298
|
repository: item.repository.nameWithOwner,
|
|
299
|
+
author: item.author.login,
|
|
300
|
+
headRefOid: item.headRefOid,
|
|
268
301
|
number: item.number,
|
|
269
302
|
title: item.title,
|
|
270
303
|
body: "",
|
|
@@ -272,7 +305,7 @@ const parsePullRequestSummary = (item: GitHubPullRequestSummaryNode): PullReques
|
|
|
272
305
|
additions: 0,
|
|
273
306
|
deletions: 0,
|
|
274
307
|
changedFiles: 0,
|
|
275
|
-
state: item
|
|
308
|
+
state: getPullRequestState(item),
|
|
276
309
|
reviewStatus: getReviewStatus(item),
|
|
277
310
|
checkStatus: "none",
|
|
278
311
|
checkSummary: null,
|
|
@@ -284,23 +317,86 @@ const parsePullRequestSummary = (item: GitHubPullRequestSummaryNode): PullReques
|
|
|
284
317
|
url: item.url,
|
|
285
318
|
})
|
|
286
319
|
|
|
287
|
-
const
|
|
320
|
+
const parsePullRequest = (item: RawPullRequestNode): PullRequestItem => {
|
|
321
|
+
const checkInfo = getCheckInfoFromContexts(item.statusCheckRollup?.contexts.nodes ?? [])
|
|
322
|
+
return {
|
|
323
|
+
...parsePullRequestSummary(item),
|
|
324
|
+
body: item.body,
|
|
325
|
+
labels: item.labels.nodes.map((label) => ({
|
|
326
|
+
name: label.name,
|
|
327
|
+
color: label.color ? `#${label.color}` : null,
|
|
328
|
+
})),
|
|
329
|
+
additions: item.additions,
|
|
330
|
+
deletions: item.deletions,
|
|
331
|
+
changedFiles: item.changedFiles,
|
|
332
|
+
checkStatus: checkInfo.checkStatus,
|
|
333
|
+
checkSummary: checkInfo.checkSummary,
|
|
334
|
+
checks: checkInfo.checks,
|
|
335
|
+
detailLoaded: true,
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const searchQuery = (mode: PullRequestQueueMode, author: string) => `${pullRequestQueueSearchQualifier(mode, author)} is:pr is:open sort:created-desc`
|
|
288
340
|
|
|
289
|
-
|
|
341
|
+
const sortNewestFirst = (pullRequests: readonly PullRequestItem[]) =>
|
|
342
|
+
[...pullRequests].sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime())
|
|
290
343
|
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
return
|
|
344
|
+
const parsePullRequestComment = (comment: RawPullRequestComment): PullRequestReviewComment | null => {
|
|
345
|
+
const line = comment.line ?? comment.original_line
|
|
346
|
+
if (!comment.path || !line || (comment.side !== "LEFT" && comment.side !== "RIGHT")) return null
|
|
347
|
+
return {
|
|
348
|
+
id: String(comment.id ?? comment.node_id ?? `${comment.path}:${comment.side}:${line}:${comment.created_at ?? ""}:${comment.body ?? ""}`),
|
|
349
|
+
path: comment.path,
|
|
350
|
+
line,
|
|
351
|
+
side: comment.side,
|
|
352
|
+
author: comment.user?.login ?? "unknown",
|
|
353
|
+
body: comment.body ?? "",
|
|
354
|
+
createdAt: comment.created_at ? new Date(comment.created_at) : null,
|
|
355
|
+
url: comment.html_url ?? comment.url ?? null,
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const parsePullRequestComments = (response: Schema.Schema.Type<typeof CommentsResponseSchema>): readonly PullRequestReviewComment[] => {
|
|
360
|
+
const pages: readonly (readonly RawPullRequestComment[])[] = Array.isArray(response[0])
|
|
361
|
+
? response as readonly (readonly RawPullRequestComment[])[]
|
|
362
|
+
: [response as readonly RawPullRequestComment[]]
|
|
363
|
+
return pages.flatMap((page) => page.flatMap((comment) => {
|
|
364
|
+
const parsed = parsePullRequestComment(comment)
|
|
365
|
+
return parsed ? [parsed] : []
|
|
366
|
+
}))
|
|
295
367
|
}
|
|
296
368
|
|
|
369
|
+
const fallbackCreatedComment = (input: CreatePullRequestCommentInput): PullRequestReviewComment => ({
|
|
370
|
+
id: `created:${input.repository}:${input.number}:${input.path}:${input.side}:${input.line}:${Date.now()}`,
|
|
371
|
+
path: input.path,
|
|
372
|
+
line: input.line,
|
|
373
|
+
side: input.side,
|
|
374
|
+
author: config.author.replace(/^@/, "") || "you",
|
|
375
|
+
body: input.body,
|
|
376
|
+
createdAt: new Date(),
|
|
377
|
+
url: null,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
export type GitHubError = CommandError | JsonParseError | Schema.SchemaError
|
|
381
|
+
|
|
382
|
+
const MERGEABLE_BY_RAW: Record<string, PullRequestMergeInfo["mergeable"]> = {
|
|
383
|
+
MERGEABLE: "mergeable",
|
|
384
|
+
CONFLICTING: "conflicting",
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const normalizeMergeable = (value: string): PullRequestMergeInfo["mergeable"] =>
|
|
388
|
+
MERGEABLE_BY_RAW[value] ?? "unknown"
|
|
389
|
+
|
|
297
390
|
export class GitHubService extends Context.Service<GitHubService, {
|
|
298
|
-
readonly listOpenPullRequests: () => Effect.Effect<readonly PullRequestItem[], GitHubError>
|
|
299
|
-
readonly listOpenPullRequestDetails: () => Effect.Effect<readonly PullRequestItem[], GitHubError>
|
|
391
|
+
readonly listOpenPullRequests: (mode: PullRequestQueueMode) => Effect.Effect<readonly PullRequestItem[], GitHubError>
|
|
392
|
+
readonly listOpenPullRequestDetails: (mode: PullRequestQueueMode) => Effect.Effect<readonly PullRequestItem[], GitHubError>
|
|
300
393
|
readonly getAuthenticatedUser: () => Effect.Effect<string, GitHubError>
|
|
301
394
|
readonly getPullRequestDiff: (repository: string, number: number) => Effect.Effect<string, CommandError>
|
|
395
|
+
readonly listPullRequestComments: (repository: string, number: number) => Effect.Effect<readonly PullRequestReviewComment[], GitHubError>
|
|
302
396
|
readonly getPullRequestMergeInfo: (repository: string, number: number) => Effect.Effect<PullRequestMergeInfo, GitHubError>
|
|
303
397
|
readonly mergePullRequest: (repository: string, number: number, action: PullRequestMergeAction) => Effect.Effect<void, CommandError>
|
|
398
|
+
readonly closePullRequest: (repository: string, number: number) => Effect.Effect<void, CommandError>
|
|
399
|
+
readonly createPullRequestComment: (input: CreatePullRequestCommentInput) => Effect.Effect<PullRequestReviewComment, GitHubError>
|
|
304
400
|
readonly toggleDraftStatus: (repository: string, number: number, isDraft: boolean) => Effect.Effect<void, CommandError>
|
|
305
401
|
readonly listRepoLabels: (repository: string) => Effect.Effect<readonly { readonly name: string; readonly color: string | null }[], GitHubError>
|
|
306
402
|
readonly addPullRequestLabel: (repository: string, number: number, label: string) => Effect.Effect<void, CommandError>
|
|
@@ -311,60 +407,40 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
311
407
|
Effect.gen(function*() {
|
|
312
408
|
const command = yield* CommandRunner
|
|
313
409
|
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
"
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
410
|
+
const paginateSearch = <Item extends Schema.Top>(label: string, query: string, schema: Item, parse: (node: Item["Type"]) => PullRequestItem) => {
|
|
411
|
+
const responseSchema = SearchResponseSchema(schema)
|
|
412
|
+
return Effect.fn(`GitHubService.${label}`)(function*(mode: PullRequestQueueMode) {
|
|
413
|
+
const pullRequests: PullRequestItem[] = []
|
|
414
|
+
let cursor: string | null = null
|
|
415
|
+
|
|
416
|
+
while (pullRequests.length < config.prFetchLimit) {
|
|
417
|
+
const pageSize = Math.min(100, config.prFetchLimit - pullRequests.length)
|
|
418
|
+
const response: SearchResponse<Item["Type"]> = yield* command.runSchema(responseSchema, "gh", [
|
|
419
|
+
"api", "graphql",
|
|
420
|
+
"-f", `query=${query}`,
|
|
421
|
+
"-F", `searchQuery=${searchQuery(mode, config.author)}`,
|
|
422
|
+
"-F", `first=${pageSize}`,
|
|
423
|
+
...(cursor ? ["-F", `after=${cursor}`] : []),
|
|
424
|
+
])
|
|
425
|
+
|
|
426
|
+
for (const node of response.data.search.nodes) {
|
|
427
|
+
if (node) pullRequests.push(parse(node))
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!response.data.search.pageInfo.hasNextPage) break
|
|
431
|
+
cursor = response.data.search.pageInfo.endCursor
|
|
432
|
+
if (!cursor) break
|
|
330
433
|
}
|
|
331
434
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return pullRequests.sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime())
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
const listOpenPullRequestDetails = Effect.fn("GitHubService.listOpenPullRequestDetails")(function*() {
|
|
341
|
-
const pullRequests: PullRequestItem[] = []
|
|
342
|
-
let cursor: string | null = null
|
|
343
|
-
|
|
344
|
-
while (pullRequests.length < config.prFetchLimit) {
|
|
345
|
-
const pageSize = Math.min(100, config.prFetchLimit - pullRequests.length)
|
|
346
|
-
const response: GraphQLSearchResponse = yield* command.runJson<GraphQLSearchResponse>("gh", [
|
|
347
|
-
"api", "graphql",
|
|
348
|
-
"-f", `query=${pullRequestSearchQuery}`,
|
|
349
|
-
"-F", `searchQuery=${searchQuery(config.author)}`,
|
|
350
|
-
"-F", `first=${pageSize}`,
|
|
351
|
-
...(cursor ? ["-F", `after=${cursor}`] : []),
|
|
352
|
-
])
|
|
353
|
-
|
|
354
|
-
for (const node of response.data.search.nodes) {
|
|
355
|
-
if (node) pullRequests.push(parsePullRequest(node))
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (!response.data.search.pageInfo.hasNextPage) break
|
|
359
|
-
cursor = response.data.search.pageInfo.endCursor
|
|
360
|
-
if (!cursor) break
|
|
361
|
-
}
|
|
435
|
+
return sortNewestFirst(pullRequests)
|
|
436
|
+
})
|
|
437
|
+
}
|
|
362
438
|
|
|
363
|
-
|
|
364
|
-
|
|
439
|
+
const listOpenPullRequests = paginateSearch("listOpenPullRequests", pullRequestSummarySearchQuery, RawPullRequestSummaryNodeSchema, parsePullRequestSummary)
|
|
440
|
+
const listOpenPullRequestDetails = paginateSearch("listOpenPullRequestDetails", pullRequestSearchQuery, RawPullRequestNodeSchema, parsePullRequest)
|
|
365
441
|
|
|
366
442
|
const getAuthenticatedUser = Effect.fn("GitHubService.getAuthenticatedUser")(function*() {
|
|
367
|
-
const viewer = yield* command.
|
|
443
|
+
const viewer = yield* command.runSchema(ViewerSchema, "gh", ["api", "user"])
|
|
368
444
|
return viewer.login
|
|
369
445
|
})
|
|
370
446
|
|
|
@@ -373,8 +449,15 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
373
449
|
return result.stdout
|
|
374
450
|
})
|
|
375
451
|
|
|
452
|
+
const listPullRequestComments = Effect.fn("GitHubService.listPullRequestComments")(function*(repository: string, number: number) {
|
|
453
|
+
const response = yield* command.runSchema(CommentsResponseSchema, "gh", [
|
|
454
|
+
"api", "--paginate", "--slurp", `repos/${repository}/pulls/${number}/comments`,
|
|
455
|
+
])
|
|
456
|
+
return parsePullRequestComments(response)
|
|
457
|
+
})
|
|
458
|
+
|
|
376
459
|
const getPullRequestMergeInfo = Effect.fn("GitHubService.getPullRequestMergeInfo")(function*(repository: string, number: number) {
|
|
377
|
-
const info = yield* command.
|
|
460
|
+
const info = yield* command.runSchema(MergeInfoResponseSchema, "gh", [
|
|
378
461
|
"pr", "view", String(number), "--repo", repository,
|
|
379
462
|
"--json", "number,title,state,isDraft,mergeable,reviewDecision,autoMergeRequest,statusCheckRollup",
|
|
380
463
|
])
|
|
@@ -399,12 +482,28 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
399
482
|
yield* command.run("gh", [...base, ...getMergeActionDefinition(action).cliArgs])
|
|
400
483
|
})
|
|
401
484
|
|
|
485
|
+
const closePullRequest = Effect.fn("GitHubService.closePullRequest")(function*(repository: string, number: number) {
|
|
486
|
+
yield* command.run("gh", ["pr", "close", String(number), "--repo", repository])
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
const createPullRequestComment = Effect.fn("GitHubService.createPullRequestComment")(function*(input: CreatePullRequestCommentInput) {
|
|
490
|
+
const response = yield* command.runSchema(PullRequestCommentSchema, "gh", [
|
|
491
|
+
"api", "--method", "POST", `repos/${input.repository}/pulls/${input.number}/comments`,
|
|
492
|
+
"-f", `body=${input.body}`,
|
|
493
|
+
"-f", `commit_id=${input.commitId}`,
|
|
494
|
+
"-f", `path=${input.path}`,
|
|
495
|
+
"-F", `line=${input.line}`,
|
|
496
|
+
"-f", `side=${input.side}`,
|
|
497
|
+
])
|
|
498
|
+
return parsePullRequestComment(response) ?? fallbackCreatedComment(input)
|
|
499
|
+
})
|
|
500
|
+
|
|
402
501
|
const toggleDraftStatus = Effect.fn("GitHubService.toggleDraftStatus")(function*(repository: string, number: number, isDraft: boolean) {
|
|
403
502
|
yield* command.run("gh", ["pr", "ready", String(number), "--repo", repository, ...(isDraft ? [] : ["--undo"])])
|
|
404
503
|
})
|
|
405
504
|
|
|
406
505
|
const listRepoLabels = Effect.fn("GitHubService.listRepoLabels")(function*(repository: string) {
|
|
407
|
-
const labels = yield* command.
|
|
506
|
+
const labels = yield* command.runSchema(RepoLabelsResponseSchema, "gh", [
|
|
408
507
|
"label", "list", "--repo", repository, "--json", "name,color", "--limit", "100",
|
|
409
508
|
])
|
|
410
509
|
return labels.map((label) => ({ name: label.name, color: `#${label.color}` }))
|
|
@@ -423,8 +522,11 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
423
522
|
listOpenPullRequestDetails,
|
|
424
523
|
getAuthenticatedUser,
|
|
425
524
|
getPullRequestDiff,
|
|
525
|
+
listPullRequestComments,
|
|
426
526
|
getPullRequestMergeInfo,
|
|
427
527
|
mergePullRequest,
|
|
528
|
+
closePullRequest,
|
|
529
|
+
createPullRequestComment,
|
|
428
530
|
toggleDraftStatus,
|
|
429
531
|
listRepoLabels,
|
|
430
532
|
addPullRequestLabel,
|