@kitlangton/ghui 0.1.4 → 0.1.5
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/package.json +1 -1
- package/src/App.tsx +21 -11
- package/src/services/GitHubService.ts +125 -66
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -709,10 +709,6 @@ const FooterHints = ({
|
|
|
709
709
|
<TextLine>
|
|
710
710
|
<span fg={colors.count}>esc</span>
|
|
711
711
|
<span fg={colors.muted}> back </span>
|
|
712
|
-
<span fg={colors.count}>j/k</span>
|
|
713
|
-
<span fg={colors.muted}> scroll </span>
|
|
714
|
-
<span fg={colors.count}>gg/G</span>
|
|
715
|
-
<span fg={colors.muted}> top/bot </span>
|
|
716
712
|
<span fg={colors.count}>v</span>
|
|
717
713
|
<span fg={colors.muted}> view </span>
|
|
718
714
|
<span fg={colors.count}>w</span>
|
|
@@ -734,12 +730,6 @@ const FooterHints = ({
|
|
|
734
730
|
<TextLine>
|
|
735
731
|
<span fg={colors.count}>esc</span>
|
|
736
732
|
<span fg={colors.muted}> back </span>
|
|
737
|
-
<span fg={colors.count}>j/k</span>
|
|
738
|
-
<span fg={colors.muted}> scroll </span>
|
|
739
|
-
<span fg={colors.count}>gg/G</span>
|
|
740
|
-
<span fg={colors.muted}> top/bot </span>
|
|
741
|
-
<span fg={colors.count}>ctrl-d/u</span>
|
|
742
|
-
<span fg={colors.muted}> page </span>
|
|
743
733
|
<span fg={colors.count}>o</span>
|
|
744
734
|
<span fg={colors.muted}> open </span>
|
|
745
735
|
<span fg={colors.count}>y</span>
|
|
@@ -1502,6 +1492,7 @@ export const App = () => {
|
|
|
1502
1492
|
const wideBodyHeight = Math.max(8, (height ?? 24) - 4)
|
|
1503
1493
|
const noticeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
1504
1494
|
const pendingGTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
1495
|
+
const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
1505
1496
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
1506
1497
|
const diffScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
1507
1498
|
const headerFooterWidth = Math.max(24, contentWidth - 2)
|
|
@@ -1523,6 +1514,9 @@ export const App = () => {
|
|
|
1523
1514
|
if (pendingGTimeoutRef.current !== null) {
|
|
1524
1515
|
clearTimeout(pendingGTimeoutRef.current)
|
|
1525
1516
|
}
|
|
1517
|
+
if (diffPrefetchTimeoutRef.current !== null) {
|
|
1518
|
+
clearTimeout(diffPrefetchTimeoutRef.current)
|
|
1519
|
+
}
|
|
1526
1520
|
}, [])
|
|
1527
1521
|
|
|
1528
1522
|
const pullRequestLoad = AsyncResult.getOrElse(pullRequestResult, () => null)
|
|
@@ -1653,6 +1647,22 @@ export const App = () => {
|
|
|
1653
1647
|
})
|
|
1654
1648
|
}
|
|
1655
1649
|
|
|
1650
|
+
useEffect(() => {
|
|
1651
|
+
if (!selectedPullRequest || diffFullView) return
|
|
1652
|
+
if (diffPrefetchTimeoutRef.current !== null) {
|
|
1653
|
+
clearTimeout(diffPrefetchTimeoutRef.current)
|
|
1654
|
+
}
|
|
1655
|
+
diffPrefetchTimeoutRef.current = setTimeout(() => {
|
|
1656
|
+
loadPullRequestDiff(selectedPullRequest)
|
|
1657
|
+
}, 250)
|
|
1658
|
+
return () => {
|
|
1659
|
+
if (diffPrefetchTimeoutRef.current !== null) {
|
|
1660
|
+
clearTimeout(diffPrefetchTimeoutRef.current)
|
|
1661
|
+
diffPrefetchTimeoutRef.current = null
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}, [selectedIndex, selectedPullRequest?.url, diffFullView])
|
|
1665
|
+
|
|
1656
1666
|
const openDiffView = () => {
|
|
1657
1667
|
if (!selectedPullRequest) return
|
|
1658
1668
|
setDiffFullView(true)
|
|
@@ -1883,7 +1893,7 @@ export const App = () => {
|
|
|
1883
1893
|
return
|
|
1884
1894
|
}
|
|
1885
1895
|
|
|
1886
|
-
// Fullscreen detail mode
|
|
1896
|
+
// Fullscreen detail mode handles its own navigation keys.
|
|
1887
1897
|
if (detailFullView) {
|
|
1888
1898
|
if (key.name === "escape" || (key.name === "return" || key.name === "enter")) {
|
|
1889
1899
|
setDetailFullView(false)
|
|
@@ -3,52 +3,105 @@ import { config } from "../config.js"
|
|
|
3
3
|
import type { CheckItem, PullRequestItem } from "../domain.js"
|
|
4
4
|
import { CommandRunner, type CommandError, type JsonParseError } from "./CommandRunner.js"
|
|
5
5
|
|
|
6
|
-
interface
|
|
6
|
+
interface GitHubPullRequestNode {
|
|
7
7
|
readonly number: number
|
|
8
8
|
readonly title: string
|
|
9
9
|
readonly body: string
|
|
10
|
-
readonly labels:
|
|
11
|
-
readonly
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
readonly labels: {
|
|
11
|
+
readonly nodes: readonly {
|
|
12
|
+
readonly name: string
|
|
13
|
+
readonly color?: string | null
|
|
14
|
+
}[]
|
|
15
|
+
}
|
|
14
16
|
readonly additions: number
|
|
15
17
|
readonly deletions: number
|
|
16
18
|
readonly changedFiles: number
|
|
17
19
|
readonly isDraft: boolean
|
|
18
|
-
readonly reviewDecision: string
|
|
19
|
-
readonly statusCheckRollup
|
|
20
|
-
readonly
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
readonly state?: string | null
|
|
25
|
-
}[]
|
|
20
|
+
readonly reviewDecision: string | null
|
|
21
|
+
readonly statusCheckRollup?: {
|
|
22
|
+
readonly contexts: {
|
|
23
|
+
readonly nodes: readonly GraphQLCheckContext[]
|
|
24
|
+
}
|
|
25
|
+
} | null
|
|
26
26
|
readonly state: string
|
|
27
27
|
readonly createdAt: string
|
|
28
28
|
readonly closedAt?: string | null
|
|
29
29
|
readonly url: string
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface GitHubSearchPullRequest {
|
|
33
|
-
readonly number: number
|
|
34
30
|
readonly repository: {
|
|
35
31
|
readonly nameWithOwner: string
|
|
36
32
|
}
|
|
37
33
|
}
|
|
38
34
|
|
|
35
|
+
type GraphQLCheckContext =
|
|
36
|
+
| {
|
|
37
|
+
readonly __typename: "CheckRun"
|
|
38
|
+
readonly name?: string | null
|
|
39
|
+
readonly status?: string | null
|
|
40
|
+
readonly conclusion?: string | null
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
readonly __typename: "StatusContext"
|
|
44
|
+
readonly context?: string | null
|
|
45
|
+
readonly state?: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface GraphQLSearchResponse {
|
|
49
|
+
readonly data: {
|
|
50
|
+
readonly search: {
|
|
51
|
+
readonly nodes: readonly (GitHubPullRequestNode | null)[]
|
|
52
|
+
readonly pageInfo: {
|
|
53
|
+
readonly hasNextPage: boolean
|
|
54
|
+
readonly endCursor: string | null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
39
60
|
interface GitHubViewer {
|
|
40
61
|
readonly login: string
|
|
41
62
|
}
|
|
42
63
|
|
|
43
|
-
const
|
|
44
|
-
|
|
64
|
+
const pullRequestSearchQuery = `
|
|
65
|
+
query PullRequests($searchQuery: String!, $first: Int!, $after: String) {
|
|
66
|
+
search(query: $searchQuery, type: ISSUE, first: $first, after: $after) {
|
|
67
|
+
nodes {
|
|
68
|
+
... on PullRequest {
|
|
69
|
+
number
|
|
70
|
+
title
|
|
71
|
+
body
|
|
72
|
+
isDraft
|
|
73
|
+
reviewDecision
|
|
74
|
+
additions
|
|
75
|
+
deletions
|
|
76
|
+
changedFiles
|
|
77
|
+
state
|
|
78
|
+
createdAt
|
|
79
|
+
closedAt
|
|
80
|
+
url
|
|
81
|
+
repository { nameWithOwner }
|
|
82
|
+
labels(first: 20) { nodes { name color } }
|
|
83
|
+
statusCheckRollup {
|
|
84
|
+
contexts(first: 100) {
|
|
85
|
+
nodes {
|
|
86
|
+
__typename
|
|
87
|
+
... on CheckRun { name status conclusion }
|
|
88
|
+
... on StatusContext { context state }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
pageInfo { hasNextPage endCursor }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
`
|
|
45
98
|
|
|
46
99
|
const normalizeDate = (value: string | null | undefined) => {
|
|
47
100
|
if (!value || value.startsWith("0001-01-01")) return null
|
|
48
101
|
return new Date(value)
|
|
49
102
|
}
|
|
50
103
|
|
|
51
|
-
const getReviewStatus = (item:
|
|
104
|
+
const getReviewStatus = (item: GitHubPullRequestNode): PullRequestItem["reviewStatus"] => {
|
|
52
105
|
if (item.isDraft) return "draft"
|
|
53
106
|
if (item.reviewDecision === "APPROVED") return "approved"
|
|
54
107
|
if (item.reviewDecision === "CHANGES_REQUESTED") return "changes"
|
|
@@ -73,8 +126,22 @@ const normalizeCheckConclusion = (raw?: string | null): CheckItem["conclusion"]
|
|
|
73
126
|
return null
|
|
74
127
|
}
|
|
75
128
|
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
129
|
+
const getContextStatus = (context: GraphQLCheckContext): CheckItem["status"] => {
|
|
130
|
+
if (context.__typename === "CheckRun") return normalizeCheckStatus(context.status)
|
|
131
|
+
if (context.state === "PENDING") return "in_progress"
|
|
132
|
+
return "completed"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const getContextConclusion = (context: GraphQLCheckContext): CheckItem["conclusion"] => {
|
|
136
|
+
if (context.__typename === "CheckRun") return normalizeCheckConclusion(context.conclusion)
|
|
137
|
+
if (context.state === "SUCCESS") return "success"
|
|
138
|
+
if (context.state === "FAILURE" || context.state === "ERROR") return "failure"
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const getCheckInfo = (item: GitHubPullRequestNode): Pick<PullRequestItem, "checkStatus" | "checkSummary" | "checks"> => {
|
|
143
|
+
const contexts = item.statusCheckRollup?.contexts.nodes ?? []
|
|
144
|
+
if (contexts.length === 0) {
|
|
78
145
|
return { checkStatus: "none", checkSummary: null, checks: [] }
|
|
79
146
|
}
|
|
80
147
|
|
|
@@ -84,48 +151,46 @@ const getCheckInfo = (item: GitHubListPullRequest): Pick<PullRequestItem, "check
|
|
|
84
151
|
let failing = false
|
|
85
152
|
const checks: CheckItem[] = []
|
|
86
153
|
|
|
87
|
-
for (const check of
|
|
88
|
-
const name = check.name ?? check.context ?? "check"
|
|
154
|
+
for (const check of contexts) {
|
|
155
|
+
const name = check.__typename === "CheckRun" ? check.name ?? "check" : check.context ?? "check"
|
|
156
|
+
const status = getContextStatus(check)
|
|
157
|
+
const conclusion = getContextConclusion(check)
|
|
89
158
|
|
|
90
|
-
checks.push({
|
|
91
|
-
name,
|
|
92
|
-
status: normalizeCheckStatus(check.status),
|
|
93
|
-
conclusion: normalizeCheckConclusion(check.conclusion),
|
|
94
|
-
})
|
|
159
|
+
checks.push({ name, status, conclusion })
|
|
95
160
|
|
|
96
|
-
if (
|
|
161
|
+
if (status === "completed") {
|
|
97
162
|
completed += 1
|
|
98
163
|
} else {
|
|
99
164
|
pending = true
|
|
100
165
|
}
|
|
101
166
|
|
|
102
|
-
if (
|
|
167
|
+
if (conclusion === "success" || conclusion === "neutral" || conclusion === "skipped") {
|
|
103
168
|
successful += 1
|
|
104
|
-
} else if (
|
|
169
|
+
} else if (conclusion) {
|
|
105
170
|
failing = true
|
|
106
171
|
}
|
|
107
172
|
}
|
|
108
173
|
|
|
109
174
|
if (pending) {
|
|
110
|
-
return { checkStatus: "pending", checkSummary: `checks ${completed}/${
|
|
175
|
+
return { checkStatus: "pending", checkSummary: `checks ${completed}/${contexts.length}`, checks }
|
|
111
176
|
}
|
|
112
177
|
|
|
113
178
|
if (failing) {
|
|
114
|
-
return { checkStatus: "failing", checkSummary: `checks ${successful}/${
|
|
179
|
+
return { checkStatus: "failing", checkSummary: `checks ${successful}/${contexts.length}`, checks }
|
|
115
180
|
}
|
|
116
181
|
|
|
117
|
-
return { checkStatus: "passing", checkSummary: `checks ${successful}/${
|
|
182
|
+
return { checkStatus: "passing", checkSummary: `checks ${successful}/${contexts.length}`, checks }
|
|
118
183
|
}
|
|
119
184
|
|
|
120
|
-
const parsePullRequest = (
|
|
185
|
+
const parsePullRequest = (item: GitHubPullRequestNode): PullRequestItem => {
|
|
121
186
|
const checkInfo = getCheckInfo(item)
|
|
122
187
|
|
|
123
188
|
return {
|
|
124
|
-
repository,
|
|
189
|
+
repository: item.repository.nameWithOwner,
|
|
125
190
|
number: item.number,
|
|
126
191
|
title: item.title,
|
|
127
192
|
body: item.body,
|
|
128
|
-
labels: item.labels.map((label) => ({
|
|
193
|
+
labels: item.labels.nodes.map((label) => ({
|
|
129
194
|
name: label.name,
|
|
130
195
|
color: label.color ? `#${label.color}` : null,
|
|
131
196
|
})),
|
|
@@ -143,22 +208,7 @@ const parsePullRequest = (repository: string, item: GitHubListPullRequest): Pull
|
|
|
143
208
|
}
|
|
144
209
|
}
|
|
145
210
|
|
|
146
|
-
const
|
|
147
|
-
"search",
|
|
148
|
-
"prs",
|
|
149
|
-
"--author",
|
|
150
|
-
author,
|
|
151
|
-
"--state",
|
|
152
|
-
"open",
|
|
153
|
-
"--limit",
|
|
154
|
-
String(config.prFetchLimit),
|
|
155
|
-
"--sort",
|
|
156
|
-
"created",
|
|
157
|
-
"--order",
|
|
158
|
-
"desc",
|
|
159
|
-
"--json",
|
|
160
|
-
searchJsonFields,
|
|
161
|
-
] as const
|
|
211
|
+
const searchQuery = (author: string) => `author:${author} is:pr is:open sort:created-desc`
|
|
162
212
|
|
|
163
213
|
type GitHubError = CommandError | JsonParseError
|
|
164
214
|
|
|
@@ -177,18 +227,27 @@ export class GitHubService extends Context.Service<GitHubService, {
|
|
|
177
227
|
const command = yield* CommandRunner
|
|
178
228
|
|
|
179
229
|
const listOpenPullRequests = Effect.fn("GitHubService.listOpenPullRequests")(function*() {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
230
|
+
const pullRequests: PullRequestItem[] = []
|
|
231
|
+
let cursor: string | null = null
|
|
232
|
+
|
|
233
|
+
while (pullRequests.length < config.prFetchLimit) {
|
|
234
|
+
const pageSize = Math.min(100, config.prFetchLimit - pullRequests.length)
|
|
235
|
+
const response: GraphQLSearchResponse = yield* command.runJson<GraphQLSearchResponse>("gh", [
|
|
236
|
+
"api", "graphql",
|
|
237
|
+
"-f", `query=${pullRequestSearchQuery}`,
|
|
238
|
+
"-F", `searchQuery=${searchQuery(config.author)}`,
|
|
239
|
+
"-F", `first=${pageSize}`,
|
|
240
|
+
...(cursor ? ["-F", `after=${cursor}`] : []),
|
|
241
|
+
])
|
|
242
|
+
|
|
243
|
+
for (const node of response.data.search.nodes) {
|
|
244
|
+
if (node) pullRequests.push(parsePullRequest(node))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!response.data.search.pageInfo.hasNextPage) break
|
|
248
|
+
cursor = response.data.search.pageInfo.endCursor
|
|
249
|
+
if (!cursor) break
|
|
250
|
+
}
|
|
192
251
|
|
|
193
252
|
return pullRequests.sort((left, right) => right.createdAt.getTime() - left.createdAt.getTime())
|
|
194
253
|
})
|