@kitlangton/ghui 0.1.17 → 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 +11 -4
- package/package.json +2 -1
- package/src/App.tsx +879 -141
- package/src/domain.ts +46 -0
- package/src/services/CommandRunner.ts +8 -1
- package/src/services/GitHubService.ts +280 -184
- package/src/ui/DetailsPane.tsx +26 -25
- package/src/ui/FooterHints.tsx +52 -0
- package/src/ui/PullRequestDiffPane.tsx +105 -33
- package/src/ui/PullRequestList.tsx +28 -9
- 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 +236 -9
|
@@ -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
|
+
})
|
|
32
|
+
|
|
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>
|
|
64
123
|
|
|
65
|
-
|
|
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,24 +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
|
+
}
|
|
288
338
|
|
|
289
|
-
|
|
339
|
+
const searchQuery = (mode: PullRequestQueueMode, author: string) => `${pullRequestQueueSearchQualifier(mode, author)} is:pr is:open sort:created-desc`
|
|
290
340
|
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
341
|
+
const sortNewestFirst = (pullRequests: readonly PullRequestItem[]) =>
|
|
342
|
+
[...pullRequests].sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime())
|
|
343
|
+
|
|
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
|
+
}))
|
|
367
|
+
}
|
|
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",
|
|
295
385
|
}
|
|
296
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>
|
|
304
398
|
readonly closePullRequest: (repository: string, number: number) => Effect.Effect<void, CommandError>
|
|
399
|
+
readonly createPullRequestComment: (input: CreatePullRequestCommentInput) => Effect.Effect<PullRequestReviewComment, GitHubError>
|
|
305
400
|
readonly toggleDraftStatus: (repository: string, number: number, isDraft: boolean) => Effect.Effect<void, CommandError>
|
|
306
401
|
readonly listRepoLabels: (repository: string) => Effect.Effect<readonly { readonly name: string; readonly color: string | null }[], GitHubError>
|
|
307
402
|
readonly addPullRequestLabel: (repository: string, number: number, label: string) => Effect.Effect<void, CommandError>
|
|
@@ -312,60 +407,40 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
312
407
|
Effect.gen(function*() {
|
|
313
408
|
const command = yield* CommandRunner
|
|
314
409
|
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
"
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return pullRequests.sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime())
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
const listOpenPullRequestDetails = Effect.fn("GitHubService.listOpenPullRequestDetails")(function*() {
|
|
342
|
-
const pullRequests: PullRequestItem[] = []
|
|
343
|
-
let cursor: string | null = null
|
|
344
|
-
|
|
345
|
-
while (pullRequests.length < config.prFetchLimit) {
|
|
346
|
-
const pageSize = Math.min(100, config.prFetchLimit - pullRequests.length)
|
|
347
|
-
const response: GraphQLSearchResponse = yield* command.runJson<GraphQLSearchResponse>("gh", [
|
|
348
|
-
"api", "graphql",
|
|
349
|
-
"-f", `query=${pullRequestSearchQuery}`,
|
|
350
|
-
"-F", `searchQuery=${searchQuery(config.author)}`,
|
|
351
|
-
"-F", `first=${pageSize}`,
|
|
352
|
-
...(cursor ? ["-F", `after=${cursor}`] : []),
|
|
353
|
-
])
|
|
354
|
-
|
|
355
|
-
for (const node of response.data.search.nodes) {
|
|
356
|
-
if (node) pullRequests.push(parsePullRequest(node))
|
|
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
|
|
357
433
|
}
|
|
358
434
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
435
|
+
return sortNewestFirst(pullRequests)
|
|
436
|
+
})
|
|
437
|
+
}
|
|
363
438
|
|
|
364
|
-
|
|
365
|
-
|
|
439
|
+
const listOpenPullRequests = paginateSearch("listOpenPullRequests", pullRequestSummarySearchQuery, RawPullRequestSummaryNodeSchema, parsePullRequestSummary)
|
|
440
|
+
const listOpenPullRequestDetails = paginateSearch("listOpenPullRequestDetails", pullRequestSearchQuery, RawPullRequestNodeSchema, parsePullRequest)
|
|
366
441
|
|
|
367
442
|
const getAuthenticatedUser = Effect.fn("GitHubService.getAuthenticatedUser")(function*() {
|
|
368
|
-
const viewer = yield* command.
|
|
443
|
+
const viewer = yield* command.runSchema(ViewerSchema, "gh", ["api", "user"])
|
|
369
444
|
return viewer.login
|
|
370
445
|
})
|
|
371
446
|
|
|
@@ -374,8 +449,15 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
374
449
|
return result.stdout
|
|
375
450
|
})
|
|
376
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
|
+
|
|
377
459
|
const getPullRequestMergeInfo = Effect.fn("GitHubService.getPullRequestMergeInfo")(function*(repository: string, number: number) {
|
|
378
|
-
const info = yield* command.
|
|
460
|
+
const info = yield* command.runSchema(MergeInfoResponseSchema, "gh", [
|
|
379
461
|
"pr", "view", String(number), "--repo", repository,
|
|
380
462
|
"--json", "number,title,state,isDraft,mergeable,reviewDecision,autoMergeRequest,statusCheckRollup",
|
|
381
463
|
])
|
|
@@ -404,12 +486,24 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
404
486
|
yield* command.run("gh", ["pr", "close", String(number), "--repo", repository])
|
|
405
487
|
})
|
|
406
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
|
+
|
|
407
501
|
const toggleDraftStatus = Effect.fn("GitHubService.toggleDraftStatus")(function*(repository: string, number: number, isDraft: boolean) {
|
|
408
502
|
yield* command.run("gh", ["pr", "ready", String(number), "--repo", repository, ...(isDraft ? [] : ["--undo"])])
|
|
409
503
|
})
|
|
410
504
|
|
|
411
505
|
const listRepoLabels = Effect.fn("GitHubService.listRepoLabels")(function*(repository: string) {
|
|
412
|
-
const labels = yield* command.
|
|
506
|
+
const labels = yield* command.runSchema(RepoLabelsResponseSchema, "gh", [
|
|
413
507
|
"label", "list", "--repo", repository, "--json", "name,color", "--limit", "100",
|
|
414
508
|
])
|
|
415
509
|
return labels.map((label) => ({ name: label.name, color: `#${label.color}` }))
|
|
@@ -428,9 +522,11 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
428
522
|
listOpenPullRequestDetails,
|
|
429
523
|
getAuthenticatedUser,
|
|
430
524
|
getPullRequestDiff,
|
|
525
|
+
listPullRequestComments,
|
|
431
526
|
getPullRequestMergeInfo,
|
|
432
527
|
mergePullRequest,
|
|
433
528
|
closePullRequest,
|
|
529
|
+
createPullRequestComment,
|
|
434
530
|
toggleDraftStatus,
|
|
435
531
|
listRepoLabels,
|
|
436
532
|
addPullRequestLabel,
|