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