@kitlangton/ghui 0.1.7 → 0.1.9
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/.env.example +4 -0
- package/package.json +1 -1
- package/src/App.tsx +219 -1417
- package/src/config.ts +17 -8
- package/src/domain.ts +17 -0
- package/src/mergeActions.ts +90 -0
- package/src/observability.ts +46 -0
- package/src/services/CommandRunner.ts +6 -1
- package/src/services/GitHubService.ts +154 -14
- package/src/ui/DetailsPane.tsx +498 -0
- package/src/ui/FooterHints.tsx +145 -0
- package/src/ui/PullRequestDiffPane.tsx +148 -0
- package/src/ui/PullRequestList.tsx +128 -0
- package/src/ui/colors.ts +28 -0
- package/src/ui/diff.ts +220 -0
- package/src/ui/modals.tsx +249 -0
- package/src/ui/primitives.tsx +111 -0
- package/src/ui/pullRequests.ts +72 -0
package/src/config.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
const parsed = Number.parseInt(value ?? "", 10)
|
|
3
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
|
|
4
|
-
}
|
|
1
|
+
import { Config, Effect } from "effect"
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
const positiveIntOr = (fallback: number) => (value: number) => Number.isFinite(value) && value > 0 ? value : fallback
|
|
4
|
+
|
|
5
|
+
const appConfig = Config.all({
|
|
6
|
+
author: Config.string("GHUI_AUTHOR").pipe(
|
|
7
|
+
Config.withDefault("@me"),
|
|
8
|
+
Config.map((value) => value.trim() || "@me"),
|
|
9
|
+
),
|
|
10
|
+
prFetchLimit: Config.int("GHUI_PR_FETCH_LIMIT").pipe(
|
|
11
|
+
Config.withDefault(200),
|
|
12
|
+
Config.map(positiveIntOr(200)),
|
|
13
|
+
),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const config = Effect.runSync(Effect.gen(function*() {
|
|
17
|
+
return yield* appConfig
|
|
18
|
+
}))
|
package/src/domain.ts
CHANGED
|
@@ -27,7 +27,24 @@ export interface PullRequestItem {
|
|
|
27
27
|
readonly checkStatus: "passing" | "pending" | "failing" | "none"
|
|
28
28
|
readonly checkSummary: string | null
|
|
29
29
|
readonly checks: readonly CheckItem[]
|
|
30
|
+
readonly autoMergeEnabled: boolean
|
|
31
|
+
readonly detailLoaded: boolean
|
|
30
32
|
readonly createdAt: Date
|
|
31
33
|
readonly closedAt: Date | null
|
|
32
34
|
readonly url: string
|
|
33
35
|
}
|
|
36
|
+
|
|
37
|
+
export interface PullRequestMergeInfo {
|
|
38
|
+
readonly repository: string
|
|
39
|
+
readonly number: number
|
|
40
|
+
readonly title: string
|
|
41
|
+
readonly state: PullRequestState
|
|
42
|
+
readonly isDraft: boolean
|
|
43
|
+
readonly mergeable: "mergeable" | "conflicting" | "unknown"
|
|
44
|
+
readonly reviewStatus: PullRequestItem["reviewStatus"]
|
|
45
|
+
readonly checkStatus: PullRequestItem["checkStatus"]
|
|
46
|
+
readonly checkSummary: string | null
|
|
47
|
+
readonly autoMergeEnabled: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type PullRequestMergeAction = "squash" | "auto" | "admin" | "disable-auto"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { PullRequestItem, PullRequestMergeAction, PullRequestMergeInfo } from "./domain.js"
|
|
2
|
+
|
|
3
|
+
export interface MergeActionDefinition {
|
|
4
|
+
readonly action: PullRequestMergeAction
|
|
5
|
+
readonly title: string
|
|
6
|
+
readonly description: string
|
|
7
|
+
readonly cliArgs: readonly string[]
|
|
8
|
+
readonly pastTense: string
|
|
9
|
+
readonly danger?: boolean
|
|
10
|
+
readonly refreshOnSuccess?: boolean
|
|
11
|
+
readonly optimisticAutoMergeEnabled?: boolean
|
|
12
|
+
readonly isAvailable: (info: PullRequestMergeInfo) => boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const isCleanlyMergeable = (info: PullRequestMergeInfo) =>
|
|
16
|
+
info.state === "open" &&
|
|
17
|
+
!info.isDraft &&
|
|
18
|
+
info.mergeable === "mergeable" &&
|
|
19
|
+
info.reviewStatus !== "changes" &&
|
|
20
|
+
info.reviewStatus !== "review" &&
|
|
21
|
+
info.checkStatus !== "pending" &&
|
|
22
|
+
info.checkStatus !== "failing"
|
|
23
|
+
|
|
24
|
+
const mergeActionDefinitions = {
|
|
25
|
+
squash: {
|
|
26
|
+
action: "squash",
|
|
27
|
+
title: "Squash merge now",
|
|
28
|
+
description: "Merge this pull request and delete the branch.",
|
|
29
|
+
cliArgs: ["--squash", "--delete-branch"],
|
|
30
|
+
pastTense: "Merged",
|
|
31
|
+
refreshOnSuccess: true,
|
|
32
|
+
isAvailable: isCleanlyMergeable,
|
|
33
|
+
},
|
|
34
|
+
auto: {
|
|
35
|
+
action: "auto",
|
|
36
|
+
title: "Enable auto-merge",
|
|
37
|
+
description: "Squash merge automatically after GitHub requirements pass.",
|
|
38
|
+
cliArgs: ["--squash", "--auto", "--delete-branch"],
|
|
39
|
+
pastTense: "Enabled auto-merge",
|
|
40
|
+
optimisticAutoMergeEnabled: true,
|
|
41
|
+
isAvailable: (info) => info.state === "open" && !info.autoMergeEnabled && !info.isDraft && info.mergeable !== "conflicting",
|
|
42
|
+
},
|
|
43
|
+
"disable-auto": {
|
|
44
|
+
action: "disable-auto",
|
|
45
|
+
title: "Disable auto-merge",
|
|
46
|
+
description: "Cancel the pending GitHub auto-merge request.",
|
|
47
|
+
cliArgs: ["--disable-auto"],
|
|
48
|
+
pastTense: "Disabled auto-merge",
|
|
49
|
+
optimisticAutoMergeEnabled: false,
|
|
50
|
+
isAvailable: (info) => info.state === "open" && info.autoMergeEnabled,
|
|
51
|
+
},
|
|
52
|
+
admin: {
|
|
53
|
+
action: "admin",
|
|
54
|
+
title: "Admin override merge",
|
|
55
|
+
description: "Bypass unmet merge requirements with --admin.",
|
|
56
|
+
cliArgs: ["--squash", "--admin", "--delete-branch"],
|
|
57
|
+
pastTense: "Admin merged",
|
|
58
|
+
danger: true,
|
|
59
|
+
refreshOnSuccess: true,
|
|
60
|
+
isAvailable: (info) => info.state === "open" && !info.isDraft && info.mergeable !== "conflicting",
|
|
61
|
+
},
|
|
62
|
+
} as const satisfies Record<PullRequestMergeAction, MergeActionDefinition>
|
|
63
|
+
|
|
64
|
+
export const mergeActions = [
|
|
65
|
+
mergeActionDefinitions.squash,
|
|
66
|
+
mergeActionDefinitions.auto,
|
|
67
|
+
mergeActionDefinitions["disable-auto"],
|
|
68
|
+
mergeActionDefinitions.admin,
|
|
69
|
+
] as const satisfies readonly MergeActionDefinition[]
|
|
70
|
+
|
|
71
|
+
export const availableMergeActions = (info: PullRequestMergeInfo | null): readonly MergeActionDefinition[] => {
|
|
72
|
+
if (!info) return []
|
|
73
|
+
return mergeActions.filter((action) => action.isAvailable(info))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const getMergeActionDefinition = (action: PullRequestMergeAction): MergeActionDefinition =>
|
|
77
|
+
mergeActionDefinitions[action]
|
|
78
|
+
|
|
79
|
+
export const mergeInfoFromPullRequest = (pullRequest: PullRequestItem): PullRequestMergeInfo => ({
|
|
80
|
+
repository: pullRequest.repository,
|
|
81
|
+
number: pullRequest.number,
|
|
82
|
+
title: pullRequest.title,
|
|
83
|
+
state: pullRequest.state,
|
|
84
|
+
isDraft: pullRequest.reviewStatus === "draft",
|
|
85
|
+
mergeable: "unknown",
|
|
86
|
+
reviewStatus: pullRequest.reviewStatus,
|
|
87
|
+
checkStatus: pullRequest.checkStatus,
|
|
88
|
+
checkSummary: pullRequest.checkSummary,
|
|
89
|
+
autoMergeEnabled: pullRequest.autoMergeEnabled,
|
|
90
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Config, Effect, Layer } from "effect"
|
|
2
|
+
import { FetchHttpClient } from "effect/unstable/http"
|
|
3
|
+
import { OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/unstable/observability"
|
|
4
|
+
|
|
5
|
+
const observabilityConfig = Config.all({
|
|
6
|
+
endpoint: Config.string("GHUI_OTLP_ENDPOINT").pipe(
|
|
7
|
+
Config.withDefault(""),
|
|
8
|
+
Config.map((value) => value.trim()),
|
|
9
|
+
),
|
|
10
|
+
motelPort: Config.string("GHUI_MOTEL_PORT").pipe(
|
|
11
|
+
Config.withDefault(""),
|
|
12
|
+
Config.map((value) => value.trim()),
|
|
13
|
+
),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const resource = {
|
|
17
|
+
serviceName: "ghui",
|
|
18
|
+
serviceVersion: "local",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Observability = {
|
|
22
|
+
layer: Layer.unwrap(Effect.gen(function*() {
|
|
23
|
+
const { endpoint, motelPort } = yield* observabilityConfig
|
|
24
|
+
const baseUrl = endpoint || (motelPort ? `http://127.0.0.1:${motelPort}` : null)
|
|
25
|
+
|
|
26
|
+
return baseUrl === null
|
|
27
|
+
? Layer.empty
|
|
28
|
+
: Layer.merge(
|
|
29
|
+
OtlpTracer.layer({
|
|
30
|
+
url: `${baseUrl}/v1/traces`,
|
|
31
|
+
exportInterval: "500 millis",
|
|
32
|
+
shutdownTimeout: "1 second",
|
|
33
|
+
resource,
|
|
34
|
+
}),
|
|
35
|
+
OtlpLogger.layer({
|
|
36
|
+
url: `${baseUrl}/v1/logs`,
|
|
37
|
+
exportInterval: "500 millis",
|
|
38
|
+
shutdownTimeout: "1 second",
|
|
39
|
+
resource,
|
|
40
|
+
}),
|
|
41
|
+
).pipe(
|
|
42
|
+
Layer.provide(OtlpSerialization.layerJson),
|
|
43
|
+
Layer.provide(FetchHttpClient.layer),
|
|
44
|
+
)
|
|
45
|
+
})),
|
|
46
|
+
} as const
|
|
@@ -49,7 +49,12 @@ export class CommandRunner extends Context.Service<CommandRunner, {
|
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
const run = Effect.fn("CommandRunner.run")(function*(command: string, args: readonly string[]) {
|
|
52
|
-
const result = yield* runProcess(command, args)
|
|
52
|
+
const result = yield* runProcess(command, args).pipe(Effect.withSpan("ghui.command.runProcess", {
|
|
53
|
+
attributes: {
|
|
54
|
+
"process.command": command,
|
|
55
|
+
"process.argv.count": args.length,
|
|
56
|
+
},
|
|
57
|
+
}))
|
|
53
58
|
if (result.exitCode !== 0) {
|
|
54
59
|
const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode}`
|
|
55
60
|
return yield* new CommandError({ command, args: [...args], detail, cause: detail })
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect"
|
|
2
2
|
import { config } from "../config.js"
|
|
3
|
-
import type { CheckItem, PullRequestItem } from "../domain.js"
|
|
3
|
+
import type { CheckItem, PullRequestItem, PullRequestMergeAction, PullRequestMergeInfo } from "../domain.js"
|
|
4
|
+
import { getMergeActionDefinition } from "../mergeActions.js"
|
|
4
5
|
import { CommandRunner, type CommandError, type JsonParseError } from "./CommandRunner.js"
|
|
5
6
|
|
|
6
|
-
interface
|
|
7
|
+
interface GitHubPullRequestSummaryNode {
|
|
7
8
|
readonly number: number
|
|
8
9
|
readonly title: string
|
|
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
|
+
}
|
|
21
|
+
|
|
22
|
+
interface GitHubPullRequestNode extends GitHubPullRequestSummaryNode {
|
|
9
23
|
readonly body: string
|
|
10
24
|
readonly labels: {
|
|
11
25
|
readonly nodes: readonly {
|
|
@@ -16,20 +30,11 @@ interface GitHubPullRequestNode {
|
|
|
16
30
|
readonly additions: number
|
|
17
31
|
readonly deletions: number
|
|
18
32
|
readonly changedFiles: number
|
|
19
|
-
readonly isDraft: boolean
|
|
20
|
-
readonly reviewDecision: string | null
|
|
21
33
|
readonly statusCheckRollup?: {
|
|
22
34
|
readonly contexts: {
|
|
23
35
|
readonly nodes: readonly GraphQLCheckContext[]
|
|
24
36
|
}
|
|
25
37
|
} | null
|
|
26
|
-
readonly state: string
|
|
27
|
-
readonly createdAt: string
|
|
28
|
-
readonly closedAt?: string | null
|
|
29
|
-
readonly url: string
|
|
30
|
-
readonly repository: {
|
|
31
|
-
readonly nameWithOwner: string
|
|
32
|
-
}
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
type GraphQLCheckContext =
|
|
@@ -57,10 +62,33 @@ interface GraphQLSearchResponse {
|
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
interface GraphQLSearchSummaryResponse {
|
|
66
|
+
readonly data: {
|
|
67
|
+
readonly search: {
|
|
68
|
+
readonly nodes: readonly (GitHubPullRequestSummaryNode | null)[]
|
|
69
|
+
readonly pageInfo: {
|
|
70
|
+
readonly hasNextPage: boolean
|
|
71
|
+
readonly endCursor: string | null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
interface GitHubViewer {
|
|
61
78
|
readonly login: string
|
|
62
79
|
}
|
|
63
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
|
+
|
|
64
92
|
const pullRequestSearchQuery = `
|
|
65
93
|
query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
66
94
|
search(query: $searchQuery, type: ISSUE, first: $first, after: $after) {
|
|
@@ -71,6 +99,7 @@ query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
|
71
99
|
body
|
|
72
100
|
isDraft
|
|
73
101
|
reviewDecision
|
|
102
|
+
autoMergeRequest { enabledAt }
|
|
74
103
|
additions
|
|
75
104
|
deletions
|
|
76
105
|
changedFiles
|
|
@@ -96,12 +125,34 @@ query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
|
96
125
|
}
|
|
97
126
|
`
|
|
98
127
|
|
|
128
|
+
const pullRequestSummarySearchQuery = `
|
|
129
|
+
query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
130
|
+
search(query: $searchQuery, type: ISSUE, first: $first, after: $after) {
|
|
131
|
+
nodes {
|
|
132
|
+
... on PullRequest {
|
|
133
|
+
number
|
|
134
|
+
title
|
|
135
|
+
isDraft
|
|
136
|
+
reviewDecision
|
|
137
|
+
autoMergeRequest { enabledAt }
|
|
138
|
+
state
|
|
139
|
+
createdAt
|
|
140
|
+
closedAt
|
|
141
|
+
url
|
|
142
|
+
repository { nameWithOwner }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
pageInfo { hasNextPage endCursor }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
`
|
|
149
|
+
|
|
99
150
|
const normalizeDate = (value: string | null | undefined) => {
|
|
100
151
|
if (!value || value.startsWith("0001-01-01")) return null
|
|
101
152
|
return new Date(value)
|
|
102
153
|
}
|
|
103
154
|
|
|
104
|
-
const getReviewStatus = (item:
|
|
155
|
+
const getReviewStatus = (item: { readonly isDraft: boolean; readonly reviewDecision: string | null }): PullRequestItem["reviewStatus"] => {
|
|
105
156
|
if (item.isDraft) return "draft"
|
|
106
157
|
if (item.reviewDecision === "APPROVED") return "approved"
|
|
107
158
|
if (item.reviewDecision === "CHANGES_REQUESTED") return "changes"
|
|
@@ -139,8 +190,7 @@ const getContextConclusion = (context: GraphQLCheckContext): CheckItem["conclusi
|
|
|
139
190
|
return null
|
|
140
191
|
}
|
|
141
192
|
|
|
142
|
-
const
|
|
143
|
-
const contexts = item.statusCheckRollup?.contexts.nodes ?? []
|
|
193
|
+
const getCheckInfoFromContexts = (contexts: readonly GraphQLCheckContext[]): Pick<PullRequestItem, "checkStatus" | "checkSummary" | "checks"> => {
|
|
144
194
|
if (contexts.length === 0) {
|
|
145
195
|
return { checkStatus: "none", checkSummary: null, checks: [] }
|
|
146
196
|
}
|
|
@@ -182,6 +232,9 @@ const getCheckInfo = (item: GitHubPullRequestNode): Pick<PullRequestItem, "check
|
|
|
182
232
|
return { checkStatus: "passing", checkSummary: `checks ${successful}/${contexts.length}`, checks }
|
|
183
233
|
}
|
|
184
234
|
|
|
235
|
+
const getCheckInfo = (item: GitHubPullRequestNode): Pick<PullRequestItem, "checkStatus" | "checkSummary" | "checks"> =>
|
|
236
|
+
getCheckInfoFromContexts(item.statusCheckRollup?.contexts.nodes ?? [])
|
|
237
|
+
|
|
185
238
|
const parsePullRequest = (item: GitHubPullRequestNode): PullRequestItem => {
|
|
186
239
|
const checkInfo = getCheckInfo(item)
|
|
187
240
|
|
|
@@ -202,20 +255,52 @@ const parsePullRequest = (item: GitHubPullRequestNode): PullRequestItem => {
|
|
|
202
255
|
checkStatus: checkInfo.checkStatus,
|
|
203
256
|
checkSummary: checkInfo.checkSummary,
|
|
204
257
|
checks: checkInfo.checks,
|
|
258
|
+
autoMergeEnabled: item.autoMergeRequest !== null,
|
|
259
|
+
detailLoaded: true,
|
|
205
260
|
createdAt: new Date(item.createdAt),
|
|
206
261
|
closedAt: normalizeDate(item.closedAt),
|
|
207
262
|
url: item.url,
|
|
208
263
|
}
|
|
209
264
|
}
|
|
210
265
|
|
|
266
|
+
const parsePullRequestSummary = (item: GitHubPullRequestSummaryNode): PullRequestItem => ({
|
|
267
|
+
repository: item.repository.nameWithOwner,
|
|
268
|
+
number: item.number,
|
|
269
|
+
title: item.title,
|
|
270
|
+
body: "",
|
|
271
|
+
labels: [],
|
|
272
|
+
additions: 0,
|
|
273
|
+
deletions: 0,
|
|
274
|
+
changedFiles: 0,
|
|
275
|
+
state: item.state.toLowerCase() === "open" ? "open" : "closed",
|
|
276
|
+
reviewStatus: getReviewStatus(item),
|
|
277
|
+
checkStatus: "none",
|
|
278
|
+
checkSummary: null,
|
|
279
|
+
checks: [],
|
|
280
|
+
autoMergeEnabled: item.autoMergeRequest !== null,
|
|
281
|
+
detailLoaded: false,
|
|
282
|
+
createdAt: new Date(item.createdAt),
|
|
283
|
+
closedAt: normalizeDate(item.closedAt),
|
|
284
|
+
url: item.url,
|
|
285
|
+
})
|
|
286
|
+
|
|
211
287
|
const searchQuery = (author: string) => `author:${author} is:pr is:open sort:created-desc`
|
|
212
288
|
|
|
213
289
|
type GitHubError = CommandError | JsonParseError
|
|
214
290
|
|
|
291
|
+
const normalizeMergeable = (value: string): PullRequestMergeInfo["mergeable"] => {
|
|
292
|
+
if (value === "MERGEABLE") return "mergeable"
|
|
293
|
+
if (value === "CONFLICTING") return "conflicting"
|
|
294
|
+
return "unknown"
|
|
295
|
+
}
|
|
296
|
+
|
|
215
297
|
export class GitHubService extends Context.Service<GitHubService, {
|
|
216
298
|
readonly listOpenPullRequests: () => Effect.Effect<readonly PullRequestItem[], GitHubError>
|
|
299
|
+
readonly listOpenPullRequestDetails: () => Effect.Effect<readonly PullRequestItem[], GitHubError>
|
|
217
300
|
readonly getAuthenticatedUser: () => Effect.Effect<string, GitHubError>
|
|
218
301
|
readonly getPullRequestDiff: (repository: string, number: number) => Effect.Effect<string, CommandError>
|
|
302
|
+
readonly getPullRequestMergeInfo: (repository: string, number: number) => Effect.Effect<PullRequestMergeInfo, GitHubError>
|
|
303
|
+
readonly mergePullRequest: (repository: string, number: number, action: PullRequestMergeAction) => Effect.Effect<void, CommandError>
|
|
219
304
|
readonly toggleDraftStatus: (repository: string, number: number, isDraft: boolean) => Effect.Effect<void, CommandError>
|
|
220
305
|
readonly listRepoLabels: (repository: string) => Effect.Effect<readonly { readonly name: string; readonly color: string | null }[], GitHubError>
|
|
221
306
|
readonly addPullRequestLabel: (repository: string, number: number, label: string) => Effect.Effect<void, CommandError>
|
|
@@ -230,6 +315,32 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
230
315
|
const pullRequests: PullRequestItem[] = []
|
|
231
316
|
let cursor: string | null = null
|
|
232
317
|
|
|
318
|
+
while (pullRequests.length < config.prFetchLimit) {
|
|
319
|
+
const pageSize = Math.min(100, config.prFetchLimit - pullRequests.length)
|
|
320
|
+
const response: GraphQLSearchSummaryResponse = yield* command.runJson<GraphQLSearchSummaryResponse>("gh", [
|
|
321
|
+
"api", "graphql",
|
|
322
|
+
"-f", `query=${pullRequestSummarySearchQuery}`,
|
|
323
|
+
"-F", `searchQuery=${searchQuery(config.author)}`,
|
|
324
|
+
"-F", `first=${pageSize}`,
|
|
325
|
+
...(cursor ? ["-F", `after=${cursor}`] : []),
|
|
326
|
+
])
|
|
327
|
+
|
|
328
|
+
for (const node of response.data.search.nodes) {
|
|
329
|
+
if (node) pullRequests.push(parsePullRequestSummary(node))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!response.data.search.pageInfo.hasNextPage) break
|
|
333
|
+
cursor = response.data.search.pageInfo.endCursor
|
|
334
|
+
if (!cursor) break
|
|
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
|
+
|
|
233
344
|
while (pullRequests.length < config.prFetchLimit) {
|
|
234
345
|
const pageSize = Math.min(100, config.prFetchLimit - pullRequests.length)
|
|
235
346
|
const response: GraphQLSearchResponse = yield* command.runJson<GraphQLSearchResponse>("gh", [
|
|
@@ -262,6 +373,32 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
262
373
|
return result.stdout
|
|
263
374
|
})
|
|
264
375
|
|
|
376
|
+
const getPullRequestMergeInfo = Effect.fn("GitHubService.getPullRequestMergeInfo")(function*(repository: string, number: number) {
|
|
377
|
+
const info = yield* command.runJson<GitHubMergeInfoResponse>("gh", [
|
|
378
|
+
"pr", "view", String(number), "--repo", repository,
|
|
379
|
+
"--json", "number,title,state,isDraft,mergeable,reviewDecision,autoMergeRequest,statusCheckRollup",
|
|
380
|
+
])
|
|
381
|
+
const checkInfo = getCheckInfoFromContexts(info.statusCheckRollup)
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
repository,
|
|
385
|
+
number: info.number,
|
|
386
|
+
title: info.title,
|
|
387
|
+
state: info.state.toLowerCase() === "open" ? "open" : "closed",
|
|
388
|
+
isDraft: info.isDraft,
|
|
389
|
+
mergeable: normalizeMergeable(info.mergeable),
|
|
390
|
+
reviewStatus: getReviewStatus(info),
|
|
391
|
+
checkStatus: checkInfo.checkStatus,
|
|
392
|
+
checkSummary: checkInfo.checkSummary,
|
|
393
|
+
autoMergeEnabled: info.autoMergeRequest !== null,
|
|
394
|
+
} satisfies PullRequestMergeInfo
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
const mergePullRequest = Effect.fn("GitHubService.mergePullRequest")(function*(repository: string, number: number, action: PullRequestMergeAction) {
|
|
398
|
+
const base = ["pr", "merge", String(number), "--repo", repository] as const
|
|
399
|
+
yield* command.run("gh", [...base, ...getMergeActionDefinition(action).cliArgs])
|
|
400
|
+
})
|
|
401
|
+
|
|
265
402
|
const toggleDraftStatus = Effect.fn("GitHubService.toggleDraftStatus")(function*(repository: string, number: number, isDraft: boolean) {
|
|
266
403
|
yield* command.run("gh", ["pr", "ready", String(number), "--repo", repository, ...(isDraft ? [] : ["--undo"])])
|
|
267
404
|
})
|
|
@@ -283,8 +420,11 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
283
420
|
|
|
284
421
|
return GitHubService.of({
|
|
285
422
|
listOpenPullRequests,
|
|
423
|
+
listOpenPullRequestDetails,
|
|
286
424
|
getAuthenticatedUser,
|
|
287
425
|
getPullRequestDiff,
|
|
426
|
+
getPullRequestMergeInfo,
|
|
427
|
+
mergePullRequest,
|
|
288
428
|
toggleDraftStatus,
|
|
289
429
|
listRepoLabels,
|
|
290
430
|
addPullRequestLabel,
|