@reviewflow/sdk 0.0.0-stage → 6.22.0
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/LICENSE +15 -0
- package/README.md +86 -2
- package/build/files-node.mjs +70 -0
- package/build/git-node.mjs +1792 -0
- package/build/index-browser.es.js +3328 -0
- package/build/index-node.mjs +3328 -0
- package/package.json +135 -3
- package/src/api/pullRequest.ts +617 -0
- package/src/config/insertCheckoutEntry.ts +54 -0
- package/src/diff/diffPaths.ts +78 -0
- package/src/diff/impacts.ts +47 -0
- package/src/diff/readingOrder.ts +428 -0
- package/src/diff/themes.ts +105 -0
- package/src/diff/types.ts +141 -0
- package/src/files/editableFile.ts +163 -0
- package/src/files.ts +13 -0
- package/src/git/groupDiffPaths.ts +134 -0
- package/src/git/manifests.ts +96 -0
- package/src/git/moduleBindings.ts +571 -0
- package/src/git/pullRequestDiff.ts +159 -0
- package/src/git/readBranchDiff.ts +359 -0
- package/src/git/readImpacts.ts +797 -0
- package/src/git/readingOrder.ts +190 -0
- package/src/git/run.ts +213 -0
- package/src/git.ts +38 -0
- package/src/github/applySuggestions.ts +399 -0
- package/src/github/editPullRequest.ts +790 -0
- package/src/github/fetchCompareDiff.ts +21 -0
- package/src/github/imageProxies.ts +43 -0
- package/src/github/lineDiff.ts +145 -0
- package/src/github/prQuery.ts +298 -0
- package/src/github/pullRequestDetail.ts +1347 -0
- package/src/github/pullRequestHead.ts +65 -0
- package/src/github/pullRequestRevisions.ts +124 -0
- package/src/github/transport.ts +171 -0
- package/src/index.ts +35 -0
- package/src/reviewflow/board.ts +333 -0
- package/src/reviewflow/changedParts.ts +80 -0
- package/src/reviewflow/checksFromReviewflow.ts +75 -0
- package/src/reviewflow/client.ts +589 -0
- package/src/reviewflow/follow.ts +345 -0
- package/src/reviewflow/status.ts +17 -0
- package/src/reviewflow/stream.ts +199 -0
- package/src/types/entities.ts +8 -0
- package/src/types/ids.ts +1 -0
- package/src/types/reviewflow.ts +32 -0
- package/src/utils/pullRequestEdit.ts +108 -0
- package/src/utils/pullRequestPart.ts +12 -0
- package/src/utils/pullRequestReaction.ts +19 -0
- package/src/utils/pullRequestSuggestion.ts +148 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import type { PullRequestRef } from "../api/pullRequest.ts";
|
|
2
|
+
import type { PrState } from "../types/entities.ts";
|
|
3
|
+
import type {
|
|
4
|
+
ReviewflowChecksConclusion,
|
|
5
|
+
ReviewflowMergeable,
|
|
6
|
+
ReviewflowPrChanges,
|
|
7
|
+
ReviewflowReviewState,
|
|
8
|
+
ReviewflowStatusLink,
|
|
9
|
+
} from "../types/reviewflow.ts";
|
|
10
|
+
import type {
|
|
11
|
+
ReviewflowClient,
|
|
12
|
+
ReviewflowGroup,
|
|
13
|
+
ReviewflowPrDetail,
|
|
14
|
+
} from "./client.ts";
|
|
15
|
+
import type { ReviewflowStreamEvent } from "./stream.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The pull requests board, assembled from what the reviewflow client answers.
|
|
19
|
+
* Pure functions over plain data: a caller keeps the snapshot wherever it
|
|
20
|
+
* likes (memory, a database) and turns it into the answer with `assembleBoard`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One pull request of the board, as reviewflow computed it. `null` is
|
|
25
|
+
* reviewflow not knowing, shown as unknown, never guessed.
|
|
26
|
+
*/
|
|
27
|
+
export interface ReviewflowBoardPr {
|
|
28
|
+
/** reviewflow's id */
|
|
29
|
+
id: string;
|
|
30
|
+
ownerRepo: string;
|
|
31
|
+
number: number;
|
|
32
|
+
title: string;
|
|
33
|
+
url: string;
|
|
34
|
+
headRef: string | null;
|
|
35
|
+
state: PrState | null;
|
|
36
|
+
reviewState: ReviewflowReviewState | null;
|
|
37
|
+
author: string | null;
|
|
38
|
+
checksFailed: number;
|
|
39
|
+
checksConclusion: ReviewflowChecksConclusion;
|
|
40
|
+
checksRunning: number;
|
|
41
|
+
/** after reviewflow's allowed-to-fail rules, so possibly fewer than `checksFailed` */
|
|
42
|
+
failedChecks: string[];
|
|
43
|
+
lintFailed: boolean;
|
|
44
|
+
approvedBy: string[];
|
|
45
|
+
changesRequestedBy: string[];
|
|
46
|
+
/** asked for the first time */
|
|
47
|
+
requestedReviewers: string[];
|
|
48
|
+
/** asked again after having reviewed */
|
|
49
|
+
reRequestedReviewers: string[];
|
|
50
|
+
requestedTeams: string[];
|
|
51
|
+
assignees: string[];
|
|
52
|
+
statusLinks: ReviewflowStatusLink[];
|
|
53
|
+
changes: ReviewflowPrChanges | null;
|
|
54
|
+
openedAt: string | null;
|
|
55
|
+
approvedAt: string | null;
|
|
56
|
+
unresolvedThreads: number | null;
|
|
57
|
+
mergeable: ReviewflowMergeable;
|
|
58
|
+
/** reviewflow's last write */
|
|
59
|
+
updatedAt: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Keys and titles are reviewflow's, kept as strings: a bucket it adds later is
|
|
64
|
+
* rendered without a release.
|
|
65
|
+
*/
|
|
66
|
+
export interface ReviewflowBoardBucket {
|
|
67
|
+
key: string;
|
|
68
|
+
title: string;
|
|
69
|
+
/** the bucket's size in reviewflow, accounts added up; `prs` stops at the pages read */
|
|
70
|
+
total: number;
|
|
71
|
+
/** in the bucket's own order */
|
|
72
|
+
prs: ReviewflowBoardPr[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ReviewflowBoardGroup {
|
|
76
|
+
key: string;
|
|
77
|
+
title: string;
|
|
78
|
+
buckets: ReviewflowBoardBucket[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `GET /api/reviewflow/board`, and the data of a `board` event: the boards of
|
|
83
|
+
* every account read, bucket by bucket with their totals added up; `board` is
|
|
84
|
+
* null before any account was read.
|
|
85
|
+
*/
|
|
86
|
+
export interface ReviewflowBoardResponse {
|
|
87
|
+
board: { fetchedAt: string; groups: ReviewflowBoardGroup[] } | null;
|
|
88
|
+
/** the token's GitHub login, named "you" on the board */
|
|
89
|
+
viewer: string | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const toBoardPr = (pr: ReviewflowPrDetail): ReviewflowBoardPr => ({
|
|
93
|
+
id: pr.id,
|
|
94
|
+
ownerRepo: pr.ownerRepo,
|
|
95
|
+
number: pr.number,
|
|
96
|
+
title: pr.title,
|
|
97
|
+
url: pr.url,
|
|
98
|
+
headRef: pr.headRef,
|
|
99
|
+
state: pr.state,
|
|
100
|
+
reviewState: pr.reviewState,
|
|
101
|
+
author: pr.author,
|
|
102
|
+
checksFailed: pr.checks.failed,
|
|
103
|
+
checksConclusion: pr.checks.conclusion,
|
|
104
|
+
checksRunning: pr.checks.running,
|
|
105
|
+
failedChecks: pr.checks.failedNames,
|
|
106
|
+
lintFailed: pr.lintFailed,
|
|
107
|
+
approvedBy: pr.approvedBy,
|
|
108
|
+
changesRequestedBy: pr.changesRequestedBy,
|
|
109
|
+
requestedReviewers: pr.requestedReviewers,
|
|
110
|
+
reRequestedReviewers: pr.reRequestedReviewers,
|
|
111
|
+
requestedTeams: pr.requestedTeams,
|
|
112
|
+
assignees: pr.assignees,
|
|
113
|
+
statusLinks: pr.statusLinks,
|
|
114
|
+
changes: pr.changes,
|
|
115
|
+
openedAt: pr.openedAt,
|
|
116
|
+
approvedAt: pr.approvedAt,
|
|
117
|
+
unresolvedThreads: pr.unresolvedThreads,
|
|
118
|
+
mergeable: pr.mergeable,
|
|
119
|
+
updatedAt: pr.updatedAt,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export interface AccountBoard {
|
|
123
|
+
groups: ReviewflowGroup[];
|
|
124
|
+
/** ISO date of the read */
|
|
125
|
+
fetchedAt: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** each account's last read, keyed by the account as the caller spells it */
|
|
129
|
+
export type ReviewflowBoardSnapshot = Record<string, AccountBoard>;
|
|
130
|
+
|
|
131
|
+
/** the account (owner) a pull request belongs to, lowercased */
|
|
132
|
+
export const accountOfOwnerRepo = (ownerRepo: string): string =>
|
|
133
|
+
ownerRepo.slice(0, ownerRepo.indexOf("/")).toLowerCase();
|
|
134
|
+
|
|
135
|
+
export const isPullRequestOnBoard = (
|
|
136
|
+
board: ReviewflowBoardResponse["board"],
|
|
137
|
+
{ ownerRepo, number }: PullRequestRef,
|
|
138
|
+
): boolean => {
|
|
139
|
+
const wanted = ownerRepo.toLowerCase();
|
|
140
|
+
return (
|
|
141
|
+
board?.groups.some((group) =>
|
|
142
|
+
group.buckets.some((bucket) =>
|
|
143
|
+
bucket.prs.some(
|
|
144
|
+
(pr) => pr.number === number && pr.ownerRepo.toLowerCase() === wanted,
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
) ?? false
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** Replaces one account's read; the other accounts' are left as they are. */
|
|
152
|
+
export const withAccountBoard = (
|
|
153
|
+
snapshot: ReviewflowBoardSnapshot,
|
|
154
|
+
account: string,
|
|
155
|
+
board: AccountBoard,
|
|
156
|
+
): ReviewflowBoardSnapshot => ({ ...snapshot, [account]: board });
|
|
157
|
+
|
|
158
|
+
export interface AccountBoardRead {
|
|
159
|
+
boards: { account: string; groups: ReviewflowGroup[] }[];
|
|
160
|
+
/** an account that failed alone: the others still count */
|
|
161
|
+
errors: { account: string; message: string }[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Reads each account's board (the client follows the extra pages past the
|
|
166
|
+
* first 100 of a bucket). One account the token cannot read fails alone.
|
|
167
|
+
*/
|
|
168
|
+
export const readAccountBoards = async (
|
|
169
|
+
client: Pick<ReviewflowClient, "board">,
|
|
170
|
+
accounts: string[],
|
|
171
|
+
): Promise<AccountBoardRead> => {
|
|
172
|
+
const settled = await Promise.allSettled(
|
|
173
|
+
accounts.map(async (account) => ({
|
|
174
|
+
account,
|
|
175
|
+
groups: await client.board(account),
|
|
176
|
+
})),
|
|
177
|
+
);
|
|
178
|
+
const read: AccountBoardRead = { boards: [], errors: [] };
|
|
179
|
+
settled.forEach((result, index) => {
|
|
180
|
+
if (result.status === "fulfilled") read.boards.push(result.value);
|
|
181
|
+
else {
|
|
182
|
+
const { reason } = result;
|
|
183
|
+
read.errors.push({
|
|
184
|
+
account: accounts[index]!,
|
|
185
|
+
message: reason instanceof Error ? reason.message : String(reason),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
return read;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
interface Flat {
|
|
193
|
+
key: string;
|
|
194
|
+
group: { key: string; title: string };
|
|
195
|
+
title: string;
|
|
196
|
+
total: number;
|
|
197
|
+
position: number;
|
|
198
|
+
prs: ReviewflowBoardPr[];
|
|
199
|
+
fetchedAt: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* One bucket per key across the accounts, totals added up: the board is its
|
|
204
|
+
* accounts' boards side by side. `fetchedAt` is the oldest read, the one the
|
|
205
|
+
* age shown has to answer for. Null when no account was read.
|
|
206
|
+
*/
|
|
207
|
+
export const assembleBoard = (
|
|
208
|
+
snapshot: ReviewflowBoardSnapshot,
|
|
209
|
+
viewer: string | null,
|
|
210
|
+
accounts: string[] = Object.keys(snapshot),
|
|
211
|
+
): ReviewflowBoardResponse => {
|
|
212
|
+
const merged = new Map<string, Flat>();
|
|
213
|
+
const fetchedAts: string[] = [];
|
|
214
|
+
for (const account of accounts) {
|
|
215
|
+
const read = snapshot[account];
|
|
216
|
+
if (read === undefined) continue;
|
|
217
|
+
fetchedAts.push(read.fetchedAt);
|
|
218
|
+
let position = 0;
|
|
219
|
+
for (const group of read.groups) {
|
|
220
|
+
for (const bucket of group.buckets) {
|
|
221
|
+
const existing = merged.get(bucket.key);
|
|
222
|
+
const prs = bucket.prs.map((pr) => toBoardPr(pr));
|
|
223
|
+
if (existing === undefined) {
|
|
224
|
+
merged.set(bucket.key, {
|
|
225
|
+
key: bucket.key,
|
|
226
|
+
group: { key: group.key, title: group.title },
|
|
227
|
+
title: bucket.title,
|
|
228
|
+
total: bucket.total,
|
|
229
|
+
position,
|
|
230
|
+
prs,
|
|
231
|
+
fetchedAt: read.fetchedAt,
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
234
|
+
existing.total += bucket.total;
|
|
235
|
+
existing.position = Math.min(existing.position, position);
|
|
236
|
+
const seen = new Set(existing.prs.map((pr) => pr.id));
|
|
237
|
+
existing.prs.push(...prs.filter((pr) => !seen.has(pr.id)));
|
|
238
|
+
}
|
|
239
|
+
position++;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (merged.size === 0) return { board: null, viewer };
|
|
244
|
+
|
|
245
|
+
const groups: ReviewflowBoardGroup[] = [];
|
|
246
|
+
for (const bucket of [...merged.values()].toSorted(
|
|
247
|
+
(a, b) => a.position - b.position,
|
|
248
|
+
)) {
|
|
249
|
+
const entry: ReviewflowBoardBucket = {
|
|
250
|
+
key: bucket.key,
|
|
251
|
+
title: bucket.title,
|
|
252
|
+
total: bucket.total,
|
|
253
|
+
prs: bucket.prs,
|
|
254
|
+
};
|
|
255
|
+
const last = groups.at(-1);
|
|
256
|
+
if (last?.key === bucket.group.key) last.buckets.push(entry);
|
|
257
|
+
else {
|
|
258
|
+
groups.push({
|
|
259
|
+
key: bucket.group.key,
|
|
260
|
+
title: bucket.group.title,
|
|
261
|
+
buckets: [entry],
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const [oldest] = fetchedAts.toSorted((a, b) => a.localeCompare(b));
|
|
266
|
+
return { board: { fetchedAt: oldest ?? "", groups }, viewer };
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export interface AppliedStreamEvent {
|
|
270
|
+
snapshot: ReviewflowBoardSnapshot;
|
|
271
|
+
/** a row of the snapshot was replaced or removed */
|
|
272
|
+
changed: boolean;
|
|
273
|
+
/**
|
|
274
|
+
* The accounts to read again. The board is not rebuilt from events: its
|
|
275
|
+
* buckets are sorted on fields the stream does not carry, so a pull request
|
|
276
|
+
* that may have changed bucket, entered the scope or left it is the signal to
|
|
277
|
+
* read the account again.
|
|
278
|
+
*/
|
|
279
|
+
readAccounts: string[];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Applies a `pr` or `prRemoved` stream event to the snapshot: a row already on
|
|
284
|
+
* the board is replaced in place (or dropped, its bucket's total one lower),
|
|
285
|
+
* and the account is flagged for a fresh read. Any other event changes nothing.
|
|
286
|
+
*/
|
|
287
|
+
export const applyStreamEvent = (
|
|
288
|
+
snapshot: ReviewflowBoardSnapshot,
|
|
289
|
+
event: ReviewflowStreamEvent,
|
|
290
|
+
): AppliedStreamEvent => {
|
|
291
|
+
if (event.type !== "pr" && event.type !== "prRemoved") {
|
|
292
|
+
return { snapshot, changed: false, readAccounts: [] };
|
|
293
|
+
}
|
|
294
|
+
const id = event.type === "pr" ? event.pr.id : event.id;
|
|
295
|
+
let changed = false;
|
|
296
|
+
const holding = new Set<string>();
|
|
297
|
+
const next: ReviewflowBoardSnapshot = {};
|
|
298
|
+
for (const [account, read] of Object.entries(snapshot)) {
|
|
299
|
+
// oxlint-disable-next-line eslint/no-loop-func
|
|
300
|
+
const groups = read.groups.map((group) => ({
|
|
301
|
+
...group,
|
|
302
|
+
buckets: group.buckets.map((bucket) => {
|
|
303
|
+
if (!bucket.prs.some((pr) => pr.id === id)) return bucket;
|
|
304
|
+
holding.add(account);
|
|
305
|
+
changed = true;
|
|
306
|
+
if (event.type === "pr") {
|
|
307
|
+
return {
|
|
308
|
+
...bucket,
|
|
309
|
+
prs: bucket.prs.map((pr) => (pr.id === id ? event.pr : pr)),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
...bucket,
|
|
314
|
+
total: Math.max(0, bucket.total - 1),
|
|
315
|
+
prs: bucket.prs.filter((pr) => pr.id !== id),
|
|
316
|
+
};
|
|
317
|
+
}),
|
|
318
|
+
}));
|
|
319
|
+
next[account] = { ...read, groups };
|
|
320
|
+
}
|
|
321
|
+
const readAccounts = new Set(holding);
|
|
322
|
+
if (event.type === "pr") {
|
|
323
|
+
const owner = accountOfOwnerRepo(event.pr.ownerRepo);
|
|
324
|
+
for (const account of Object.keys(snapshot)) {
|
|
325
|
+
if (account.toLowerCase() === owner) readAccounts.add(account);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
snapshot: changed ? next : snapshot,
|
|
330
|
+
changed,
|
|
331
|
+
readAccounts: [...readAccounts],
|
|
332
|
+
};
|
|
333
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { PullRequestPart } from "../api/pullRequest.ts";
|
|
2
|
+
import { pullRequestParts } from "../utils/pullRequestPart.ts";
|
|
3
|
+
import type { ReviewflowPrDetail } from "./client.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What each part of the pull request screen shows, as far as reviewflow knows
|
|
7
|
+
* it. Two reads of a part are the same while its fingerprint is: a push moves
|
|
8
|
+
* every part (each names the head), the checks move the checks and only their
|
|
9
|
+
* conclusion moves the header's merge state, a comment moves the timeline
|
|
10
|
+
* alone, an edited description the header alone. The last open conversation
|
|
11
|
+
* being resolved, or a first one opened, moves the merge state too: a branch
|
|
12
|
+
* protection may require every conversation resolved, while a count going
|
|
13
|
+
* from 2 to 1 changes nothing there and stays the timeline's.
|
|
14
|
+
*
|
|
15
|
+
* What reviewflow does not carry is in no fingerprint and moves nothing: a
|
|
16
|
+
* reaction, the viewed marks, the base branch moving under an unchanged pull
|
|
17
|
+
* request ("behind"), the viewer's rights. A read the app makes after some time,
|
|
18
|
+
* or an explicit refresh, reads those.
|
|
19
|
+
*/
|
|
20
|
+
const partFields: Record<PullRequestPart, (pr: ReviewflowPrDetail) => unknown> =
|
|
21
|
+
{
|
|
22
|
+
detail: (pr) => [
|
|
23
|
+
pr.title,
|
|
24
|
+
pr.headRef,
|
|
25
|
+
pr.baseRef,
|
|
26
|
+
pr.headSha,
|
|
27
|
+
pr.baseSha,
|
|
28
|
+
pr.state,
|
|
29
|
+
pr.reviewState,
|
|
30
|
+
pr.author,
|
|
31
|
+
pr.checks.conclusion,
|
|
32
|
+
pr.approvedBy,
|
|
33
|
+
pr.changesRequestedBy,
|
|
34
|
+
pr.requestedReviewers,
|
|
35
|
+
pr.reRequestedReviewers,
|
|
36
|
+
pr.requestedTeams,
|
|
37
|
+
pr.assignees,
|
|
38
|
+
pr.labels,
|
|
39
|
+
pr.mergeable,
|
|
40
|
+
pr.unresolvedThreads === 0,
|
|
41
|
+
pr.reviewActivityAt,
|
|
42
|
+
pr.bodyEditedAt,
|
|
43
|
+
pr.closedAt,
|
|
44
|
+
],
|
|
45
|
+
checks: (pr) => [pr.headSha, pr.checks],
|
|
46
|
+
timeline: (pr) => [
|
|
47
|
+
pr.headSha,
|
|
48
|
+
pr.baseSha,
|
|
49
|
+
pr.baseRef,
|
|
50
|
+
pr.title,
|
|
51
|
+
pr.state,
|
|
52
|
+
pr.labels,
|
|
53
|
+
pr.assignees,
|
|
54
|
+
pr.requestedReviewers,
|
|
55
|
+
pr.reRequestedReviewers,
|
|
56
|
+
pr.requestedTeams,
|
|
57
|
+
pr.approvedBy,
|
|
58
|
+
pr.changesRequestedBy,
|
|
59
|
+
pr.reviewActivityAt,
|
|
60
|
+
pr.commentActivityAt,
|
|
61
|
+
pr.reviewCommentActivityAt,
|
|
62
|
+
pr.unresolvedThreads,
|
|
63
|
+
],
|
|
64
|
+
commits: (pr) => [pr.headSha, pr.baseSha],
|
|
65
|
+
files: (pr) => [pr.headSha, pr.baseSha],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const partFingerprint = (
|
|
69
|
+
pr: ReviewflowPrDetail,
|
|
70
|
+
part: PullRequestPart,
|
|
71
|
+
): string => JSON.stringify(partFields[part](pr));
|
|
72
|
+
|
|
73
|
+
/** The parts of the pull request screen a reviewflow change makes stale. */
|
|
74
|
+
export const changedPullRequestParts = (
|
|
75
|
+
previous: ReviewflowPrDetail,
|
|
76
|
+
next: ReviewflowPrDetail,
|
|
77
|
+
): PullRequestPart[] =>
|
|
78
|
+
pullRequestParts.filter(
|
|
79
|
+
(part) => partFingerprint(previous, part) !== partFingerprint(next, part),
|
|
80
|
+
);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { PullRequestChecksResponse } from "../api/pullRequest.ts";
|
|
2
|
+
import type { GhPullRequestChecks } from "../github/pullRequestDetail.ts";
|
|
3
|
+
import { parsePullRequestChecks } from "../github/pullRequestDetail.ts";
|
|
4
|
+
import type { ReviewflowCheckRun, ReviewflowCommitStatus } from "./client.ts";
|
|
5
|
+
|
|
6
|
+
const upperOrNull = (value: string | null): string | null =>
|
|
7
|
+
value === null ? null : value.toUpperCase();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* reviewflow's copy of the head's checks, read as GitHub's are: its values are
|
|
11
|
+
* the REST api's, lowercase where GraphQL's are uppercase. A run stored before
|
|
12
|
+
* reviewflow kept its status reads as completed when it has a conclusion.
|
|
13
|
+
*
|
|
14
|
+
* The runs are rebuilt as the GraphQL contexts and handed to
|
|
15
|
+
* `parsePullRequestChecks`, so the outcome mapping and the ordering stay the
|
|
16
|
+
* one GitHub-read checks use.
|
|
17
|
+
*/
|
|
18
|
+
export const checksFromReviewflow = (
|
|
19
|
+
{
|
|
20
|
+
checkRuns,
|
|
21
|
+
statuses,
|
|
22
|
+
}: {
|
|
23
|
+
checkRuns: ReviewflowCheckRun[];
|
|
24
|
+
statuses: ReviewflowCommitStatus[];
|
|
25
|
+
},
|
|
26
|
+
at: string,
|
|
27
|
+
): PullRequestChecksResponse => {
|
|
28
|
+
const nodes: NonNullable<
|
|
29
|
+
NonNullable<
|
|
30
|
+
NonNullable<GhPullRequestChecks["headCommits"]["nodes"]>[number]
|
|
31
|
+
>["commit"]["statusCheckRollup"]
|
|
32
|
+
>["contexts"]["nodes"] = [
|
|
33
|
+
...checkRuns.map((run) => ({
|
|
34
|
+
__typename: "CheckRun" as const,
|
|
35
|
+
name: run.name,
|
|
36
|
+
status:
|
|
37
|
+
upperOrNull(run.status) ??
|
|
38
|
+
(run.conclusion === null ? "QUEUED" : "COMPLETED"),
|
|
39
|
+
conclusion: upperOrNull(run.conclusion),
|
|
40
|
+
title: run.title,
|
|
41
|
+
detailsUrl: run.detailsUrl,
|
|
42
|
+
})),
|
|
43
|
+
...statuses.map((status) => ({
|
|
44
|
+
__typename: "StatusContext" as const,
|
|
45
|
+
context: status.context,
|
|
46
|
+
state: status.state.toUpperCase(),
|
|
47
|
+
description: status.description,
|
|
48
|
+
targetUrl: status.targetUrl,
|
|
49
|
+
})),
|
|
50
|
+
];
|
|
51
|
+
const parsed = parsePullRequestChecks({
|
|
52
|
+
data: {
|
|
53
|
+
repository: {
|
|
54
|
+
pullRequest: {
|
|
55
|
+
headCommits: {
|
|
56
|
+
nodes: [
|
|
57
|
+
{
|
|
58
|
+
commit: {
|
|
59
|
+
statusCheckRollup: {
|
|
60
|
+
contexts: { totalCount: nodes.length, nodes },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
errors: [],
|
|
70
|
+
ownerRepo: "",
|
|
71
|
+
number: 0,
|
|
72
|
+
fetchedAt: at,
|
|
73
|
+
});
|
|
74
|
+
return { ...parsed, source: "reviewflow" };
|
|
75
|
+
};
|